diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathConfig.java index 8f53d20259..f239ab1530 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathConfig.java @@ -132,12 +132,24 @@ default boolean avoidWilderness() { return true; } + @ConfigItem( + keyName = "avoidDangerousNpcs", + name = "Avoid dangerous NPCs", + description = "Route around tiles next to aggressive NPCs that attack you while passing
" + + "(e.g. undead trees at Draynor Manor). A penalty, not a block, so chokepoints still work.", + position = 1, + section = sectionSettings + ) + default boolean avoidDangerousNpcs() { + return true; + } + @ConfigItem( keyName = "useAgilityShortcuts", name = "Use agility shortcuts", description = "Whether to include agility shortcuts in the path.
" + "You must also have the required agility level", - position = 1, + position = 2, section = sectionSettings ) default boolean useAgilityShortcuts() { @@ -149,7 +161,7 @@ default boolean useAgilityShortcuts() { name = "Use grapple shortcuts", description = "Whether to include crossbow grapple agility shortcuts in the path.
" + "You must also have the required agility, ranged and strength levels", - position = 2, + position = 3, section = sectionSettings ) default boolean useGrappleShortcuts() { @@ -161,7 +173,7 @@ default boolean useGrappleShortcuts() { name = "Use boats", description = "Whether to include small boats in the path
" + "(e.g. the boat to Fishing Platform)", - position = 3, + position = 4, section = sectionSettings ) default boolean useBoats() { @@ -172,7 +184,7 @@ default boolean useBoats() { keyName = "useCanoes", name = "Use canoes", description = "Whether to include canoes in the path", - position = 4, + position = 5, section = sectionSettings ) default boolean useCanoes() { @@ -183,7 +195,7 @@ default boolean useCanoes() { keyName = "useCharterShips", name = "Use charter ships", description = "Whether to include charter ships in the path", - position = 5, + position = 6, section = sectionSettings ) default boolean useCharterShips() { @@ -195,7 +207,7 @@ default boolean useCharterShips() { name = "Use ships", description = "Whether to include passenger ships in the path
" + "(e.g. the customs ships to Karamja)", - position = 6, + position = 7, section = sectionSettings ) default boolean useShips() { @@ -207,7 +219,7 @@ default boolean useShips() { name = "Use fairy rings", description = "Whether to include fairy rings in the path.
" + "You must also have completed the required quests or miniquests", - position = 7, + position = 8, section = sectionSettings ) default boolean useFairyRings() { @@ -218,7 +230,7 @@ default boolean useFairyRings() { keyName = "useGnomeGliders", name = "Use gnome gliders", description = "Whether to include gnome gliders in the path", - position = 8, + position = 9, section = sectionSettings ) default boolean useGnomeGliders() { @@ -230,7 +242,7 @@ default boolean useGnomeGliders() { name = "Use minecarts", description = "Whether to include minecarts in the path
" + "(e.g. the Keldagrim and Lovakengj minecart networks)", - position = 9, + position = 10, section = sectionSettings ) default boolean useMinecarts() { @@ -241,7 +253,7 @@ default boolean useMinecarts() { keyName = "useSpiritTrees", name = "Use spirit trees", description = "Whether to include spirit trees in the path", - position = 10, + position = 11, section = sectionSettings ) default boolean useSpiritTrees() { @@ -253,7 +265,7 @@ default boolean useSpiritTrees() { name = "Use teleportation items", description = "Whether to include teleportation items from the player's inventory and equipment.
" + "Options labelled (perm) only use permanent non-charge items.", - position = 11, + position = 12, section = sectionSettings ) default TeleportationItem useTeleportationItems() { @@ -265,7 +277,7 @@ default TeleportationItem useTeleportationItems() { name = "Use teleportation levers", description = "Whether to include teleportation levers in the path
" + "(e.g. the lever from Edgeville to Wilderness)", - position = 12, + position = 13, section = sectionSettings ) default boolean useTeleportationLevers() { @@ -277,7 +289,7 @@ default boolean useTeleportationLevers() { name = "Use teleportation portals", description = "Whether to include teleportation portals in the path
" + "(e.g. the portal from Ferox Enclave to Castle Wars)", - position = 13, + position = 14, section = sectionSettings ) default boolean useTeleportationPortals() { @@ -288,7 +300,7 @@ default boolean useTeleportationPortals() { keyName = "useTeleportationSpells", name = "Use teleportation spells", description = "Whether to include teleportation spells in the path", - position = 14, + position = 15, section = sectionSettings ) default boolean useTeleportationSpells() { @@ -300,7 +312,7 @@ default boolean useTeleportationSpells() { name = "Use teleportation to minigames", description = "Whether to include teleportation to minigames/activities/grouping in the path
" + "(e.g. the Nightmare Zone minigame teleport). These teleports share a 20 minute cooldown.", - position = 15, + position = 16, section = sectionSettings ) default boolean useTeleportationMinigames() { @@ -311,7 +323,7 @@ default boolean useTeleportationMinigames() { keyName = "useWildernessObelisks", name = "Use wilderness obelisks", description = "Whether to include wilderness obelisks in the path", - position = 16, + position = 17, section = sectionSettings ) default boolean useWildernessObelisks() { @@ -322,7 +334,7 @@ default boolean useWildernessObelisks() { keyName = "useNpcs", name = "Use npcs", description = "Whether to include npc transports in the path
(e.g. Tree gnome village maze or Lumbridge cellar)", - position = 17, + position = 18, section = sectionSettings ) default boolean useNpcs() { @@ -333,7 +345,7 @@ default boolean useNpcs() { keyName = "useQuetzals", name = "Use quetzals", description = "Whether to include quetzals in the path.
", - position = 18, + position = 19, section = sectionSettings ) default boolean useQuetzals() { @@ -344,7 +356,7 @@ default boolean useQuetzals() { keyName = "useMagicCarpets", name = "Use Magic Carpets", description = "Whether to include magic carpets in the path.
", - position = 19, + position = 20, section = sectionSettings ) default boolean useMagicCarpets() { @@ -355,7 +367,7 @@ default boolean useMagicCarpets() { keyName = "useHotAirBalloons", name = "Use Hot Air Balloons", description = "Whether to include hot air balloons in the path.", - position = 19, + position = 21, section = sectionSettings ) default boolean useHotAirBalloons() { @@ -366,7 +378,7 @@ default boolean useHotAirBalloons() { keyName = "useMagicMushtrees", name = "Use Magic Mushtrees", description = "Whether to include magic mushtrees in the path.", - position = 19, + position = 22, section = sectionSettings ) default boolean useMagicMushtrees() { @@ -377,7 +389,7 @@ default boolean useMagicMushtrees() { keyName = "useSeasonalTransports", name = "Use Seasonal Transports", description = "Whether to include seasonal League transports (e.g. Map of Alacrity) in the path. League worlds only.", - position = 19, + position = 23, section = sectionSettings ) default boolean useSeasonalTransports() { @@ -388,7 +400,7 @@ default boolean useSeasonalTransports() { keyName = "usePoh", name = "Use Player-owned-house Teleports", description = "Whether to include teleportation through the PoH", - position = 20, + position = 24, section = sectionSettings ) default boolean usePoh() { @@ -400,7 +412,7 @@ default boolean usePoh() { name = "Cancel instead of recalculating", description = "Whether the path should be cancelled rather than recalculated " + "when the recalculate distance limit is exceeded", - position = 21, + position = 25, section = sectionSettings ) default boolean cancelInstead() { @@ -411,7 +423,7 @@ default boolean cancelInstead() { keyName = "showTransportInfo", name = "Show transport info", description = "Whether to display transport destination hint info, e.g. which chat option and text to click", - position = 22, + position = 26, section = sectionSettings ) default boolean showTransportInfo() { @@ -423,7 +435,7 @@ default boolean showTransportInfo() { name = "Teleport distance", description = "Distance before using a teleport
" + "(This is to avoid using teleports when you are to close", - position = 23, + position = 27, section = sectionSettings ) default int distanceBeforeUsingTeleport() { @@ -438,7 +450,7 @@ default int distanceBeforeUsingTeleport() { keyName = "recalculateDistance", name = "Recalculate distance", description = "Distance from the path the player should be for it to be recalculated (-1 for never)", - position = 24, + position = 28, section = sectionSettings ) default int recalculateDistance() { @@ -453,7 +465,7 @@ default int recalculateDistance() { keyName = "finishDistance", name = "Finish distance", description = "Distance from the target tile at which the path should be ended (-1 for never)", - position = 25, + position = 29, section = sectionSettings ) default int reachedDistance() { @@ -464,7 +476,7 @@ default int reachedDistance() { keyName = "showTileCounter", name = "Show tile counter", description = "Whether to display the number of tiles travelled, number of tiles remaining or disable counting", - position = 26, + position = 30, section = sectionSettings ) default TileCounter showTileCounter() { @@ -475,7 +487,7 @@ default TileCounter showTileCounter() { keyName = "tileCounterStep", name = "Tile counter step", description = "The number of tiles between the displayed tile counter numbers", - position = 27, + position = 31, section = sectionSettings ) default int tileCounterStep() @@ -495,7 +507,7 @@ default int tileCounterStep() name = "Calculation cutoff", description = "The cutoff threshold in number of ticks (0.6 seconds) of no progress being
" + "made towards the path target before the calculation will be stopped", - position = 28, + position = 32, section = sectionSettings ) default int calculationCutoff() diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java index b35f09514a..cee0e1570e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/CollisionMap.java @@ -160,6 +160,10 @@ private static int packedPointFromOrdinal(int startPacked, OrdinalDirection dire private volatile long cachedRegionIdTime = 0; private static final long REGION_CACHE_MS = 5000; private static final int TOA_PUZZLE_REGION = 14162; + // Extra g-cost (in tile-distance units) for stepping onto a tile next to an aggressive-NPC + // hazard. High enough to strongly prefer a detour, but a penalty (not a block) so a true + // chokepoint is still traversable. + private static final int DANGEROUS_TILE_PENALTY = 100; private int getCachedRegionId() { long now = System.currentTimeMillis(); @@ -261,7 +265,18 @@ public List getNeighbors(Node node, VisitedTiles visited, PathfinderConfig } if (traversable[i]) { - neighbors.add(new Node(neighborPacked, node)); + if (config.isAvoidDangerousNpcs() + && config.isDangerousAdjacentTile(neighborPacked) + && !targets.contains(neighborPacked)) { + // Penalty (not a skip): the path keeps >=2 tiles from the hazard when a + // reasonable detour exists, but a chokepoint still routes through. + int penalizedCost = node.cost + + WorldPointUtil.distanceBetween(node.packedPosition, neighborPacked) + + DANGEROUS_TILE_PENALTY; + neighbors.add(new Node(neighborPacked, node, penalizedCost)); + } else { + neighbors.add(new Node(neighborPacked, node)); + } } else if (Math.abs(d.x + d.y) == 1 && isBlocked(x + d.x, y + d.y, z)) { // The transport starts from a blocked adjacent tile, e.g. fairy ring // Only checks non-teleport transports (includes portals and levers, but not items and spells) @@ -354,7 +369,16 @@ public List getReverseNeighbors(Node node, VisitedTiles visitedBackward, P } if (traversable[i]) { - neighbors.add(new Node(prevPacked, node)); + if (config.isAvoidDangerousNpcs() && config.isDangerousAdjacentTile(prevPacked)) { + // Mirror the forward danger penalty so the bidirectional search costs the same + // edge consistently from both ends and can't pick a hazard-adjacent meeting. + int penalizedCost = node.cost + + WorldPointUtil.distanceBetween(node.packedPosition, prevPacked) + + DANGEROUS_TILE_PENALTY; + neighbors.add(new Node(prevPacked, node, penalizedCost)); + } else { + neighbors.add(new Node(prevPacked, node)); + } } else if (Math.abs(d.x + d.y) == 1 && isBlocked(WorldPointUtil.unpackWorldX(prevPacked), WorldPointUtil.unpackWorldY(prevPacked), z)) { int wx = WorldPointUtil.unpackWorldX(prevPacked); 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 918c52459c..211e1fdeef 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 @@ -59,6 +59,9 @@ public class PathfinderConfig { private static final WorldPoint SPIRIT_TREE_HOSIDIUS = new WorldPoint(1693, 3540, 0); private static final WorldPoint SPIRIT_TREE_FARMING_GUILD = new WorldPoint(1251, 3750, 0); private static final Set STATIC_BLOCKED_EDGES_PACKED = loadStaticBlockedEdgesFromResources(); + // Tiles within 1 of an aggressive-NPC hazard tile (the melee-aggro ring). Stepping onto one + // gets a high pathfinding penalty when avoidDangerousNpcs is on, so paths keep >=2 tiles away. + private static final Set DANGEROUS_ADJACENT_TILES_PACKED = loadDangerousTilesFromResources(); /** Order matches {@link #spiritTreeDestinationToggle(int)} — add destinations in both places only here + switch. */ private static final WorldPoint[] SPIRIT_TREE_DESTINATIONS_ORDERED = { @@ -102,6 +105,8 @@ public class PathfinderConfig { @Getter private volatile boolean avoidWilderness; @Getter + private volatile boolean avoidDangerousNpcs; + @Getter private volatile boolean useSpiritTrees; private volatile boolean useAgilityShortcuts, useGrappleShortcuts, @@ -192,6 +197,7 @@ public CollisionMap getMap() { public void refresh(WorldPoint target) { calculationCutoffMillis = (long) config.calculationCutoff() * Constants.GAME_TICK_LENGTH; avoidWilderness = ShortestPathPlugin.override("avoidWilderness", config.avoidWilderness()); + avoidDangerousNpcs = ShortestPathPlugin.override("avoidDangerousNpcs", config.avoidDangerousNpcs()); useAgilityShortcuts = ShortestPathPlugin.override("useAgilityShortcuts", config.useAgilityShortcuts()); useGrappleShortcuts = ShortestPathPlugin.override("useGrappleShortcuts", config.useGrappleShortcuts()); useBoats = ShortestPathPlugin.override("useBoats", config.useBoats()); @@ -594,6 +600,43 @@ private static void addStaticEdge(Set edges, WorldPoint origin, WorldPoint WorldPointUtil.packWorldPoint(destination))); } + /** True if {@code packedPoint} is within 1 tile of an aggressive-NPC hazard tile. */ + public boolean isDangerousAdjacentTile(int packedPoint) { + return DANGEROUS_ADJACENT_TILES_PACKED.contains(packedPoint); + } + + private static Set loadDangerousTilesFromResources() { + Set tiles = new HashSet<>(); + final String prefixComment = "#"; + final String delimColumn = "\t"; + + try { + String s = new String(Util.readAllBytes( + ShortestPathPlugin.class.getResourceAsStream("dangerous_tiles.tsv")), StandardCharsets.UTF_8); + Scanner scanner = new Scanner(s); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (line.startsWith(prefixComment) || line.isBlank()) { + continue; + } + WorldPoint hazard = parseBlockedEdgePoint(line.split(delimColumn)[0]); + // Penalize the hazard tile and all 8 neighbours (the melee-aggro ring) so the + // path keeps >=2 tiles away. Hazard tiles themselves are usually blocked anyway. + for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + tiles.add(WorldPointUtil.packWorldPoint( + hazard.getX() + dx, hazard.getY() + dy, hazard.getPlane())); + } + } + } + scanner.close(); + } catch (IOException e) { + throw new RuntimeException("Unable to load shortest-path dangerous tiles", e); + } + + return Collections.unmodifiableSet(tiles); + } + private static boolean blocksWalkingEdgeWhenUnavailable(Transport transport) { if (transport == null || transport.getOrigin() == null || transport.getDestination() == null) { return false; 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 c90ff191d9..750e5dd419 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 @@ -1645,6 +1645,21 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part path.get(i), RECOVERY_MINIMAP_REACH_EUCLIDEAN - 1, wp -> inInstance || isKnownWalkableOrUnloaded(wp)); } + // Don't let recovery park the player on a tile next to an aggressive NPC + // (e.g. an undead tree). The planner avoids those via avoidDangerousNpcs, + // but this runtime fallback would otherwise strand us in melee. Step the + // target back along the path to the nearest non-hazard tile. + PathfinderConfig dangerCfg = ShortestPathPlugin.pathfinderConfig; + if (dangerCfg != null && dangerCfg.isAvoidDangerousNpcs() && recoverTarget != null + && dangerCfg.isDangerousAdjacentTile(WorldPointUtil.packWorldPoint(recoverTarget))) { + int safeIdx = recoverIdx; + while (safeIdx > indexOfStartPoint + && dangerCfg.isDangerousAdjacentTile(WorldPointUtil.packWorldPoint(path.get(safeIdx)))) { + safeIdx--; + } + recoverIdx = safeIdx; + recoverTarget = path.get(safeIdx); + } boolean clicked = recoverTarget != null && !recoverTarget.equals(playerLoc) && Rs2Walker.walkMiniMap(recoverTarget); diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/dangerous_tiles.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/dangerous_tiles.tsv new file mode 100644 index 0000000000..2149505b46 --- /dev/null +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/dangerous_tiles.tsv @@ -0,0 +1,14 @@ +# Tile Display info +# Tiles occupied by aggressive NPCs that attack you while you path next to them. +# When the "Avoid dangerous NPCs" pathfinder option is on, a high step penalty is applied +# to every tile within 1 of these (the melee-aggro ring), so paths keep >=2 tiles away +# when a reasonable detour exists. It is a penalty, not a block: at a true chokepoint the +# path still goes through. Each entry is "x y plane". +3103 3347 0 Draynor Manor undead tree +3107 3342 0 Draynor Manor undead tree +3107 3344 0 Draynor Manor undead tree +3108 3346 0 Draynor Manor undead tree +3111 3339 0 Draynor Manor undead tree +3111 3348 0 Draynor Manor undead tree +3115 3344 0 Draynor Manor undead tree +3120 3344 0 Draynor Manor undead tree