From ace726c709729fc599c03f3851920295390c79a2 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 17 Jan 2026 11:37:10 +0100 Subject: [PATCH] Bump ThievingPlugin version to 2.1.0; enhance ThievingOverlay with target display and NPC strategy integration --- .../microbot/thieving/ThievingNpcOverlay.java | 49 ++++ .../microbot/thieving/ThievingOverlay.java | 19 +- .../microbot/thieving/ThievingPlugin.java | 19 +- .../microbot/thieving/ThievingScript.java | 225 ++++++++++++------ .../thieving/npc/ThievingNpcStrategy.java | 53 +++++ .../thieving/npc/WealthyCitizenStrategy.java | 186 +++++++++++++++ 6 files changed, 475 insertions(+), 76 deletions(-) create mode 100644 src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingNpcOverlay.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/thieving/npc/ThievingNpcStrategy.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/thieving/npc/WealthyCitizenStrategy.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingNpcOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingNpcOverlay.java new file mode 100644 index 0000000000..6f4fb7e939 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingNpcOverlay.java @@ -0,0 +1,49 @@ +package net.runelite.client.plugins.microbot.thieving; + +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; +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.Shape; + +/** + * Draws a simple outline/label on the current thieving target NPC. + */ +public class ThievingNpcOverlay extends Overlay { + private static final Color TARGET_COLOR = new Color(0, 200, 60, 140); + + private final ThievingPlugin plugin; + + @Inject + ThievingNpcOverlay(ThievingPlugin plugin) { + this.plugin = plugin; + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_SCENE); + setNaughty(); + } + + @Override + public Dimension render(Graphics2D graphics) { + final Rs2NpcModel npc = plugin.getThievingScript().getThievingNpc(); + if (npc == null) return null; + + // Compute on client thread to avoid cross-thread actor access issues. + final Shape hull = Microbot.getClientThread().runOnClientThreadOptional(npc::getConvexHull).orElse(null); + if (hull == null) return null; + + OverlayUtil.renderPolygon(graphics, hull, TARGET_COLOR); + + final String label = plugin.getThievingScript().getThievingNpcName(); + Microbot.getClientThread().runOnClientThreadOptional(() -> npc.getCanvasTextLocation(graphics, label, npc.getLogicalHeight() + 40)) + .ifPresent(point -> OverlayUtil.renderTextLocation(graphics, point, label, Color.WHITE)); + + return null; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingOverlay.java index fea86190d5..c38287d772 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingOverlay.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingOverlay.java @@ -2,6 +2,7 @@ import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.thieving.enums.ThievingNpc; +import net.runelite.client.plugins.microbot.thieving.npc.ThievingNpcStrategy; import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.ui.overlay.OverlayPanel; @@ -14,6 +15,7 @@ import java.awt.Dimension; import java.awt.Graphics2D; import java.time.Duration; +import java.util.List; public class ThievingOverlay extends OverlayPanel { private final ThievingPlugin plugin; @@ -45,7 +47,7 @@ private String getShadowVeilString() { @Override public Dimension render(Graphics2D graphics) { try { - panelComponent.setPreferredSize(new Dimension(220, 160)); + panelComponent.setPreferredSize(new Dimension(240, 200)); panelComponent.getChildren().add( TitleComponent.builder() @@ -68,6 +70,13 @@ public Dimension render(Graphics2D graphics) { .build() ); + panelComponent.getChildren().add( + LineComponent.builder() + .left("Target:") + .right(plugin.getConfig().THIEVING_NPC().toString()) + .build() + ); + panelComponent.getChildren().add( LineComponent.builder() .left("NPC:") @@ -113,6 +122,12 @@ public Dimension render(Graphics2D graphics) { .right(getFormattedDuration(plugin.getRunTime())) .build() ); + + final ThievingNpcStrategy strategy = plugin.getThievingScript().getActiveStrategy(); + if (strategy != null) { + final List lines = strategy.overlayLines(plugin.getThievingScript()); + panelComponent.getChildren().addAll(lines); + } } catch (Exception ex) { Microbot.logStackTrace(this.getClass().getSimpleName(), ex); } @@ -126,4 +141,4 @@ private String getFormattedDuration(Duration duration) long seconds = duration.getSeconds() % 60; return String.format("%02d:%02d:%02d", hours, minutes, seconds); } -} \ No newline at end of file +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java index 7dffe1edad..95dca2bc69 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java @@ -34,7 +34,7 @@ ) @Slf4j public class ThievingPlugin extends Plugin { - public static final String version = "2.0.7"; + public static final String version = "2.1.0"; @Inject @Getter @@ -49,6 +49,8 @@ ThievingConfig provideConfig(ConfigManager configManager) { @Inject private ThievingOverlay thievingOverlay; @Inject + private ThievingNpcOverlay thievingNpcOverlay; + @Inject @Getter private ThievingScript thievingScript; @@ -61,16 +63,18 @@ ThievingConfig provideConfig(ConfigManager configManager) { protected void startUp() throws AWTException { if (overlayManager != null) { overlayManager.add(thievingOverlay); + overlayManager.add(thievingNpcOverlay); } startXp = 0; - maxCoinPouch = determineMaxCoinPouch(); + maxCoinPouch = determineMaxCoinPouch(); thievingScript.run(); } protected void shutDown() { thievingScript.shutdown(); overlayManager.remove(thievingOverlay); - maxCoinPouch = 0; + overlayManager.remove(thievingNpcOverlay); + maxCoinPouch = 0; startXp = 0; } @@ -111,11 +115,12 @@ public State getState() { @Subscribe public void onChatMessage(ChatMessage event) { - if (!event.getMessage().toLowerCase().contains("you can only cast shadow veil every 30 seconds.")) { - return; + final String message = event.getMessage().toLowerCase(); + if (message.contains("you can only cast shadow veil every 30 seconds.")) { + log.warn("Attempted to cast shadow veil while it was active"); + getThievingScript().forceShadowVeilActive = System.currentTimeMillis()+30_000; } - log.warn("Attempted to cast shadow veil while it was active"); - getThievingScript().forceShadowVeilActive = System.currentTimeMillis()+30_000; + getThievingScript().onChatMessage(event); } @Subscribe diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java index 0dd71b015b..c88e971aa5 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java @@ -9,15 +9,18 @@ import net.runelite.api.Tile; import net.runelite.api.TileObject; import net.runelite.api.coords.WorldPoint; -import net.runelite.api.NPC; import net.runelite.api.GameState; import net.runelite.api.Skill; +import net.runelite.api.events.ChatMessage; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; import net.runelite.client.plugins.microbot.api.tileitem.Rs2TileItemCache; import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; import net.runelite.client.plugins.microbot.thieving.enums.ThievingNpc; import net.runelite.client.plugins.microbot.thieving.enums.ThievingFood; +import net.runelite.client.plugins.microbot.thieving.npc.ThievingNpcStrategy; +import net.runelite.client.plugins.microbot.thieving.npc.WealthyCitizenStrategy; 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.coords.Rs2WorldPoint; @@ -28,7 +31,6 @@ import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; import net.runelite.client.plugins.microbot.util.models.RS2Item; 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.Login; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; @@ -54,7 +56,6 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.Objects; @Slf4j public class ThievingScript extends Script { @@ -75,12 +76,17 @@ public class ThievingScript extends Script { protected volatile long forceShadowVeilActive = System.currentTimeMillis()-1_000; private long nextShadowVeil = 0; + private long startupGraceUntil = 0; + private final Map npcStrategies = Map.of( + ThievingNpc.WEALTHY_CITIZEN, new WealthyCitizenStrategy() + ); private static final int DOOR_CHECK_RADIUS = 10; private static final ActionTimer DOOR_TIMER = new ActionTimer(); private final long[] doorCloseTime = new long[3]; private int doorCloseIndex = 0; private long lastAction = Long.MAX_VALUE; + private long lastHopAt = 0; protected static int getCloseDoorTime() { return DOOR_TIMER.getRemainingTime(); @@ -106,10 +112,30 @@ public ThievingScript(final ThievingConfig config, final ThievingPlugin plugin) this.plugin = plugin; } +ThievingNpcStrategy getActiveStrategy() { + return npcStrategies.get(config.THIEVING_NPC()); +} + + private String getNpcName(Rs2NpcModel npc) { + if (npc == null) return null; + return Microbot.getClientThread().runOnClientThreadOptional(npc::getName).orElse(null); + } + + private boolean shouldHop() { + return config.THIEVING_NPC() != ThievingNpc.WEALTHY_CITIZEN; + } + + private State applyOverride(State state) { + final ThievingNpcStrategy strategy = getActiveStrategy(); + if (strategy == null) return state; + final State overridden = strategy.overrideState(this, state); + return overridden == null ? state : overridden; + } + private Predicate validateName(Predicate stringPredicate) { return npc -> { if (npc == null) return false; - final String name = npc.getName(); + final String name = getNpcName(npc); if (name == null) return false; return stringPredicate.test(name); }; @@ -118,7 +144,17 @@ private Predicate validateName(Predicate stringPredicate) { protected String getThievingNpcName() { final Rs2NpcModel npc = thievingNpc; if (npc == null) return "null"; - else return thievingNpc.getName(); + else return getNpcName(thievingNpc); + } + + /** + * Ensure we have a current thieving NPC reference; refreshes the cache if needed. + */ + public Rs2NpcModel ensureThievingNpc() { + if (isNpcNull(thievingNpc)) { + thievingNpc = getThievingNpcCache(); + } + return thievingNpc; } private Predicate getThievingNpcFilter() { @@ -129,6 +165,11 @@ private Predicate getThievingNpcFilter() { log.error("Config Thieving NPC is null"); return filter; } + final ThievingNpcStrategy strategy = getActiveStrategy(); + if (strategy != null) { + final Predicate customFilter = strategy.npcFilter(this); + if (customFilter != null) return customFilter; + } switch (finalNpc) { case VYRES: filter = validateName(ThievingData.VYRES::contains); @@ -140,10 +181,6 @@ private Predicate getThievingNpcFilter() { case ELVES: filter = validateName(ThievingData.ELVES::contains); break; - case WEALTHY_CITIZEN: - filter = validateName("Wealthy citizen"::equalsIgnoreCase); - filter = filter.and(npc -> npc != null && npc.isInteracting() && npc.getInteracting() != null); - break; default: filter = validateName(name -> name.toLowerCase().contains(finalNpc.getName())); break; @@ -157,25 +194,30 @@ private Rs2NpcModel getThievingNpcCache() { if (config.THIEVING_NPC() == ThievingNpc.VYRES && startingNpc != null) { comparator = Comparator - .comparing((Rs2NpcModel npc) -> !startingNpc.equalsIgnoreCase(npc.getName())) + .comparing((Rs2NpcModel npc) -> { + final String name = getNpcName(npc); + return name == null || !startingNpc.equalsIgnoreCase(name); + }) .thenComparingInt(Rs2NpcModel::getDistanceFromPlayer); } else { comparator = Comparator.comparingInt(Rs2NpcModel::getDistanceFromPlayer); } - final Optional npcOptional = Rs2Npc.getNpcs() - .filter(getThievingNpcFilter()) + final Optional npcOptional = Microbot.getRs2NpcCache().query() + .where(getThievingNpcFilter()) + .toListOnClientThread() + .stream() .filter(n -> !isNpcNull(n)) .min(comparator); if (npcOptional.isEmpty()) return null; Rs2NpcModel npc = npcOptional.get(); if (startingNpc == null && config.THIEVING_NPC() == ThievingNpc.VYRES) { - startingNpc = npc.getName(); - log.info("Set starting npc to {}", startingNpc); + startingNpc = getNpcName(npc); + log.debug("Set starting npc to {}", startingNpc); } - log.info("Found new NPC={} to thieve @ {}", npc.getName(), toString(npc.getWorldLocation())); + log.debug("Found new NPC={} to thieve @ {}", getNpcName(npc), toString(npc.getWorldLocation())); return npc; } @@ -183,7 +225,7 @@ private T getAttackingNpcs(Function, T> consumer, T defa final Player me = Microbot.getClient().getLocalPlayer(); if (me == null) return defaultValue; - final Rs2NpcModel[] npcs = Rs2Npc.getNpcs().toArray(Rs2NpcModel[]::new); + final Rs2NpcModel[] npcs = Microbot.getRs2NpcCache().query().toList().toArray(Rs2NpcModel[]::new); if (npcs.length == 0) return defaultValue; final Predicate customFilter = config.THIEVING_NPC() == ThievingNpc.VYRES ? @@ -232,21 +274,24 @@ private int getMostExpensiveGroundItemId() { } private State getCurrentState() { - if (getMostExpensiveGroundItemId() != -1) return State.LOOT; + // Grace period right after startup/login to allow inventory/bank to populate. + if (System.currentTimeMillis() < startupGraceUntil) return State.WALK_TO_START; + + if (getMostExpensiveGroundItemId() != -1) return applyOverride(State.LOOT); if (config.escapeAttacking() && (underAttack || isBeingAttackByNpc())) { if (!underAttack) underAttack = true; - return State.ESCAPE; + return applyOverride(State.ESCAPE); } - if (!hasReqs()) return State.BANK; + if (!hasReqs()) return applyOverride(State.BANK); if (config.useFood()) { boolean needToEat = Rs2Player.getHealthPercentage() <= config.hitpoints(); boolean needToDrink = config.food() == ThievingFood.ANCIENT_BREW && !Rs2Player.hasPrayerPoints(); if (needToEat || needToDrink) { - return State.EAT; + return applyOverride(State.EAT); } if (config.food() == ThievingFood.ANCIENT_BREW && @@ -256,29 +301,31 @@ private State getCurrentState() { } } - if (Rs2Inventory.isFull()) return State.DROP; + if (Rs2Inventory.isFull()) return applyOverride(State.DROP); - if (isNpcNull(thievingNpc) && (thievingNpc = getThievingNpcCache()) == null) return State.WALK_TO_START; + if (isNpcNull(thievingNpc) && (thievingNpc = getThievingNpcCache()) == null && shouldHop()) return applyOverride(State.HOP); if (config.THIEVING_NPC() == ThievingNpc.VYRES) { - final WorldPoint[] housePolygon = ThievingData.getVyreHouse(thievingNpc.getName()); + final WorldPoint[] housePolygon = ThievingData.getVyreHouse(getNpcName(thievingNpc)); - if (Rs2Npc.getNpcs() - .filter(Rs2NpcModel.matches(true, "Vyrewatch Sentinel")) + if (Microbot.getRs2NpcCache().query() + .where(Rs2NpcModel.matches(true, "Vyrewatch Sentinel")) + .toListOnClientThread() + .stream() .anyMatch(npc -> isPointInPolygon(housePolygon, npc.getWorldLocation()))) { - log.info("Vyrewatch Sentinel inside house"); - return State.HOP; + log.debug("Vyrewatch Sentinel inside house"); + return applyOverride(State.HOP); } if (!isPointInPolygon(housePolygon, thievingNpc.getWorldLocation())) { if (!sleepUntil(() -> isPointInPolygon(housePolygon, thievingNpc.getWorldLocation()), 1_200 + (int)(Math.random() * 1_800))) { - log.info("Vyre='{}' outside house @ {}", thievingNpc.getName(), toString(thievingNpc.getWorldLocation())); - return State.HOP; + log.debug("Vyre='{}' outside house @ {}", getNpcName(thievingNpc), toString(thievingNpc.getWorldLocation())); + return applyOverride(State.HOP); } } if (!isPointInPolygon(housePolygon, Rs2Player.getWorldLocation())) { - return State.WALK_TO_START; + return applyOverride(State.WALK_TO_START); } // delayed door closing logic @@ -291,23 +338,23 @@ private State getCurrentState() { // did we close the door 3 times in the last 2min? (probably someone troll opening door) if (Arrays.stream(doorCloseTime).allMatch(time -> time != 0 && current - time < 120_000)) { Arrays.fill(doorCloseTime, 0); - return State.HOP; + return applyOverride(State.HOP); } doorCloseTime[doorCloseIndex] = current; doorCloseIndex = (doorCloseIndex+1) % doorCloseTime.length; - return State.CLOSE_DOOR; + return applyOverride(State.CLOSE_DOOR); } } else { // delayed door closing - log.info("Found {} open door(s).", doors.size()); + log.debug("Found {} open door(s).", doors.size()); DOOR_TIMER.set(System.currentTimeMillis()+3_000+(int) (Math.random()*4_000)); } } - if (shouldOpenCoinPouches()) return State.COIN_POUCHES; + if (shouldOpenCoinPouches()) return applyOverride(State.COIN_POUCHES); - if (shouldCastShadowVeil()) return State.SHADOW_VEIL; - return State.PICKPOCKET; + if (shouldCastShadowVeil()) return applyOverride(State.SHADOW_VEIL); + return applyOverride(State.PICKPOCKET); } protected boolean shouldRun() { @@ -315,6 +362,14 @@ protected boolean shouldRun() { return true; } + public void markActionNow() { + lastAction = System.currentTimeMillis(); + } + + public void sleepBriefly(int min, int max) { + sleep(min, max); + } + private boolean sleepUntilWithInterrupt(BooleanSupplier awaitedCondition, BooleanSupplier interruptCondition, int time) { final AtomicBoolean interrupted = new AtomicBoolean(false); final boolean result = sleepUntil(() -> { @@ -337,9 +392,9 @@ private boolean walkTo(String info, WorldPoint dst, int distance) { log.error("{} to null", info); return false; } - log.info("{} to {}", info, toString(dst)); + log.debug("{} to {}", info, toString(dst)); final WalkerState walkerState = Rs2Walker.walkWithState(dst, distance); - log.info("{} @ {} - dst={}", walkerState, Rs2Player.getWorldLocation(), dst); + log.debug("{} @ {} - dst={}", walkerState, Rs2Player.getWorldLocation(), dst); return walkerState == WalkerState.ARRIVED; } @@ -353,7 +408,7 @@ private void loop() { final WorldPoint loc = Rs2Player.getWorldLocation(); if (loc != null) { startingLocation = loc; - log.info("Set starting location to {}", loc); + log.debug("Set starting location to {}", loc); } } @@ -368,7 +423,7 @@ private void loop() { } } - if (currentState != State.PICKPOCKET) log.info("State {}", currentState); + if (currentState != State.PICKPOCKET) log.debug("State {}", currentState); final WorldPoint myLoc; switch(currentState) { @@ -406,8 +461,8 @@ private void loop() { } if (escape == null) { if (thievingNpc == null) thievingNpc = getThievingNpcCache(); - final String name = thievingNpc == null ? null : thievingNpc.getName(); - escape = name == null ? ThievingData.NULL_WORLD_POINT : ThievingData.getVyreEscape(thievingNpc.getName()); + final String name = thievingNpc == null ? null : getNpcName(thievingNpc); + escape = name == null ? ThievingData.NULL_WORLD_POINT : ThievingData.getVyreEscape(name); } if (escape != ThievingData.NULL_WORLD_POINT) { walkTo("Escaping", escape, 3); @@ -416,7 +471,11 @@ private void loop() { if (underAttack) { underAttack = false; if (!isRunning()) return; - hopWorld(); + if (shouldHop()) { + hopWorld(); + } else { + log.debug("Skipping world hop (Wealthy Citizen)"); + } } if (walkTo("Walk to start", startingLocation, 3)) { @@ -448,7 +507,11 @@ private void loop() { if (Rs2Inventory.isFull()) Rs2Player.eatAt(99); return; case HOP: - hopWorld(); + if (shouldHop()) { + hopWorld(); + } else { + log.debug("Skipping world hop (Wealthy Citizen)"); + } return; case COIN_POUCHES: repeatedAction(() -> Rs2Inventory.interact("coin pouch", "Open-all"), () -> !Rs2Inventory.hasItem("coin pouch"), 3); @@ -463,7 +526,7 @@ private void loop() { if (thievingNpc != null) { walkTo("Walk to npc ", thievingNpc.getWorldLocation(), 1); } else { - hopWorld(); + thievingNpc = getThievingNpcCache(); return; } } else { @@ -476,17 +539,21 @@ private void loop() { return; case CLOSE_DOOR: if (isNpcNull(thievingNpc)) return; - final String name = thievingNpc.getName(); + final String name = getNpcName(thievingNpc); final WorldPoint[] house = ThievingData.getVyreHouse(name); final WorldPoint myLoc2 = Rs2Player.getWorldLocation(); if (isPointInPolygon(house, myLoc2)) { - log.info("Closing door {} in {} house", toString(myLoc2), name); + log.debug("Closing door {} in {} house", toString(myLoc2), name); if (closeNearbyDoor(DOOR_CHECK_RADIUS)) DOOR_TIMER.unset(); } else if (isPointInPolygon(house, thievingNpc.getWorldLocation())) { walkTo("Walk to npc", thievingNpc.getWorldLocation(), 1); } else { log.warn("This door close state should never happen"); - hopWorld(); + if (shouldHop()) { + hopWorld(); + } else { + log.debug("Skipping world hop (Wealthy Citizen) during close-door fallback"); + } } return; case PICKPOCKET: @@ -498,10 +565,10 @@ private void loop() { if (config.THIEVING_NPC() != ThievingNpc.VYRES || (new Rs2WorldPoint(myLoc)).distanceToPath(npc.getWorldLocation()) < Integer.MAX_VALUE) { if (equip(ThievingData.ROGUE_SET)) { - log.info("Equipped rogue set"); + log.debug("Equipped rogue set"); } } else { - log.info("Cannot reach {} @ {}", thievingNpc.getName(), thievingNpc.getWorldLocation()); + log.debug("Cannot reach {} @ {}", getNpcName(thievingNpc), thievingNpc.getWorldLocation()); } DOOR_TIMER.set(); return; @@ -509,12 +576,17 @@ private void loop() { } if (!Rs2Equipment.isWearing("dodgy necklace") && Rs2Inventory.hasItem("dodgy necklace")) { - log.info("Equipping dodgy necklace"); + log.debug("Equipping dodgy necklace"); Rs2Inventory.wield("dodgy necklace"); sleepUntilWithInterrupt(() -> Rs2Equipment.isWearing("dodgy necklace"), 1_800); return; } + final ThievingNpcStrategy strategy = getActiveStrategy(); + if (strategy != null && strategy.handlePickpocket(this)) { + return; + } + // limit is so breaks etc. don't cause a high last action time long timeSince = Math.min(System.currentTimeMillis()-lastAction, 1_000); if (timeSince < 250) { @@ -528,7 +600,7 @@ private void loop() { var highlighted = net.runelite.client.plugins.npchighlight.NpcIndicatorsPlugin.getHighlightedNpcs(); if (highlighted.isEmpty()) { if (isNpcNull(thievingNpc)) return; - Rs2Npc.pickpocket(thievingNpc); + Rs2Npc.pickpocket(thievingNpc.getNpc()); } else { Rs2Npc.pickpocket(highlighted); } @@ -545,12 +617,13 @@ public boolean run() { lastAction = System.currentTimeMillis(); nextShadowVeil = System.currentTimeMillis()+60_000; underAttack = false; + startupGraceUntil = System.currentTimeMillis() + 4_000; startTime = Instant.now(); mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try { loop(); } catch (SelfInterruptException ex) { - log.info("Self Interrupt: {}", ex.getMessage()); + log.debug("Self Interrupt: {}", ex.getMessage()); thievingNpc = null; } catch (Exception ex) { log.error("Error in main loop", ex); @@ -561,35 +634,36 @@ public boolean run() { } private boolean hasReqs() { + if (System.currentTimeMillis() < startupGraceUntil) return true; boolean hasReqs = true; if (config.useFood()) { if (config.food() == ThievingFood.ANCIENT_BREW) { if (!hasAncientBrew()) { Rs2Prayer.toggle(Rs2PrayerEnum.REDEMPTION, false); - log.info("Missing ancient brew"); + log.debug("Missing ancient brew"); hasReqs = false; } } else { if (Rs2Inventory.getInventoryFood().isEmpty()) { - log.info("Missing food"); + log.debug("Missing food"); hasReqs = false; } } } if (config.dodgyNecklaceAmount() > 0 && !Rs2Inventory.hasItem("Dodgy necklace")) { - log.info("Missing dodgy necklaces"); + log.debug("Missing dodgy necklaces"); hasReqs = false; } if (config.shadowVeil()) { if (Rs2Inventory.itemQuantity("Cosmic rune") < 5) { - log.info("Missing cosmic runes"); + log.debug("Missing cosmic runes"); hasReqs = false; } boolean hasRunes = Rs2Equipment.isWearing("Lava battlestaff") || Rs2Inventory.hasItem("Earth rune", "Fire rune"); if (!hasRunes) { - log.info("Missing lava battle staff or earth & fire runes"); + log.debug("Missing lava battle staff or earth & fire runes"); hasReqs = false; } } @@ -670,7 +744,7 @@ private boolean shouldOpenCoinPouches() { private boolean isNpcNull(Rs2NpcModel npc) { if (npc == null) return true; - final String name = npc.getName(); + final String name = getNpcName(npc); if (name == null) return true; if (name.isBlank() || name.equalsIgnoreCase("null")) return true; if (npc.getId() == -1) return true; @@ -727,7 +801,7 @@ private boolean closeNearbyDoor(int radius) { return false; } if (Rs2Player.isStunned()) return false; - log.info("Closed door @ {}", toString(doorWp)); + log.debug("Closed door @ {}", toString(doorWp)); doorCount++; } return true; @@ -846,12 +920,12 @@ private void bankAndEquip() { if (!Rs2Bank.isOpen()) { BankLocation bank; if (config.THIEVING_NPC() == ThievingNpc.VYRES && ThievingData.OUTSIDE_HALLOWED_BANK.distanceTo(Rs2Player.getWorldLocation()) < 20) { - log.info("Near Hallowed"); + log.debug("Near Hallowed"); bank = BankLocation.HALLOWED_SEPULCHRE; // it's not needed for banking, but if we have it in inv we should equip it equip(ThievingData.VYRE_SET, false); } else { - log.info("Not Near Hallowed"); + log.debug("Not Near Hallowed"); bank = Rs2Bank.getNearestBank(); if (bank == BankLocation.DARKMEYER) { if (!equip(ThievingData.VYRE_SET)) { @@ -946,7 +1020,11 @@ private void bankAndEquip() { if (underAttack) { underAttack = false; - hopWorld(); + if (shouldHop()) { + hopWorld(); + } else { + log.debug("Skipping world hop (Wealthy Citizen)"); + } } if (walkTo("Return to npc", startingLocation, 3)) { @@ -971,10 +1049,15 @@ private void dropAllExceptImportant() { private void hopWorld() { - thievingNpc = null; + final long now = System.currentTimeMillis(); + if (now - lastHopAt < 15000) { + log.debug("Skipping hop; last hop was {}ms ago", now - lastHopAt); + return; + } + lastHopAt = now; DOOR_TIMER.unset(); final int maxAttempts = 5; - log.info("Hopping world, please wait"); + log.debug("Hopping world, please wait"); for (int attempt = 0; attempt < maxAttempts; attempt++) { final int currentWorld = Microbot.getClient().getWorld(); @@ -997,7 +1080,8 @@ private void hopWorld() { boolean changed = sleepUntilWithInterrupt(() -> Microbot.getClient().getWorld() != currentWorld, attackedInterrupt, 15_000); if (changed) { - log.info("Successful hop world"); + log.debug("Successful hop world"); + thievingNpc = null; // force fresh lookup after hop return; } sleep(250, 350); @@ -1009,7 +1093,7 @@ private void hopWorld() { private void drinkAncientBrew() { for (String dose : ThievingData.ANCIENT_BREW_DOSES) { if (Rs2Inventory.contains(dose) && Rs2Inventory.interact(dose, "Drink")) { - log.info("Drinking: {}", dose); + log.debug("Drinking: {}", dose); sleepUntil(() -> Rs2Player.hasPrayerPoints(), 800); break; } @@ -1023,6 +1107,13 @@ private boolean hasAncientBrew() { return false; } + public void onChatMessage(ChatMessage event) { + final ThievingNpcStrategy strategy = getActiveStrategy(); + if (strategy != null) { + strategy.onChatMessage(event); + } + } + @Override public void shutdown() { super.shutdown(); diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/npc/ThievingNpcStrategy.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/npc/ThievingNpcStrategy.java new file mode 100644 index 0000000000..39448e7ea7 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/npc/ThievingNpcStrategy.java @@ -0,0 +1,53 @@ +package net.runelite.client.plugins.microbot.thieving.npc; + +import net.runelite.api.events.ChatMessage; +import net.runelite.client.plugins.microbot.thieving.State; +import net.runelite.client.plugins.microbot.thieving.ThievingScript; +import net.runelite.client.plugins.microbot.thieving.enums.ThievingNpc; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; +import net.runelite.client.ui.overlay.components.LineComponent; + +import java.util.function.Predicate; +import java.util.Collections; +import java.util.List; + +/** + * Encapsulates NPC-specific behaviour for the thieving script. + * Implementations can override filtering, state selection, pickpocket handling + * and react to chat messages without bloating the main script. + */ +public interface ThievingNpcStrategy { + ThievingNpc getNpc(); + + /** + * Returns the filter used to pick the target NPC when highlights are not set. + */ + Predicate npcFilter(ThievingScript script); + + /** + * Allow an NPC to adjust the state machine decision. + */ + default State overrideState(ThievingScript script, State currentState) { + return currentState; + } + + /** + * Hook executed inside the PICKPOCKET state right before the default interaction. + * Return true to skip the default pickpocket logic for this tick. + */ + default boolean handlePickpocket(ThievingScript script) { + return false; + } + + /** + * Hook for relaying chat messages relevant to the NPC. + */ + default void onChatMessage(ChatMessage event) {} + + /** + * Strategy-specific overlay lines for the main thieving overlay. + */ + default List overlayLines(ThievingScript script) { + return Collections.emptyList(); + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/npc/WealthyCitizenStrategy.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/npc/WealthyCitizenStrategy.java new file mode 100644 index 0000000000..fcb5efe14a --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/npc/WealthyCitizenStrategy.java @@ -0,0 +1,186 @@ +package net.runelite.client.plugins.microbot.thieving.npc; + +import net.runelite.api.events.ChatMessage; +import net.runelite.client.plugins.microbot.thieving.State; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.thieving.ThievingScript; +import net.runelite.client.plugins.microbot.thieving.enums.ThievingNpc; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; +import net.runelite.client.util.Text; +import net.runelite.client.ui.overlay.components.LineComponent; + +import java.util.ArrayList; +import java.util.Set; +import java.util.function.Predicate; +import java.util.List; + +/** + * Wealthy citizen specific behaviour: + * - Track urchin distraction windows (auto-pickpocket, 100% success) + * - Handle the post-world-hop sweaty period where no loot is received + * - Provide a dedicated NPC filter so the main script stays clean + */ +public class WealthyCitizenStrategy implements ThievingNpcStrategy { + private static final long DISTRACTION_DURATION_MS = 20_000L; + private static final long SWEATY_DURATION_MS = 15_000L; + private static final Set URCHINS = Set.of("leo", "julia", "aurelia", "street urchin"); + + private volatile long distractionUntil = 0; + private volatile boolean clickedThisDistraction = false; + private volatile long sweatyUntil = 0; + + @Override + public ThievingNpc getNpc() { + return ThievingNpc.WEALTHY_CITIZEN; + } + + private String npcName(Rs2NpcModel npc) { + if (npc == null) return null; + return Microbot.getClientThread().runOnClientThreadOptional(npc::getName).orElse(null); + } + + private String interactingName(Rs2NpcModel npc) { + if (npc == null) return null; + return Microbot.getClientThread().runOnClientThreadOptional(() -> { + if (!npc.isInteracting() || npc.getInteracting() == null) return null; + return npc.getInteracting().getName(); + }).orElse(null); + } + + @Override + public Predicate npcFilter(ThievingScript script) { + return npc -> { + if (npc == null) return false; + final String name = npcName(npc); + final String interactor = interactingName(npc); + if (name == null || interactor == null) return false; + return "wealthy citizen".equalsIgnoreCase(name) && URCHINS.contains(interactor.toLowerCase()); + }; + } + + @Override + public State overrideState(ThievingScript script, State currentState) { + // During distraction we prefer to stay near the current NPC instead of wandering off. + if (isDistracted() && currentState == State.WALK_TO_START && script.getThievingNpc() != null) { + return State.PICKPOCKET; + } + // If hop would have been selected, just wait near start. + if (currentState == State.HOP) { + return State.WALK_TO_START; + } + if (!isDistracted() && (currentState == State.PICKPOCKET || currentState == State.WALK_TO_START)) { + return script.getThievingNpc() == null ? State.WALK_TO_START : State.IDLE; + } + return currentState; + } + + @Override + public boolean handlePickpocket(ThievingScript script) { + final long now = System.currentTimeMillis(); + + if (now < sweatyUntil) { + // Interaction is slowed and yields no loot right after hopping; wait it out briefly. + script.markActionNow(); + script.sleepBriefly(400, 650); + return true; + } + + final Rs2NpcModel distractedTarget = getDistractedTarget(script); + if (distractedTarget != null) { + // Single click per distraction window; auto-pickpocket handles the rest. + if (!clickedThisDistraction) { + distractedTarget.click("Pickpocket"); + clickedThisDistraction = true; + } + script.markActionNow(); + // Sleep lightly once; subsequent ticks will just idle while distraction lasts. + script.sleepBriefly(300, 500); + return true; + } + + // Not distracted: skip the default pickpocket call this tick, just wait. + script.markActionNow(); + script.sleepBriefly(500, 900); + return true; + } + + @Override + public void onChatMessage(ChatMessage event) { + final String message = Text.removeTags(event.getMessage()).toLowerCase(); + if (message.contains("urchin distract a wealthy citizen")) { + distractionUntil = System.currentTimeMillis() + DISTRACTION_DURATION_MS; + clickedThisDistraction = false; + } else if (message.contains("hands are sweaty and keep dropping the items")) { + sweatyUntil = System.currentTimeMillis() + SWEATY_DURATION_MS; + } + } + + @Override + public List overlayLines(ThievingScript script) { + final List lines = new ArrayList<>(); + lines.add(LineComponent.builder() + .left("Mode") + .right("Wealthy citizen") + .build()); + + final long distractionMs = getDistractionRemainingMs(); + final long sweatyMs = getSweatyRemainingMs(); + + lines.add(LineComponent.builder() + .left("Distracted") + .right(distractionMs > 0 ? formatSeconds(distractionMs) + " left" : "Waiting") + .build()); + + if (sweatyMs > 0) { + lines.add(LineComponent.builder() + .left("Sweaty cooldown") + .right(formatSeconds(sweatyMs) + " left") + .build()); + } + + return lines; + } + + private boolean isDistracted() { + final boolean active = System.currentTimeMillis() < distractionUntil; + if (!active) clickedThisDistraction = false; + return active; + } + + private boolean isNpcValid(Rs2NpcModel npc) { + final String name = npcName(npc); + return npc != null && name != null && !name.isBlank(); + } + + private boolean isDistractedNpc(Rs2NpcModel npc) { + final String targetName = npcName(npc); + final String interactor = interactingName(npc); + return isNpcValid(npc) + && interactor != null + && "wealthy citizen".equalsIgnoreCase(targetName) + && URCHINS.contains(interactor.toLowerCase()); + } + + private long getDistractionRemainingMs() { + return Math.max(0, distractionUntil - System.currentTimeMillis()); + } + + private long getSweatyRemainingMs() { + return Math.max(0, sweatyUntil - System.currentTimeMillis()); + } + + private String formatSeconds(long ms) { + return (ms / 1000) + "s"; + } + + private Rs2NpcModel getDistractedTarget(ThievingScript script) { + if (!isDistracted()) return null; + Rs2NpcModel target = script.ensureThievingNpc(); + if (!isDistractedNpc(target)) { + target = Microbot.getRs2NpcCache().query() + .where(npcFilter(script)) + .nearestOnClientThread(); + } + return isDistractedNpc(target) ? target : null; + } +}