From 7bde0691f44e843d78b2822f3a18360207b8d483 Mon Sep 17 00:00:00 2001 From: Sami Date: Wed, 5 Aug 2026 20:08:13 +0200 Subject: [PATCH 1/5] feat: unify walker planning with pinned upstream core --- .../shortest-path-upstream-drift.yml | 68 + docs/F2P_WEBWALKER_HARNESS.md | 166 +- ...005-walker-transport-execution-boundary.md | 58 + ...0006-upstream-first-planner-convergence.md | 86 + docs/walker-audit.md | 4 + docs/walker-p2-unification.md | 4 + docs/walker-planner-selection-gate.md | 237 +++ docs/walker-roadmap.md | 553 +++++ microbot-cli | 6 + runelite-client/build.gradle.kts | 65 + .../client/plugins/microbot/Script.java | 4 +- .../agentserver/AgentServerPlugin.java | 1 + .../handler/WalkerShadowHandler.java | 167 ++ .../microbot/questhelper/QuestScript.java | 20 +- .../shortestpath/PlannerSelectionMode.java | 48 + .../shortestpath/ShortestPathConfig.java | 13 + .../shortestpath/ShortestPathPlugin.java | 14 +- .../microbot/shortestpath/Transport.java | 140 +- .../TransportExecutionRegistry.java | 341 ++++ .../TransportItemRequirement.java | 280 +++ .../shortestpath/TransportItemResolver.java | 169 ++ .../shortestpath/UPSTREAM_COMPARISON.md | 245 ++- .../WEBWALKER_IMPROVEMENT_PLAN.md | 10 +- .../shortestpath/pathfinder/CollisionMap.java | 42 +- .../shortestpath/pathfinder/Node.java | 23 +- .../shortestpath/pathfinder/PathEdge.java | 126 ++ .../pathfinder/PathTerminationReason.java | 17 + .../shortestpath/pathfinder/Pathfinder.java | 409 ++-- .../pathfinder/PathfinderConfig.java | 248 ++- .../pathfinder/TransportNode.java | 10 +- .../pathfinder/TransportPlanningPolicy.java | 24 + .../microbot/testing/TestRunnerPlugin.java | 111 +- .../webwalker/F2PWebWalkerHarnessPlugin.java | 469 ++++- .../testing/webwalker/F2PWebWalkerRoute.java | 151 +- .../GeLumbridgeTeleportHarnessPlugin.java | 51 +- .../plugins/microbot/util/bank/Rs2Bank.java | 16 +- .../util/depositbox/Rs2DepositBox.java | 15 +- .../LeaguesTransportInjection.java | 20 +- .../leaguetransport/Rs2LeaguesTransport.java | 14 +- .../plugins/microbot/util/magic/Rs2Magic.java | 19 + .../plugins/microbot/util/magic/Rs2Staff.java | 24 +- .../plugins/microbot/util/magic/Rs2Tome.java | 20 +- .../microbot/util/npc/Rs2NpcManager.java | 18 +- .../microbot/util/poh/PohTeleports.java | 2 - .../util/skills/slayer/Rs2Slayer.java | 47 +- .../util/walker/Rs2ActiveRouteStatus.java | 152 ++ .../util/walker/Rs2HotAirBalloon.java | 114 ++ .../microbot/util/walker/Rs2PathApi.java | 1788 ++++++++++++++++- .../walker/Rs2PlannerShadowComparison.java | 118 ++ .../util/walker/Rs2PlannerShadowContext.java | 165 ++ .../walker/Rs2PlannerShadowCoverageStats.java | 32 + .../util/walker/Rs2PlannerShadowStats.java | 120 ++ .../util/walker/Rs2PlanningSnapshot.java | 141 ++ .../microbot/util/walker/Rs2RouteMetrics.java | 105 + .../microbot/util/walker/Rs2RoutePlanner.java | 14 + .../microbot/util/walker/Rs2RoutePolicy.java | 108 + .../microbot/util/walker/Rs2RouteRequest.java | 159 ++ .../microbot/util/walker/Rs2RouteResult.java | 185 ++ .../microbot/util/walker/Rs2RouteStep.java | 53 + .../util/walker/Rs2RouteTermination.java | 11 + .../util/walker/Rs2TerminalTravelMode.java | 9 + .../util/walker/Rs2TransportEdge.java | 121 ++ .../util/walker/Rs2TransportExecutor.java | 25 + .../walker/Rs2TransportItemRequirement.java | 224 +++ .../util/walker/Rs2TransportLoadout.java | 78 + .../walker/Rs2TransportPlanningPolicy.java | 28 + .../util/walker/Rs2TransportType.java | 40 + .../microbot/util/walker/Rs2Walker.java | 1592 ++++++++++----- .../walker/Rs2WalkerShadowExecutionStats.java | 50 + .../util/walker/TransportRouteAnalysis.java | 123 +- .../util/walker/UpstreamRoutePlanner.java | 372 ++++ .../walker/awaits/Rs2WalkerRuntimeAwaits.java | 8 - .../banking/Rs2WalkerBankingPlanner.java | 863 ++++++-- .../util/walker/door/Rs2DoorProbe.java | 24 +- .../lifecycle/Rs2WalkerLifecycleRuntime.java | 98 +- .../util/walker/obstacle/LiveScene.java | 7 +- .../util/walker/obstacle/Rs2LiveScene.java | 14 +- .../walker/obstacle/Rs2ObstacleHandler.java | 2 +- .../walker/obstacle/TransportResolver.java | 10 +- .../util/walker/recovery/RouteRecovery.java | 25 +- .../client/ui/overlay/OverlayRenderer.java | 3 +- .../shortestpath/agility_shortcuts.tsv | 171 +- .../microbot/shortestpath/blocked_edges.tsv | 3 + .../plugins/microbot/shortestpath/canoes.tsv | 102 +- .../microbot/shortestpath/collision-map.zip | Bin 1050209 -> 1200360 bytes .../microbot/shortestpath/minecarts.tsv | 220 +- .../microbot/shortestpath/quetzals.tsv | 58 +- .../plugins/microbot/shortestpath/ships.tsv | 5 + .../microbot/shortestpath/spirit_trees.tsv | 14 +- .../shortestpath/teleportation_items.tsv | 80 +- .../shortestpath/teleportation_minigames.tsv | 10 +- .../shortestpath/teleportation_spells.tsv | 105 +- .../microbot/shortestpath/transports.tsv | 128 +- .../handler/WalkerShadowHandlerTest.java | 48 + .../shortestpath/LiveCollisionTest.java | 8 + .../shortestpath/ShortestPathCoreTest.java | 528 ++++- .../TransportExecutionRegistryTest.java | 196 ++ .../TransportItemRequirementTest.java | 201 ++ .../shortestpath/WalkerRouteCorpusTest.java | 553 ++++- ...hfinderConfigTransportRefreshHashTest.java | 21 + .../PathfinderHomeTeleportTest.java | 39 + .../PathfinderItemRequirementTest.java | 87 + .../PathfinderPathMaterializationTest.java | 107 + .../PathfinderSpecialRequirementTest.java | 41 + .../PathfinderTerminationReasonTest.java | 166 ++ .../TransportPlanningPolicyTest.java | 63 + .../testing/TestRunnerPluginTest.java | 40 + .../webwalker/F2PWebWalkerRouteTest.java | 83 + .../walker/LocalPlannerComparisonMain.java | 643 ++++++ .../util/walker/Rs2HotAirBalloonTest.java | 51 + .../util/walker/Rs2PathApiPlanningTest.java | 1373 +++++++++++++ .../walker/Rs2PlannerShadowContextTest.java | 159 ++ .../util/walker/Rs2WalkerUnitTest.java | 321 ++- .../TargetWalkabilityPreflightTest.java | 16 +- .../walker/TransportRouteAnalysisTest.java | 115 ++ .../util/walker/UpstreamRoutePlannerTest.java | 121 ++ .../BankedTransportItemPlanningTest.java | 286 ++- .../util/walker/door/Rs2DoorProbeTest.java | 48 +- .../walker/obstacle/MineableResolverTest.java | 9 +- .../walker/obstacle/ObstacleRegistryTest.java | 8 +- .../obstacle/TransportResolverTest.java | 9 +- .../walker/recovery/RouteRecoveryTest.java | 15 +- .../client-thread-guardrail-baseline.txt | 82 +- .../src/upstreamPlanner/ADAPTER_PATCHES.md | 12 + runelite-client/src/upstreamPlanner/LICENSE | 25 + runelite-client/src/upstreamPlanner/README.md | 9 + .../src/upstreamPlanner/UPSTREAM_REVISION | 1 + .../main/java/shortestpath/Destination.java | 266 +++ .../shortestpath/DestinationRequirements.java | 97 + .../java/shortestpath/ItemVariations.java | 304 +++ .../java/shortestpath/JewelleryBoxTier.java | 35 + .../shortestpath/PrimitiveIntHashMap.java | 312 +++ .../java/shortestpath/PrimitiveIntList.java | 306 +++ .../java/shortestpath/ShortestPathConfig.java | 1247 ++++++++++++ .../java/shortestpath/ShortestPathPlugin.java | 57 + .../java/shortestpath/TeleportationItem.java | 40 + .../main/java/shortestpath/TileCounter.java | 25 + .../src/main/java/shortestpath/TileStyle.java | 24 + .../src/main/java/shortestpath/Util.java | 78 + .../java/shortestpath/WorldPointUtil.java | 500 +++++ .../shortestpath/leagues/LeagueModeState.java | 189 ++ .../shortestpath/leagues/LeagueRegion.java | 70 + .../leagues/LeagueRegionChecker.java | 168 ++ .../pathfinder/AbstractNodeKind.java | 44 + .../shortestpath/pathfinder/CollisionMap.java | 284 +++ .../shortestpath/pathfinder/EdgeOverride.java | 11 + .../shortestpath/pathfinder/IntDeque.java | 106 + .../shortestpath/pathfinder/IntMinHeap.java | 124 ++ .../shortestpath/pathfinder/NodeGraph.java | 336 ++++ .../pathfinder/OrdinalDirection.java | 22 + .../shortestpath/pathfinder/PathStep.java | 24 + .../pathfinder/PathTerminationReason.java | 9 + .../shortestpath/pathfinder/Pathfinder.java | 417 ++++ .../pathfinder/PathfinderConfig.java | 1136 +++++++++++ .../pathfinder/PathfinderResult.java | 40 + .../shortestpath/pathfinder/SplitFlagMap.java | 171 ++ .../pathfinder/TransportAvailability.java | 130 ++ .../shortestpath/pathfinder/VisitedTiles.java | 193 ++ .../pathfinder/WildernessChecker.java | 68 + .../transport/BankPickupRequirements.java | 471 +++++ .../shortestpath/transport/LoadInterner.java | 48 + .../shortestpath/transport/Transport.java | 643 ++++++ .../transport/TransportLoader.java | 161 ++ .../shortestpath/transport/TransportType.java | 198 ++ .../transport/TransportTypeConfig.java | 162 ++ .../transport/parser/FieldParser.java | 18 + .../parser/ItemRequirementParser.java | 116 ++ .../transport/parser/QuestParser.java | 45 + .../parser/SkillRequirementParser.java | 88 + .../transport/parser/TransportRecord.java | 178 ++ .../transport/parser/TsvParser.java | 94 + .../transport/parser/VarCheckType.java | 23 + .../transport/parser/VarRequirement.java | 136 ++ .../parser/VarRequirementParser.java | 101 + .../transport/parser/WorldPointParser.java | 33 + .../requirement/ItemRequirement.java | 70 + .../transport/requirement/TransportItems.java | 161 ++ scripts/check-shortest-path-boundary.py | 218 ++ scripts/check-shortest-path-upstream.py | 107 + scripts/check-shortest-path-vendored-core.py | 208 ++ scripts/compare-shortest-path-planners.py | 447 +++++ scripts/compare-shortest-path-transports.py | 835 ++++++++ scripts/evaluate-walker-rollout-evidence.py | 447 +++++ scripts/evaluate-walker-shadow-evidence.py | 777 +++++++ ...eport-shortest-path-planner-performance.py | 400 ++++ scripts/run-f2p-webwalker-harness.sh | 15 + scripts/run-ge-lumbridge-teleport-harness.sh | 2 + scripts/shortest-path-planner-corpus.json | 422 ++++ .../shortest-path-planner-harness/README.md | 55 + .../UpstreamPlannerComparisonMain.java | 632 ++++++ .../upstream-exact-transport-identity.patch | 232 +++ .../upstream-planner-comparison.init.gradle | 45 + scripts/shortest-path-transport-baseline.json | 282 +++ scripts/shortest-path-upstream-baseline.json | 88 + .../shortest-path-vendored-core-baseline.json | 24 + .../test_check_shortest_path_boundary.py | 339 ++++ .../test_check_shortest_path_vendored_core.py | 88 + .../test_compare_shortest_path_planners.py | 270 +++ .../test_compare_shortest_path_transports.py | 563 ++++++ .../test_evaluate_walker_rollout_evidence.py | 171 ++ .../test_evaluate_walker_shadow_evidence.py | 324 +++ ...eport_shortest_path_planner_performance.py | 148 ++ 202 files changed, 32953 insertions(+), 1701 deletions(-) create mode 100644 .github/workflows/shortest-path-upstream-drift.yml create mode 100644 docs/decisions/adr-0005-walker-transport-execution-boundary.md create mode 100644 docs/decisions/adr-0006-upstream-first-planner-convergence.md create mode 100644 docs/walker-planner-selection-gate.md create mode 100644 docs/walker-roadmap.md create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WalkerShadowHandler.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/PlannerSelectionMode.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/TransportExecutionRegistry.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/TransportItemRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/TransportItemResolver.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathEdge.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathTerminationReason.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/TransportPlanningPolicy.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2ActiveRouteStatus.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2HotAirBalloon.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowComparison.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowContext.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowCoverageStats.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowStats.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlanningSnapshot.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteMetrics.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RoutePlanner.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RoutePolicy.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteRequest.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteResult.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteStep.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteTermination.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TerminalTravelMode.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportEdge.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportExecutor.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportItemRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportLoadout.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportPlanningPolicy.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportType.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerShadowExecutionStats.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/UpstreamRoutePlanner.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/agentserver/handler/WalkerShadowHandlerTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/TransportExecutionRegistryTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/TransportItemRequirementTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderHomeTeleportTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderItemRequirementTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderPathMaterializationTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderSpecialRequirementTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderTerminationReasonTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/TransportPlanningPolicyTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/testing/TestRunnerPluginTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerRouteTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/LocalPlannerComparisonMain.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2HotAirBalloonTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2PathApiPlanningTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowContextTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/TransportRouteAnalysisTest.java create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/util/walker/UpstreamRoutePlannerTest.java create mode 100644 runelite-client/src/upstreamPlanner/ADAPTER_PATCHES.md create mode 100644 runelite-client/src/upstreamPlanner/LICENSE create mode 100644 runelite-client/src/upstreamPlanner/README.md create mode 100644 runelite-client/src/upstreamPlanner/UPSTREAM_REVISION create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/Destination.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/DestinationRequirements.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/ItemVariations.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/JewelleryBoxTier.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/PrimitiveIntHashMap.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/PrimitiveIntList.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/ShortestPathConfig.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/ShortestPathPlugin.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/TeleportationItem.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/TileCounter.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/TileStyle.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/Util.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/WorldPointUtil.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/leagues/LeagueModeState.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/leagues/LeagueRegion.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/leagues/LeagueRegionChecker.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/AbstractNodeKind.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/CollisionMap.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/EdgeOverride.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/IntDeque.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/IntMinHeap.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/NodeGraph.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/OrdinalDirection.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/PathStep.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/PathTerminationReason.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/Pathfinder.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/PathfinderConfig.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/PathfinderResult.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/SplitFlagMap.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/TransportAvailability.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/VisitedTiles.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/pathfinder/WildernessChecker.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/BankPickupRequirements.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/LoadInterner.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/Transport.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/TransportLoader.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/TransportType.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/TransportTypeConfig.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/parser/FieldParser.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/parser/ItemRequirementParser.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/parser/QuestParser.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/parser/SkillRequirementParser.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/parser/TransportRecord.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/parser/TsvParser.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/parser/VarCheckType.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/parser/VarRequirement.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/parser/VarRequirementParser.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/parser/WorldPointParser.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/requirement/ItemRequirement.java create mode 100644 runelite-client/src/upstreamPlanner/src/main/java/shortestpath/transport/requirement/TransportItems.java create mode 100755 scripts/check-shortest-path-boundary.py create mode 100755 scripts/check-shortest-path-upstream.py create mode 100755 scripts/check-shortest-path-vendored-core.py create mode 100755 scripts/compare-shortest-path-planners.py create mode 100755 scripts/compare-shortest-path-transports.py create mode 100755 scripts/evaluate-walker-rollout-evidence.py create mode 100755 scripts/evaluate-walker-shadow-evidence.py create mode 100755 scripts/report-shortest-path-planner-performance.py create mode 100644 scripts/shortest-path-planner-corpus.json create mode 100644 scripts/shortest-path-planner-harness/README.md create mode 100644 scripts/shortest-path-planner-harness/UpstreamPlannerComparisonMain.java create mode 100644 scripts/shortest-path-planner-harness/upstream-exact-transport-identity.patch create mode 100644 scripts/shortest-path-planner-harness/upstream-planner-comparison.init.gradle create mode 100644 scripts/shortest-path-transport-baseline.json create mode 100644 scripts/shortest-path-upstream-baseline.json create mode 100644 scripts/shortest-path-vendored-core-baseline.json create mode 100644 scripts/tests/test_check_shortest_path_boundary.py create mode 100644 scripts/tests/test_check_shortest_path_vendored_core.py create mode 100644 scripts/tests/test_compare_shortest_path_planners.py create mode 100644 scripts/tests/test_compare_shortest_path_transports.py create mode 100644 scripts/tests/test_evaluate_walker_rollout_evidence.py create mode 100644 scripts/tests/test_evaluate_walker_shadow_evidence.py create mode 100644 scripts/tests/test_report_shortest_path_planner_performance.py diff --git a/.github/workflows/shortest-path-upstream-drift.yml b/.github/workflows/shortest-path-upstream-drift.yml new file mode 100644 index 00000000000..c45d7b0f852 --- /dev/null +++ b/.github/workflows/shortest-path-upstream-drift.yml @@ -0,0 +1,68 @@ +name: Shortest Path upstream drift + +on: + schedule: + - cron: "17 6 * * 1" + workflow_dispatch: + pull_request: + paths: + - ".github/workflows/shortest-path-upstream-drift.yml" + - "scripts/check-shortest-path-upstream.py" + - "scripts/check-shortest-path-boundary.py" + - "scripts/check-shortest-path-vendored-core.py" + - "scripts/compare-shortest-path-transports.py" + - "scripts/compare-shortest-path-planners.py" + - "scripts/evaluate-walker-shadow-evidence.py" + - "scripts/report-shortest-path-planner-performance.py" + - "scripts/shortest-path-planner-corpus.json" + - "scripts/shortest-path-planner-harness/**" + - "scripts/shortest-path-upstream-baseline.json" + - "scripts/shortest-path-vendored-core-baseline.json" + - "scripts/shortest-path-transport-baseline.json" + - "scripts/tests/test_compare_shortest_path_transports.py" + - "scripts/tests/test_compare_shortest_path_planners.py" + - "scripts/tests/test_evaluate_walker_shadow_evidence.py" + - "scripts/tests/test_report_shortest_path_planner_performance.py" + - "scripts/tests/test_check_shortest_path_boundary.py" + - "scripts/tests/test_check_shortest_path_vendored_core.py" + - "runelite-client/src/upstreamPlanner/**" + - "runelite-client/src/main/java/net/runelite/client/plugins/microbot/**" + - "runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/**" + +permissions: + contents: read + +jobs: + check-upstream: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 11 + cache: gradle + - name: Test semantic transport comparison + run: python3 -m unittest scripts/tests/test_compare_shortest_path_transports.py + - name: Test planner comparison orchestration + run: python3 -m unittest scripts/tests/test_compare_shortest_path_planners.py + - name: Test live planner shadow evidence evaluation + run: python3 -m unittest scripts/tests/test_evaluate_walker_shadow_evidence.py + - name: Test planner performance evidence evaluation + run: python3 -m unittest scripts/tests/test_report_shortest_path_planner_performance.py + - name: Test vendored planner pin verification + run: python3 -m unittest scripts/tests/test_check_shortest_path_vendored_core.py + - name: Enforce vendored planner pin and patch surface + run: scripts/check-shortest-path-vendored-core.py + - name: Compare local and reviewed upstream planners + run: scripts/compare-shortest-path-planners.py --require-all + - name: Test shortest-path boundary check + run: python3 -m unittest scripts/tests/test_check_shortest_path_boundary.py + - name: Enforce shortest-path plugin-state boundary + run: python3 scripts/check-shortest-path-boundary.py + - name: Compare reviewed Shortest Path baseline with upstream + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: scripts/check-shortest-path-upstream.py + - name: Enforce reviewed semantic transport baseline + run: scripts/compare-shortest-path-transports.py --check-baseline --limit 20 diff --git a/docs/F2P_WEBWALKER_HARNESS.md b/docs/F2P_WEBWALKER_HARNESS.md index 62a97745659..71a0fdf43e4 100644 --- a/docs/F2P_WEBWALKER_HARNESS.md +++ b/docs/F2P_WEBWALKER_HARNESS.md @@ -35,6 +35,158 @@ scripts/run-f2p-webwalker-harness.sh F2P-15 The runner forwards route settings through `microbot.test.webwalker.*` system properties because the Gradle `runTest` task only propagates `microbot.test.*` properties into the launched client JVM. +`TestRunnerPlugin` starts before ordinary plugins and clears the persisted enabled flag for the selected +test target. It starts that target only after a game tick reports `LOGGED_IN`, a local player is present and +the welcome-screen Play widget is no longer visible, then clears the enabled flag again while leaving the +target active for the current process. This readiness contract is intentional: RuneLite can report +`LOGGED_IN` and expose a local player before the welcome overlay has stopped blocking interaction, and a +persisted harness flag must not start its private route timeout during the next client's login sequence. + +## Planner modes and evidence + +Planner selection is explicit and defaults to `LOCAL`: + +- `LOCAL` runs only the local planner; +- `SHADOW` executes local and compares upstream asynchronously; +- `UPSTREAM_F2P_CANARY` calculates both candidates for non-members policy and selects upstream only after a + semantic match. Members-policy requests remain local. + +Enable shadow evidence for a harness run with: + +```bash +MICROBOT_WEBWALKER_PLANNER_MODE=SHADOW \ +scripts/run-f2p-webwalker-harness.sh F2P-17 +``` + +`MICROBOT_WEBWALKER_UPSTREAM_PLANNER_SHADOW=true` remains a harness compatibility alias for `SHADOW`; new +automation should set the mode directly. + +The harness waits up to two minutes for the bounded shadow worker to settle and embeds the same +coordinate-free schema-v2 object served by `/walker/shadow` under `shadowEvidence` in `result.json`. An +enabled run fails if it submits no comparison, leaves work pending, or observes a semantic divergence or +planner failure. Extract and run the full coverage evaluator with: + +```bash +jq '.shadowEvidence' ~/.runelite/test-results/f2p-webwalker/result.json \ + > build/walker-shadow-snapshot.json +scripts/evaluate-walker-shadow-evidence.py \ + build/walker-shadow-snapshot.json \ + --json-output build/walker-shadow-evidence.json \ + --markdown-output build/walker-shadow-evidence.md +``` + +A single route can prove that evidence collection works but cannot satisfy the production selection gate's +cross-category minimums. Run the representative route mix described in `docs/walker-planner-selection-gate.md`. + +The accepted 2026-08-05 aggregate combines 12 independently validated fresh-client snapshots and passes the +full F2P live gate: 141/141 semantic matches, 75 active routes, 15 active replans, 11 recovery replans, 39 +underground comparisons, 18 walking-only cave selections and 71 exact arrivals, with no divergence, planner +failure, pending/discarded work, unreachable outcome or exit. Keep the inputs separate and use the evaluator; +do not treat one long process or hand-added counters as equivalent evidence. + +Run the opt-in F2P selection canary separately: + +```bash +MICROBOT_WEBWALKER_PLANNER_MODE=UPSTREAM_F2P_CANARY \ +MICROBOT_WEBWALKER_OUTPUT_DIR=/tmp/microbot-f2p-canary \ +scripts/run-f2p-webwalker-harness.sh F2P-17 +``` + +The canary keeps the active route calculating until both planners have completed, then atomically exposes one +selected route. It fails evidence collection if an ordinary canary run records a semantic divergence, planner +failure or no upstream selection. The accepted 2026-08-05 underground run completed five repetitions, made +ten upstream selections and recorded ten arrivals with no divergence or failure. + +The test-only forced-failure mode proves the release-independent local fallback without changing production +failure handling: + +```bash +MICROBOT_WEBWALKER_PLANNER_MODE=UPSTREAM_F2P_CANARY \ +MICROBOT_WEBWALKER_FORCE_UPSTREAM_FAILURE=true \ +MICROBOT_WEBWALKER_EXPECT_LOCAL_FALLBACK=true \ +MICROBOT_WEBWALKER_OUTPUT_DIR=/tmp/microbot-f2p-rollback \ +scripts/run-f2p-webwalker-harness.sh F2P-17 +``` + +The failure hook is honored only in test mode. An accepted rollback run requires planner failures and local +failure fallbacks, requires zero upstream selections, and still requires every live route arrival. The +2026-08-05 run recorded ten injected failures, ten local fallbacks and ten arrivals. Terminal outcomes are +bound to the generation-matched ready route, so `LOCAL` walks and members-policy walks under the F2P canary +cannot inflate these counts; the normal and forced-failure F2P-17 runs each retained all ten eligible arrivals. + +Evaluate the two fresh-client artifacts as one release decision instead of reviewing their embedded checks +independently: + +```bash +scripts/evaluate-walker-rollout-evidence.py \ + /tmp/microbot-f2p-canary/result.json \ + /tmp/microbot-f2p-rollback/result.json \ + --json-output build/walker-f2p-rollout-evidence.json \ + --markdown-output build/walker-f2p-rollout-evidence.md +``` + +The paired evaluator requires the pinned candidate, distinct client sessions, the same required route set, +settled accounting, ten normal upstream selections and arrivals, and ten forced failure fallbacks and +arrivals. Its report is coordinate-free and rejects exception-message exposure. + +Prerequisite-bearing selection-gate routes are intentionally excluded from the default fresh-account suite. +Run them explicitly on a suitable profile: + +```bash +MICROBOT_WEBWALKER_PLANNER_MODE=SHADOW \ +scripts/run-f2p-webwalker-harness.sh F2P-18 +``` + +It first performs ten real `compareRoutes` calls and requires all ten explicit bank-to-target shadow legs to +settle and select an item-gated transport. It then performs three Lumbridge-to-Champions' Guild repetitions, +disables agility shortcuts and teleports, enables canoes, and requires at least five completed `CANOE` shadow +selections in addition to exact arrivals. The accepted 2026-08-05 rerun produced 49/49 matching comparisons, +10/10 matching item-gated bank legs and six exact terminal arrivals. + +The representative terminal-travel slice requires at least 90 coins for two outbound journeys and one reverse +setup journey: + +```bash +MICROBOT_WEBWALKER_PLANNER_MODE=SHADOW \ +scripts/run-f2p-webwalker-harness.sh F2P-19 +``` + +It disables agility shortcuts and teleports, enables ships, and requires at least three completed +`TERMINAL_TRAVEL` shadow selections in addition to exact arrivals. + +The ordinary-replan slice injects twelve replans only while a long surface route is active and requires each +one to finish in the upstream shadow before accepting the final arrival: + +```bash +MICROBOT_WEBWALKER_PLANNER_MODE=SHADOW \ +scripts/run-f2p-webwalker-harness.sh F2P-20 +``` + +The recovery slice queues its replans through a test-only hook that is consumed by the walker thread. This +exercises the same recovery evidence context as a real stall without manufacturing a client-thread sleep or +calling the recovery helper from the harness thread. Three repetitions produce at least five alternating Port +Sarim / Rimmington legs; each sufficiently long outbound or reverse setup leg attempts two progress-gated +recovery replans. The verifier uses observed results rather than requested injections and requires ten +completed comparisons plus five recovered arrivals. The accepted 2026-08-05 session produced 11/11 matching +recovery comparisons and six recovered arrivals with no exit or unreachable outcome: + +```bash +MICROBOT_WEBWALKER_PLANNER_MODE=SHADOW \ +scripts/run-f2p-webwalker-harness.sh F2P-21 +``` + +The existing spell-teleport stress harness can capture a separate fresh-session slice: + +```bash +MICROBOT_GE_LUMBRIDGE_ITERATIONS=3 \ +MICROBOT_GE_LUMBRIDGE_UPSTREAM_PLANNER_SHADOW=true \ +scripts/run-ge-lumbridge-teleport-harness.sh +``` + +Extract its `shadowEvidence` as a second file and pass both snapshots to the evaluator. It validates every +session before aggregation and rejects duplicate session start identities. Do not concatenate JSON or add +counters by hand. + ## Agent Loop 1. Run the full suite. @@ -64,4 +216,16 @@ The runner forwards route settings through `microbot.test.webwalker.*` system pr | F2P-14 | `3092,3245,0` | `3109,3341,0` | Draynor Manor approach | | F2P-15 | `3109,3341,0` | `3106,3363,0` | Draynor Manor door/object handling | | F2P-16 | `3106,3363,0` | `3092,3245,0` | Reverse manor exit behavior | -| F2P-17 | current live player tile | `3237,9858,0` | Captures current origin, then walks to Varrock Sewers 5 times on a F2P world with agility shortcuts and teleports disabled | +| F2P-17 | `3236,3458,0` | `3237,9858,0` | Walks from the fixed Varrock surface manhole to the sewers 5 times on a F2P world with agility shortcuts and teleports disabled; setup climbs out before every repetition, so a prior run ending underground cannot turn the case into a no-op | + +## Selection-gate routes + +These prerequisite-bearing routes are available by explicit ID and are not included by the default `all` +filter. + +| ID | From | To | Coverage | +|---|---:|---:|---| +| F2P-18 | `3243,3237,0` | `3199,3344,0` | Ten explicit item-gated bank-to-target comparisons plus three River Lum canoe repetitions and five or more `CANOE` planner selections | +| F2P-19 | `3029,3217,0` | `2956,3146,0` | Two Port Sarim-to-Musa Point repetitions with three or more fare-gated `TERMINAL_TRAVEL` planner selections | +| F2P-20 | `3029,3217,0` | `2946,3368,0` | Long surface walk with twelve deliberately injected and settled `ACTIVE_REPLAN` comparisons | +| F2P-21 | `3029,3217,0` | `2957,3214,0` | Five alternating surface walks with two walker-thread `RECOVERY_REPLAN` comparisons each and five recovered arrivals | diff --git a/docs/decisions/adr-0005-walker-transport-execution-boundary.md b/docs/decisions/adr-0005-walker-transport-execution-boundary.md new file mode 100644 index 00000000000..4b5847c73ec --- /dev/null +++ b/docs/decisions/adr-0005-walker-transport-execution-boundary.md @@ -0,0 +1,58 @@ +# ADR 0005: Separate Transport Description from Execution Capability + +- Status: Accepted (2026-08-05) + +## Context +Shortest Path transport data describes where an edge goes and what it requires, while Microbot also +needs to execute the edge against a live game client. Those responsibilities cannot be treated as the +same contract: imported data can describe a route that Microbot does not yet know how to operate, and +some local transports carry behavior that a flattened data value cannot preserve. In particular, +`PohTransport` owns an executable POH action and seasonal handlers are a pluggable runtime API. + +Converting every interaction handler directly from the local `Transport` class to the immutable +`Rs2TransportEdge` value would remove that behavior or recreate it through type switches and legacy +adapters. Retaining an unrestricted concrete planner object as the public route contract would instead +prevent a future upstream planner adapter. + +## Decision +Represent every selected route transport with: + +- an immutable, planner-independent `Rs2TransportEdge` description; +- an explicit `Rs2TransportExecutor` capability naming the Microbot runtime branch that owns it; and +- for terminal `SHIP`, `NPC` and `BOAT` travel, an explicit planner-independent interaction mode; +- only for the local engine, an opaque package-private payload containing the exact selected local + transport needed by the existing handlers. + +The pure planner-side `TransportExecutionRegistry` is authoritative for whether a local catalog row is +executable. Unregistered rows fail closed during transport refresh and cannot be selected merely because +their data was imported. Runtime dispatch also rejects `UNSUPPORTED` as a defensive invariant. The local +payload is never recovered by matching endpoints or rescanning the mutable catalog. + +Terminal catalog families describe a journey rather than the live target kind: rows may point at an NPC +or a scene object. The runtime resolves that kind from the configured semantic name/action near the exact +selected origin. The registry still owns the interaction sequence. Direct travel and a known dialogue +destination flow are separate modes; rows needing an unimplemented destination selection remain +unregistered even when another row in the same catalog family is executable. + +## Consequences +- Upstream catalog convergence cannot silently create routes that stall at an unimplemented interaction. +- An upstream planner adapter can emit the same immutable edge and executor capability without exposing + its model classes. +- POH and other behavior-bearing transports remain correct while their handlers are gradually moved + behind Microbot-owned execution interfaces. +- The concrete local payload remains an internal implementation detail rather than a migration target by + itself; replacing it requires an equivalent executable capability, not a blanket signature rewrite. +- Missing executor families become explicit delivery work. At adoption, the non-Lumbridge home teleports + and hot-air-balloon network were deliberately fail-closed; the shared exact-name home-teleport executor + subsequently closed the former gap. The balloon network now has an exact six-destination map executor + and observed-landing contract; its static, dual-engine and locked-account fail-closed evidence is complete, + while a successful flight remains pending on an account with an unlocked station. +- Terminal-travel coverage cannot be inferred from `TransportType`. The current audit deliberately keeps + 41 multi-step rows fail-closed: 30 `Board` rows for the multi-destination Boat/Boaty networks, six + destination-selecting Rowboat rows and five unimplemented `Talk-to` rows. Their exact interaction + groups are pinned by the catalog capability test. +- Inventory-item interactions are capabilities too. The Barrows dig executor is registered only for the + six reviewed mound-to-individual-crypt pairs with an exact one-spade requirement; arbitrary object-less + `Dig` rows remain unregistered. Individual-crypt stairs use ordinary object execution and a representative + surface-mound anchor because the live exit spawn can vary. Randomized sarcophagus-to-tunnel entry remains + outside the static catalog until the executor can consume observed run state. diff --git a/docs/decisions/adr-0006-upstream-first-planner-convergence.md b/docs/decisions/adr-0006-upstream-first-planner-convergence.md new file mode 100644 index 00000000000..0c8e7b68a78 --- /dev/null +++ b/docs/decisions/adr-0006-upstream-first-planner-convergence.md @@ -0,0 +1,86 @@ +# ADR 0006: Establish an Upstream-Compatible Planner Boundary and Selection Gate + +- Status: Architecture accepted; opt-in F2P canary implemented and validated (2026-08-05); + default/release selection pending durable performance evidence + +## Context + +Microbot needs Shortest Path's current collision, transport semantics and planner improvements, but it also +owns live interaction, recovery, banking and automation policy that the display plugin does not. Selectively +copying transport families improved correctness, yet continuing that work before proving an interchangeable +planner would preserve a second search implementation indefinitely behind a facade that still read mutable +local configuration. + +The reviewed upstream planner also does not retain the exact selected transport in its public `PathStep`. +Endpoint rematching is ambiguous when multiple transports share an origin and destination, so production +execution cannot safely adopt it without exact edge identity. + +## Decision + +Make production-capable upstream planner convergence the next walker milestone: + +- freeze opportunistic broad transport-family copying after the reviewed ship slice; continue parity work + required by the adapter or selection gate, incident-driven data fixes and pinned collision updates; +- dispatch planning through Microbot-owned `Rs2RouteRequest`, immutable `Rs2RoutePolicy`, `Rs2RoutePlanner` + and `Rs2RouteResult` contracts; +- resolve mutable client/config state before engine dispatch; an engine may not read Microbot plugin globals; +- inject Microbot executor capability as catalog-admission policy outside the pathfinder core; +- retain exact selected source identity only as an opaque package-private adapter payload; +- run the reviewed upstream engine in shadow mode against real requests before selecting it for execution; +- accept or reject the upstream core against a pinned commit, expanded corpus, runtime evidence and explicit + performance report. + +Planner selection is an explicit three-state policy, not two interacting booleans: + +- `LOCAL` selects only the local planner and remains the default; +- `SHADOW` executes the local route and compares the upstream result asynchronously; +- `UPSTREAM_F2P_CANARY` calculates both candidates for non-members policy, publishes no executable route + until comparison is complete, selects upstream only for a semantic match, and otherwise retains local. + +The local result is a containment fallback, not an absolute correctness oracle. A semantic divergence is +therefore observable and rejecting evidence even though the canary conservatively executes the local route. +The active route remains calculating until one final candidate is selected atomically. Upstream results are +temporarily materialized into the legacy `Pathfinder` view so existing execution and overlay consumers retain +exact selected transport identity; that compatibility shell must be removed with the local planner after the +fallback sunset. + +## Consequences + +- `LOCAL` remains the production default until the final reachable revision passes the evidence gates and a + release explicitly changes the scoped policy. +- New local search algorithms require a pinned incident or benchmark and must preserve the engine boundary. +- Upstream schema/data work continues where required for the adapter, but semantic-debt reduction alone is no + longer the primary walker milestone. +- The production package now contains the reviewed upstream core in an isolated source set. Its exact-identity, + live-collision and walking-cost hooks remain a small declared adapter patch surface, enforced by an offline + tree digest and optional byte-for-byte comparison with the pinned checkout. +- Shadow mode is default-off and cannot select a route for execution. Synchronous queries, ordinary active + walker routes and cave-route selection publish bounded comparison evidence while the local core remains + authoritative. +- Walker execution, recovery, live collision capture and automation policy remain Microbot-owned whichever + planner is selected. +- The production switch is not implied by this ADR. The repeatable headless thresholds and live-shadow + requirements are defined in `docs/walker-planner-selection-gate.md`; until they pass, the accepted decision + is to preserve the upstream-compatible boundary while retaining the local core as authoritative. +- Upstream is the maintenance-preferred candidate, not a predetermined production winner. The selection + decision weighs correctness, runtime reliability, performance and maintenance cost through the same + engine-neutral contract. +- Passing the selection gate permits a controlled rollout; it does not remove the local core. The first + upstream-authoritative release is limited to F2P policy because that is the scope of the accepted live + evidence. Expanding selection to members policy requires a separate representative live-shadow slice for + members-only executor, requirement and network behavior. Every upstream-authoritative scope must retain a + tested, release-independent local-planner fallback and dual-planner telemetry during staged rollout. Any + confirmed upstream planner failure or unexplained semantic divergence on a request selected for execution + triggers rollback to the local planner. +- The fallback has an explicit sunset instead of becoming permanent architecture. The local planner becomes + eligible for removal only after every supported rollout scope has used upstream authoritatively for at + least two completed releases and has accumulated at least 1,000 settled production dual-planner + comparisons with no unresolved semantic divergence or planner failure. The rollback path must also have a + passing release test and there must be no open incident that requires the local core. At that point the next + planned release removes the local planner; extending it requires a dated decision record naming the incident + and a new expiry condition. +- The opt-in F2P canary and rollback behavior were validated live on 2026-08-05 using the underground + Varrock Sewers object-transition route. The normal run made ten upstream selections and ten arrivals with + no divergence or failure. A separate test-only injected-failure run made zero upstream selections, recorded + ten local failure fallbacks and still produced ten arrivals. This validates the selector and independent + fallback mechanism; it does not by itself authorize changing the default or cutting a release. diff --git a/docs/walker-audit.md b/docs/walker-audit.md index 0cc804833f6..41b1a0318cd 100644 --- a/docs/walker-audit.md +++ b/docs/walker-audit.md @@ -2,6 +2,10 @@ _Audit date: 2026-07-25. Scope: `shortestpath/` (pathfinding) and `util/walker/` (runtime execution)._ +> Historical audit. The maintained direction and current status are in +> [`walker-roadmap.md`](walker-roadmap.md). In particular, live collision now defaults on and the +> upstream collision baseline has moved since this snapshot. + ## Verdict **Path *generation* is healthy. The runtime *executor* is the problem, and collision-map diff --git a/docs/walker-p2-unification.md b/docs/walker-p2-unification.md index a1366712797..97ee83f5765 100644 --- a/docs/walker-p2-unification.md +++ b/docs/walker-p2-unification.md @@ -1,5 +1,9 @@ # Walker P2 — Unify the Obstacle Model +> Historical design record. See [`walker-roadmap.md`](walker-roadmap.md) for the maintained plan and +> the decision to evolve stateful door/transport behavior from pinned incidents instead of performing a +> wholesale rewrite. + _Plan date: 2026-07-26. Depends on P1 (docs/walker-audit.md): `state/WalkerRouteState`, `recovery/RouteRecovery` + harness, `geometry/WalkerPathGeometry`, and the pre-existing `door/` + `obstacle/` packages are in place._ diff --git a/docs/walker-planner-selection-gate.md b/docs/walker-planner-selection-gate.md new file mode 100644 index 00000000000..43ceb18d3f4 --- /dev/null +++ b/docs/walker-planner-selection-gate.md @@ -0,0 +1,237 @@ +# Walker planner selection gate + +## Production selection under review + +The upstream-compatible boundary is correct, but selecting either search core for long-term production is +not yet justified. `Skretzo/shortest-path` provides the maintenance-preferred candidate algorithm and reviewed +reference data; that preference does not override the correctness, runtime and performance evidence gates. +Microbot's resolved planning snapshot remains authoritative for executable-edge admission, live collision, +account policy and intentional data overlays. Microbot should continue to own banking orchestration, +interaction, recovery and automation policy. + +This avoids an indefinitely diverging local search fork while keeping Microbot runtime behavior independent +of a RuneLite display plugin. The `Rs2RoutePlanner` boundary keeps those responsibilities separate and makes +the production choice reversible. + +## Required evidence + +A production-core decision requires all of these gates: + +1. **Pinned implementation:** the packaged core matches the reviewed upstream commit except for the declared, + digest-pinned adapter patch surface. +2. **Headless correctness:** every parity corpus case agrees on termination, endpoint, route cost and exact + selected edge; every intentional policy divergence remains documented and observable. +3. **Headless performance:** at least five clean, same-revision comparison runs satisfy the performance + thresholds below. +4. **Live shadow behavior:** active surface, underground, transport, bank/replan and recovery routes produce + enough categorized evidence to show no unexplained semantic divergence or planner failure. Stale and + discarded shadow work is capacity telemetry, not a route mismatch. +5. **Explicit rollout decision and rollback:** record the evidence and choose the production core + deliberately. The first production rollout must retain a tested, release-independent way to select the + local planner and preserve comparable telemetry for both engines. During staged dual-running, any confirmed + upstream planner failure or unexplained semantic divergence on a request selected for execution triggers + rollback to the local planner. The normal-canary and forced-failure artifacts must jointly pass + `scripts/evaluate-walker-rollout-evidence.py`; do not infer selection from packaging the adapter, enabling + shadow mode or reviewing two self-reported harness checks independently. +6. **Evidence-scoped rollout:** accepted F2P evidence permits only an F2P-policy upstream-authoritative + rollout. Selecting upstream for members policy requires a separate representative members-world slice + covering members-only executors, requirements and transport networks. + +The current implementation satisfies gates 1, 2, 4, 5 and 6 at opt-in canary scope. Shadowing exists for synchronous, ordinary active and +cave routes. Its coordinate-free coverage counters, read-only Agent Server endpoint and opt-in F2P harness +capture produced an accepted 12-session aggregate covering recovery, transport, item-gated, bank-workflow, +surface, underground, walking-only, live-collision, active-route/replan, arrival and executor-diversity +minimums without a semantic divergence or planner failure. Five clean same-revision samples from an ephemeral +detached commit containing the exact current tree pass gate 3's correctness and timing thresholds; the final +review commit must reproduce or adopt that evidence under a durable revision identity. Production selection +The explicit `UPSTREAM_F2P_CANARY` selector now keeps members policy local, waits for both F2P candidates +before atomically publishing an active route, selects upstream only on a semantic match and records distinct +divergence/failure local fallbacks. A live underground object-transition run selected upstream 10/10 times +and arrived 10/10 times. A separate test-only forced-failure run selected upstream 0/10 times, recorded ten +failure fallbacks and still arrived 10/10 times. Production default/release selection remains pending because +the performance evidence must be repeated or formally adopted under the final reachable review revision. + +## Performance protocol + +Generate independent comparison reports from the exact code under review: + +```bash +for sample in 1 2 3 4 5; do + scripts/compare-shortest-path-planners.py \ + --require-all \ + --output-dir "build/shortest-path-performance/sample-${sample}" +done + +scripts/report-shortest-path-planner-performance.py \ + build/shortest-path-performance/sample-*/report.json \ + --json-output build/shortest-path-performance/evidence.json \ + --markdown-output build/shortest-path-performance/evidence.md +``` + +The checked-in evaluator requires: + +- five or more reports with identical local, upstream, RuneLite, corpus and adapter-patch identities; +- a clean local worktree for every sample; +- no correctness, packaged-adapter or unsupported-capability failure; +- an upstream/local median comparable-suite elapsed ratio no greater than `1.5`; +- for each comparable case, an upstream maximum no greater than both `2,000 ms` and the larger of three + times the local maximum or a `100 ms` noise allowance. + +The relative case threshold prevents a material regression from hiding in a favorable suite total. The +noise allowance avoids rejecting a sub-tick route because one engine takes a few additional milliseconds. +Thresholds are command-line options, but changing them for a decision requires an accompanying rationale. + +Bank-aware cases are excluded from core timing because Microbot currently composes direct, to-bank and +from-bank searches while upstream models bank state in one graph search. Their route semantics remain part +of the correctness gate. The documented Twinflame provider-policy divergence is also excluded. Node counts +and peak-heap deltas remain diagnostic: the engines use different graph structures and independent JVM heap +baselines, so those values are not interchangeable resource measurements. + +## Live shadow protocol + +For every evidence slice, start a fresh client so its process-lifetime counters describe one review session, +enable the developer-only shadow mode and verify collection before running routes: + +```bash +./microbot-cli plugin-config set shortestpath plannerSelectionMode SHADOW +./microbot-cli walker shadow +``` + +The schema-versioned endpoint reports only invocation and route-property tags; it does not expose route +coordinates, target names, paths or exception messages. It distinguishes deliberate recovery from ordinary +replanning, reports selected transport executor/type families and counts a live-collision route only when the +pinned overlay actually answers at least one collision-edge read. Exercise ordinary surface routes, +underground/cave routes, selected transports, bank-item planning, live collision and deliberate +recovery/replanning. Let the one-worker shadow queue settle, then capture it. The evaluator accepts multiple +fresh-session snapshots so resource- or account-specific harnesses remain isolated: + +```bash +./microbot-cli walker shadow > build/walker-shadow-underground.json +scripts/evaluate-walker-shadow-evidence.py \ + build/walker-shadow-underground.json \ + build/walker-shadow-teleport.json \ + build/walker-shadow-network.json \ + --json-output build/walker-shadow-evidence.json \ + --markdown-output build/walker-shadow-evidence.md +``` + +Every input is validated independently before aggregation. All inputs must use schema v2 and the same pinned +candidate engine, every queue must be settled and each `startedAtEpochMillis` must be unique so the same +client session cannot be counted twice. A divergence, planner failure or invalid accounting in any member +session rejects the aggregate; splitting scenarios across clients does not relax any total or category +threshold. + +The default gate requires at least 100 completed comparisons and zero divergences or planner failures. Its +overlapping coverage minimums are 75 ordinary active routes, 15 ordinary active replans, 10 recovery-triggered +replans, 60 surface routes, 20 routes using underground coordinates, 10 walking-only cave selections and 20 +routes selecting transports. Five routes must select an item- or fare-gated transport. Ten comparisons must +be the explicit bank-to-target leg of `compareRoutes`, and five of those must select an item- or fare-gated +transport. This proves the bank workflow was compared without mislabeling every request with the bank setting +enabled as bank-dependent. Twenty-five routes must actually consult captured live-collision edges; merely +turning the setting on does not count. + +Transport evidence must cover at least four distinct executor families. The default grouped minimums are five +local-transition routes (`OBJECT` or `BARROWS_DIG`), five teleport routes, five network routes and three +terminal-travel routes. Per-executor and per-type counters remain available for reviewing the concrete mix. +The accepted fresh-client inputs must contain at least 50 terminal blocking walks that arrived and at least +five arrivals after recovery was actually triggered. A recovery-triggered `UNREACHABLE` or `EXIT` rejects +the evidence; other terminal non-arrivals remain visible warnings for manual classification. +Terminal outcomes are route-scoped: the blocking walk captures whether its active logical route actually +entered planner comparison before arrival clears that route. A members-policy walk in +`UPSTREAM_F2P_CANARY`, or any walk in `LOCAL`, must not increment these counters merely because the +process-wide mode supports comparison. `SHADOW` routes and eligible F2P canary routes do increment them. +The candidate engine ID must equal the pinned reviewed commit, shadow mode must still be enabled and no +comparison may be pending at capture. Stale and discarded work is reported as sampling telemetry; it does not +become a semantic mismatch, and it does not count toward completed coverage when discarded before execution. + +Exact walking-path equality is diagnostic rather than a semantic rejection because equal-cost tie paths are +valid. The endpoint reports a coordinate-free `routeShapeDifferences` total and preserves the most recently +completed differing comparison as `latestRouteShapeDifference`, even after the active route is torn down. +Every counted shape difference has already matched termination, endpoint, route cost and exact selected +transport identity; it is therefore an equal-cost route-shape alternative, not a semantic mismatch. Before a +rollout decision, classify the observed volume by invocation and by walking-only, transport, surface and +underground coverage. Inspect at least one representative route from every non-zero class and explain any +class responsible for more than one quarter of completed comparisons. A class associated with a non-arrival, +recovery regression or repeated visible detour remains rejecting until explained or fixed. + +## Staged rollout and fallback sunset + +Planner selection uses one explicit mode: `LOCAL`, `SHADOW` or `UPSTREAM_F2P_CANARY`. `LOCAL` remains the +default. `SHADOW` always executes local and compares upstream asynchronously. `UPSTREAM_F2P_CANARY` is +eligible only when the resolved immutable policy is non-members; it calculates both candidates and keeps the +active route in the calculating phase until selection completes. A semantic match may select the exact +upstream route. A planner failure or semantic divergence retains local and increments a dedicated fallback +counter; members requests remain local and are not canary comparisons. A route-shape-only difference remains +a semantic match when termination, endpoint, cost and exact selected transports agree. + +The first upstream-authoritative release is F2P-policy only. Comparison remains enabled for eligible +production requests, with `LOCAL` selectable independently of the upstream source set. Local fallback is +conservative containment, not evidence that local is the correct oracle; every divergence still requires +review and remains rejecting release evidence. The upstream result is temporarily materialized into the +legacy completed-pathfinder view for existing runtime consumers. Remove that compatibility shell with the +local core when the fallback sunset is reached. + +The local core becomes eligible for deletion only after every supported rollout scope has completed at least +two upstream-authoritative releases and at least 1,000 settled production dual-planner comparisons without an +unresolved semantic divergence or planner failure. The release test must prove the independent rollback path, +and no open walker incident may require the local core. Once these conditions pass, remove the local planner in +the next planned release. Any extension requires a dated ADR amendment identifying the incident, owner and new +expiry condition. + +Semantic divergences and planner failures are always rejecting outcomes. The endpoint preserves their latest +coordinate-free summaries as `latestDivergence` and `latestFailure` for the process lifetime, including after +active-route teardown. The evaluator rejects a non-zero terminal counter that lacks its corresponding +diagnostic; a failure exposes only the exception class, never its message or route data. + +## Current signal + +Five clean samples from one detached revision containing the exact current tree pass the performance +evaluator with no correctness or threshold failure. The comparable-suite median was about `1,334.8 ms` +locally and `522.6 ms` upstream, an upstream/local ratio of `0.392` against the `1.5` limit. Every comparable +case also passed its absolute and relative maximum. The reports are under +`build/shortest-path-performance/current-tree-20260805/`; because their synthetic revision is deliberately +not a branch commit, repeat or formally adopt them on the final reachable review revision before production +selection. + +Twelve independently validated fresh sessions aggregate to 141/141 semantic matches, 71/71 exact walker +arrivals and zero divergence, planner failure, pending, discarded, unreachable or exit outcomes. All live +minimums pass: 75 ordinary active routes, 15 ordinary active replans, 11 recovery replans, 102 surface +comparisons, 39 underground comparisons, 18 walking-only cave selections, 97 transport selections, 59 +item- or fare-gated selections, 10 explicit item-gated bank-to-target legs and 137 live-collision-consulted +searches. The executor evidence covers object transitions, spell teleports, canoes and terminal travel, so all +four grouped minimums and the four-executor diversity minimum pass. Two completed comparisons were stale +after deliberate route replacement; they remain valid completed semantic results but do not represent the +latest route generation. + +Seventy-six comparisons used a different exact path while still matching termination, endpoint, route cost +and exact selected transports. The retained coordinate-free diagnostics classify these as equal-cost +route-shape alternatives. Thirty-one occurred in two transport-free surface replan/recovery sessions where +every comparison differed; both sessions still completed all blocking walks, including six recovered +arrivals. Thirty-three occurred in the bank/canoe session, whose retained representative selected the same +exact canoe and cost; that session completed 49/49 comparisons and all six walks. The remaining twelve were +spread across surface active-route/replan slices; retained representatives cover spell, canoe and +transport-free alternatives. None of the 39 underground comparisons differed—the two sewer-session shape +warnings came from their surface setup legs. No shape-only class coincided with a non-arrival, recovery +regression or visible route failure. +A prior canoe session recorded one non-canoe semantic divergence but did not retain the superseded comparison, +so it remains rejected historical evidence and is not part of the aggregate. The rebuilt three-repetition +canoe session completed 7/7 comparisons and 6/6 walks without reproducing it; any recurrence will now retain +its mismatch category. + +The first dedicated bank session then exposed a reproducible local-core defect: all ten bank-to-target +comparisons selected the farther Barbarian Village canoe at cost 130 locally while upstream selected the +nearer Edgeville canoe at cost 78. The local frontier's geometric heuristic was not admissible in a graph with +long-distance transports. Cost-ordering the local frontier restored exact parity, and a real catalog/collision +regression pins that route. The rebuilt session completed 49/49 matching comparisons, including 10/10 explicit +item-gated bank-to-target legs, and all six terminal walks arrived. The rejected pre-fix session is not part of +the aggregate. + +The headless correctness and live-shadow gates are closed, and the exact current tree passes the clean timing +thresholds. The opt-in F2P selector and independent rollback path are implemented and live-validated on the +underground F2P-17 route: the normal canary recorded 10/10 upstream selections and arrivals with zero +divergence/failure; the forced-failure run recorded 10/10 local failure fallbacks and arrivals with zero +upstream selections. Both were repeated after terminal evidence became route-generation scoped and retained +all 10 eligible arrivals; focused members-canary and local-mode regressions prove ineligible walks leave the +execution totals unchanged. The paired release evaluator accepts those two fresh-client artifacts with no +failure, shortfall or warning. `LOCAL` remains the default. Do not change the release default until the +performance result is tied to the final reachable review revision and the final evidence bundle is approved. diff --git a/docs/walker-roadmap.md b/docs/walker-roadmap.md new file mode 100644 index 00000000000..8965347e1e6 --- /dev/null +++ b/docs/walker-roadmap.md @@ -0,0 +1,553 @@ +# Walker Roadmap + +_Canonical plan. Last reviewed: 2026-08-05._ + +## Direction + +Microbot is not a source fork of `Skretzo/shortest-path`, but it must not drift from it silently. +The ownership boundary is: + +- **Shortest Path upstream:** primary reviewed external reference for static collision and + transport/teleport semantics, and the maintenance-preferred candidate planner rather than merely a + source of occasional fixes. This preference does not predetermine the production engine, and upstream + data is not automatically authoritative for Microbot execution. +- **Microbot:** route execution, live collision, obstacle interaction, recovery, banking and automation + policy. +- **Import rule:** review upstream changes by behavior and data semantics. Never overwrite Microbot + executor behavior or local restrictions merely to make the trees look alike. +- **Planner rule:** choose the production engine from correctness, runtime reliability, performance and + maintenance evidence gathered through the same engine-neutral boundary. New local search algorithms + require a pinned incident or benchmark rather than divergence for its own sake. + +The reviewed upstream commit and resource blobs live in +`scripts/shortest-path-upstream-baseline.json`. Run: + +```bash +scripts/check-shortest-path-upstream.py +``` + +The command exits `2` when upstream moved and prints changes in tracked planner/data scopes. Use +`--json` for other automation or `--allow-drift` for an informational local check. The scheduled +`shortest-path-upstream-drift.yml` workflow runs it weekly and fails visibly when review is due. + +The production-packaged planner core has a separate offline integrity gate: + +```bash +scripts/check-shortest-path-vendored-core.py +``` + +This pins all 50 vendored Java files and adapter metadata. Passing `--upstream-checkout` additionally proves +every undeclared source is byte-identical to the exact reviewed checkout, the declared patch/addition lists +are complete, and the bundled license is unchanged. + +Transport convergence has a second, stricter contract: + +```bash +scripts/compare-shortest-path-transports.py --check-baseline +``` + +This compares routes by semantic identity, including named network endpoints when Microbot models a +different boarding or landing tile. It compares interaction object, requirements, fare/items, +membership, wilderness limit, consumability and duration separately. The exact reviewed debt is pinned +in `scripts/shortest-path-transport-baseline.json`; counts and content digests make route swaps visible, +not merely changes to the total. Updating that baseline requires reviewing the changed identities and +field dimensions. Intentional differences may carry a rationale bound to their exact field-drift digest; +the check fails when the underlying routes or fields change. Unexplained debt remains unresolved rather +than implicitly accepted, so the baseline is not an allowlist or a claim that remaining differences are correct. +For agility, RuneLite's explicit course-obstacle catalog classifies course traversal separately while +retaining those identities in total upstream-only debt; both the classified set and full set are digest-pinned. + +## Current state + +### Upstream convergence + +- Reviewed upstream: `Skretzo/shortest-path@ff8e961b32120175709df9630ece9468cc11347f`. +- Collision map imported from that commit after the planner core, route corpus and benchmark all + passed against it. It expands the map from 2,724 to 2,726 regions. +- All four spellbook home teleports are represented in the catalog. Microbot deliberately keeps one row + per spellbook and does not copy upstream's animation-setting variants; animation display settings must + not change route availability. They share an exact-name, zero-rune widget executor because RuneLite's + `MagicAction` catalog only represents Lumbridge. Spellbook, quest, cooldown and Wilderness requirements + remain catalog/planner gates rather than being weakened to accommodate that UI difference. Lumbridge + has live round-trip evidence; the alternative-spellbook variants still require direct live evidence. +- Microbot-only `blocked_edges.tsv`, `dangerous_tiles.tsv`, `npcs.tsv` and `restrictions.tsv` remain + local policy/data overlays. +- At the reviewed commit the semantic inventory has 6,601 shared route identities, 1,016 + upstream-only identities, 952 Microbot-only identities and 1,379 shared identities with at least one + comparable field difference. These totals include unresolved representation differences and are a + debt inventory, not a walker-quality score. +- `hot_air_balloons.tsv`, `magic_mushtrees.tsv`, `minecarts.tsv` and `teleportation_levers.tsv` have + exact compared parity. The Lovakengj minecart import also fixes the old unconditional 20-coin fare: + paid routes require varbit `7796<11`, while free routes require `7796=11`. +- Minigame teleport identities now fully match upstream: Guardians of the Rift lands inside the Temple + of the Eye, the Keldagrim Rat Pits destination is restored, and Varrock uses the current landing. The + transport model now enforces upstream's special Total level, Combat level and Quest-points + requirements; this closes the previously ignored 40 Combat gate for Pest Control. +- The first teleport-item schema slice is behavior-compatible: explicit requirements preserve AND + groups, OR alternatives and quantities, including upstream's maximum-quantity rule within an OR + group, while legacy semicolon rows retain their historical any-item behavior. Pathfinder + availability, refresh snapshots, bank withdrawal planning and Slayer transport preparation use the + structured requirements rather than flattening them. +- The symbolic requirement adapter now resolves pinned walking collections for axes, pickaxes, + machetes, grapple gear, keys, passes, currencies and the upstream cape/apron families. Unsupported + symbols fail closed. Rune collections delegate to the canonical `Runes`, `Rs2Staff` and `Rs2Tome` + catalogs and retain raw/combo-rune alternatives separately from equipped staff and offhand providers. + The owned route edge preserves those source semantics, and bank preparation produces one immutable + withdrawal-and-equipment loadout so an inventory staff is never mistaken for an active provider. + All 43 shipped non-home spell rows now carry the reviewed upstream item requirements without changing + Microbot's existing landing coordinates; coordinate drift remains a separate evidence-gated decision. +- All 45 River Lum and River Dougne canoe routes have exact compared field parity, including the full + twelve-axe collection and upstream route costs. The executor selects `CanoeMapLum` or + `CanoeMapDougne` from the reviewed station object family, unknown stations fail closed and the route + corpus proves selection of the western chain. Live River Dougne execution remains required. +- Barrows now has twelve deliberately Microbot-owned static edges because the reviewed Shortest Path + artifact has no mound, individual-crypt or tunnel transitions. Six exact mound-to-crypt pairs require + a spade and use a dedicated inventory-item executor; six staircase edges return each individual crypt + to a representative anchor on its own surface mound. The exit spawn may vary within the mound, so + runtime completion uses the existing bounded landing tolerance and then replans from the observed tile. + The route corpus proves every mound/crypt pair and separately proves that no sarcophagus becomes a + static tunnel edge. Quest Helper observes which sarcophagus is empty per run, so randomized tunnel + entry remains fail-closed pending a state-aware executor and live evidence. One mound round trip is + still required to verify the current staircase action, surface landing envelope and handoff logs. +- The direct Max-cape family and Quest-point cape now match the reviewed upstream identities and + requirements. Four Max-cape destinations were restored, a duplicate Black chinchompa row was removed, + and nested display labels resolve to their executable leaf action. POH home variants remain under + Microbot's programmatic POH ownership. +- The Quetzal network has exact compared parity across all 28 station-side route identities. All 14 + whistle destinations, items and unlocks are represented inline; their 14 reviewed field differences + are intentional because Microbot splits upstream's family-level consumable row into charged and + perfected-infinite variants. This keeps item `33120` usable under Inventory (perm). The Gorge landing + uses the current tile, Cam Torum uses the live map label and unlocks use the canonical varplayer bitmask. +- Laguna Aurorae now has all nine reviewed object-`26262` outbound spirit-tree perimeter origins rather + than only an inbound destination. A resource-backed route proves the north-west approach can select the + network and reach the Grand Exchange. Spirit-tree upstream-only debt falls from 11 identities to the two + POH directions that remain intentionally programmatic; the corpus rejects static POH duplicates. The + current `Travel` action and `E: Laguna Aurorae` destination label still require a live round trip. +- The ordinary-transport remainder is now fully classified. Two Elemental Workshop wall directions use + the current object `26115`, a curated collision-edge override and the concrete battered-key requirement; + the upstream steel-key-ring alternative remains fail-closed because possession of an arbitrary ring does + not prove that this key is stored on it. The remaining 15 upstream-only identities are digest-pinned as + four superseded Piscatoris gate anchors, eight id-less Marim staircases, one interaction-less Daero jump + and two intentionally disabled Varrock Palace trellis routes. Static route coverage proves that the + battered key selects the wall edge while a key ring alone cannot. Live wall interaction remains pending. +- Four missing Pandemonium ship routes now connect Port Sarim and Musa Point through the exact reviewed + Captain Tobias, Customs officer and Seaman Morris interactions. Each direction is gated by the + `Pandemonium` quest and a 30-coin fare, uses the direct terminal-travel executor and is selected only + when both prerequisites are available. The six remaining upstream-only ship identities are fully + classified as Microbot's current Corsair Cove, Ardougne and Void Outpost deck/landing representations. + Static route coverage proves exact edge selection and fail-closed prerequisite behavior; a live + Pandemonium round trip remains pending. +- The semantic comparator no longer treats a column missing from one schema as an empty requirement. + This removes false membership drift from item/minigame/portal families without hiding fields that are + present in both artifacts. +- The first agility-shortcut correctness slice fixes all 13 shared item-field differences: twelve grapple + edges require both a supported crossbow and a mith grapple, while the Trollheim rope edge retains both + its rope item and attached-rope varbit. The Lumbridge-farm fence and northern Varlamore rocks use their + current upstream landings and durations, with route-corpus tests proving the planner selects each edge. + RuneLite's `Obstacles.OBSTACLE_IDS` authoritatively classifies 114 of the 231 remaining upstream-only + identities as known course traversal; they stay visible in total debt but are not bulk-import candidates + for the general walker graph. The other 117 remain ordinary-world or unresolved representation debt. + Three Trollheim climbing-rock ascents were moved from generic transports to boots-gated agility edges, + while their unrestricted descents remain generic; route coverage proves the asymmetric policy. The full + 88-edge Isafdar forest family now uses current upstream landings, Agility gates and traversal durations + instead of unconditional generic transports; this adds 22 missing edges and removes four stale landing + variants. A real three-obstacle dense-forest route pins the imported chain. The remaining 28 agility + identities that Microbot had represented as unrestricted generic transports now retain upstream's + requirements across Brimhaven Dungeon, the Lumbridge cellar, Karamja rocks, Slayer Tower and Darkmeyer. + Catalog assertions pin all levels, durations and unlock varbits, while one real route per family proves + planner selection. The comparator separately classifies this cross-file bypass pattern and pins it at + zero; upstream's two intentional Darkmeyer diagonal generic approaches remain exactly once alongside + their gated shortcut variants. Live interaction evidence for the corrected fence, rocks, grapple, rope, + Isafdar and these five converted families remains pending. + +### Executor reliability + +- Live collision is enabled by default, revision/version keyed and prunes stale captures. +- Door and raw-scene observations are cached to bound repeated scanning. +- `Rs2PathApi` is the compatibility seam around mutable shortest-path plugin state. No in-tree consumer + outside the facade or shortest-path implementation now constructs or reads `Pathfinder` or imports and + mutates `PathfinderConfig`; legacy concrete accessors remain public only for binary compatibility. + Presence-only recovery, obstacle and door-catalog queries now use named facade operations or immutable + `Rs2TransportEdge` views; the hot-air-balloon handler consumes its owned selected edge directly. Concrete + `Transport` dependencies still exist in behavior-bearing execution handlers, planner overlays and legacy + compatibility APIs, so the final stable boundary remains intentionally narrower than the current + compatibility surface. +- Synchronous planning now dispatches through the engine-neutral `Rs2RoutePlanner` contract. Before an + engine receives a request, `Rs2PathApi` resolves an immutable `Rs2RoutePolicy` containing bank visibility, + Wilderness and dangerous-NPC policy, teleport policy, membership, live-collision enablement, cutoff, + enabled transport families and restricted points. Engines reject unresolved requests instead of reading + mutable plugin globals. The local dual-engine runner uses this same contract rather than constructing its + planner directly. Its selected immutable edge retains the exact local source object only as a package-private + opaque identity, so comparison and execution never rematch ambiguous endpoints. +- Microbot executor capability is injected into `PathfinderConfig` through `TransportPlanningPolicy` at plugin + composition. The pathfinder core no longer imports `TransportExecutionRegistry`; a boundary regression + rejects that coupling if it returns. Headless planner tests can admit an explicit synthetic catalog, while + production continues to fail closed through `Rs2TransportPlanningPolicy`. +- Planner termination is now explicit. The local pathfinder distinguishes target reached, exhausted + graph, cutoff, cancellation and caught failure; `Rs2RouteResult` exposes the same states through a + Microbot-owned enum instead of treating every stopped worker as a successful search. +- Completed-path materialization is now keyed to the exact final `Node` identity rather than a shared + dirty boolean. A live forced-recovery run exposed a race where an older reader could clear the dirty + flag after a newer best node was published, leaving the point path older than its edge sequence; the + strict contiguous-route invariant and a focused regression now pin the fix. Blocking walks also treat + up to two client-thread read timeouts as transient, retain their active route and use a bounded + collision-free route-index fallback for the failed reachability read. Repeated stalls still fail visibly. +- Local frontier ordering now uses travelled cost rather than the old geometric A* score. A live bank-leg + shadow run proved that the heuristic was not admissible once long-distance transports were present: the + local core chose the farther Barbarian Village canoe at cost 130 while the pinned upstream core chose the + nearer Edgeville canoe at cost 78. A real collision-map/canoe-catalog regression now pins exact edge and + cost parity. The accepted rerun matched all 49 comparisons, including all ten explicit bank-to-target legs, + and all six terminal canoe/setup walks arrived. +- Chosen routes now retain typed walking and transport edges. Forward and bidirectional reconstruction + preserve the exact local `Transport` selected by the search, while `Rs2RouteResult` exposes only + immutable Microbot-owned edge, transport and item-requirement values. Destination bank-item planning + consumes those exact edges for fare, rune, fairy-ring, purchasable-item and AND/OR item-requirement + selection. The transitional `LegacyRoutePlan` handoff has been removed and the boundary checker rejects + its reintroduction. The old public `Transport`-returning helper remains deprecated for Hub binary + compatibility; it is not used by the active banking flow or accepted as an executor contract. +- Transport executability is now explicit. `TransportExecutionRegistry` rejects catalog rows that have + no live Microbot handler before pathfinding, and each immutable `Rs2TransportEdge` records its + planner-independent `Rs2TransportExecutor`. The exact local `Transport` remains only as an opaque + package-private handler payload because POH transports carry executable subtype behavior. This boundary + is recorded in `docs/decisions/adr-0005-walker-transport-execution-boundary.md`. +- The catalog capability audit makes terminal-travel debt explicit instead of treating a whole resource + family as executable. Every spell teleport and all 225 directed hot-air-balloon edges have an explicit + executor. Forty-one multi-step terminal rows remain deliberately fail-closed: 30 multi-destination + Boat/Boaty `Board` rows, six destination-selecting Rowboat rows and five unimplemented `Talk-to` rows. + The exact interaction groups are test-pinned. Balloon dispatch recognizes the base baskets and + their six unlocked-station transforms, selects one of the six exact RuneLite map components and requires + an observed landing before completing the edge. The dual-engine corpus pins Castle Wars-to-Varrock + selection and cost. Live prerequisite evidence also proves the planner fails closed: on an account with + no unlocked balloon stations, enabling the feature evaluated all 225 edges and admitted zero. A successful + map interaction and landing still require direct evidence on an account with an unlocked station. +- Route results now expose planner-independent cost, explored-node, checked-transport and elapsed-time + metrics. Missing engine measurements remain explicitly unavailable rather than being reported as zero. +- The pinned dual-engine adapter runs the local planner and the reviewed upstream commit from one + immutable policy corpus. Overland, underground, surface-to-underground-to-surface, unreachable, + Wilderness-interior, ambiguous network, hot-air-balloon, start-at-bank and separate-bank-detour cases + plus carried/banked raw-rune, separate staff/tome and missing-ordinary-item spell cases agree on reachability, + termination, exact selected transport IDs and path cost. One reviewed divergence is pinned explicitly: + upstream consumes its one allowed staff substitution on the first elemental clause and therefore rejects + a Twinflame staff for a spell requiring both fire and water; Microbot deliberately reuses the same selected + combination staff across every clause it provides, matching the executable loadout. The 16-case corpus + therefore requires 15 exact parity results and one documented expected divergence; an unexpected new + difference or disappearance of the reviewed difference fails the harness. The production-packaged upstream + adapter and the independently compiled evaluation checkout both carry the selected `Transport` reference + through `NodeGraph` into `PathStep`; each maps that exact object to a corpus or execution identity and never + rematches by endpoints. The harness now fails when the packaged adapter differs semantically from the + independent pinned build, except for the same explicitly documented input-policy divergence. It records + node, elapsed and peak-heap diagnostics without treating noisy performance measurements as correctness + gates. For bank detours the local workflow composes direct and bank-leg searches while upstream tracks + bank state inside one search; their final route behavior is comparable, but their node/time measurements + are not planner-core parity. +- `SHIP`, `NPC` and `BOAT` describe the journey, not whether the live target is an NPC or scene object. + Immutable selected edges therefore carry both `TERMINAL_TRAVEL` and an explicit interaction mode. + Direct travel resolves the configured name/action against both target kinds near the selected origin; + Mountain Guide rows use the registered dialogue-destination mode, and unknown flows fail before + planning. Travel attempts are terminal for the current path scan, and one exact selected + edge can be clicked at most once per top-level walk invocation. Legacy `SHIP` destination labels retain + their configured action first and may fall back only to the live `Travel` action; explicit dialogue, + quick-travel and boat actions are never replaced. Arrival accepts the catalogued landing or its immediate + planned continuation, covering current ships that auto-complete an obsolete deck/gangplank pair without + scanning arbitrary later route points. Live Port Sarim-to-Musa Point and reverse walks each selected the + exact ship edge, issued one `Travel` interaction, observed the current ground landing and completed the + transport handoff without a timeout. +- The Al Kharid/Tempoross Ferry is the representative direct object-backed `BOAT` row. Static route-corpus + coverage selects it, transformed-object matching is semantic (`Ferry` + `Board`) and completion requires + the exact catalog landing. A rebuilt live attempt correctly admitted zero boat rows on a free world; + the available profile was rejected by the game server when moved to a members world, so no ferry + interaction occurred and successful outbound/reverse evidence remains pending. +- The Al Kharid toll-gate incident now has one execution owner and a fail-closed completion contract. The + raw door scanner defers this catalog edge to the selected transport executor. That executor resolves the + current transformed scene object by `Gate` name, configured action and exact edge geometry instead of + trusting historical object ids, which collided with an unrelated live object in the current client. A + ranged click may server-walk before presenting its confirmation, so completion waits for movement/dialogue + onset and requires the exact selected tile on the opposite side. Unit tests reject adjacent-origin, + wrong-name, wrong-action and wrong-location false positives. Rebuilt live walks in both directions selected + the Gate, issued the configured toll action once, reached the exact opposite-side destination and emitted + the expected handoff without an unresolved timeout or generic-door interception. +- The Varrock Sewers manhole incident removed a Microbot-only `Open;Manhole;881` row that incorrectly + promoted object-state preparation into a surface-to-underground graph edge. The catalog now retains only + upstream's traversing `Climb-down;Manhole;882` edge; the existing closed-object mapping may open object + `881`, refind `882` and then execute the selected transition. A catalog regression rejects reintroducing + the preparation edge, and the F2P live harness completed five of five exact arrivals at `3237,9858,0` + without an unresolved trapdoor wait or route stall. +- The route corpus proves selection of each terminal travel family and retains Microbot's explicit + Port Sarim-to-Musa Point ship-deck/gangplank edge. It also proves both directions through the east + Draynor sewer transition and the underground corridor to the west sewer. + +## Workstreams + +### Current priority: finish the F2P selection evidence before expanding breadth + +The ship slice is the last opportunistic broad transport-family import before the engine decision. Do not +resume copying families merely to reduce tree drift or add local search algorithms until the following +production-adapter slice is complete. Adapter- or gate-required parity work, incident fixes and reviewed +collision updates continue through the same evidence boundary: + +1. **Complete:** both local and reviewed-upstream engines consume the same resolved `Rs2RouteRequest` and + `Rs2RoutePolicy` contract. +2. **Complete:** the already-filtered executable catalog, live-collision view and Microbot restrictions are + projected into an engine-neutral immutable planning snapshot; executor admission stays outside both cores. +3. **Complete for the F2P evidence scope:** default-off shadow mode covers synchronous queries, ordinary active + walker requests and cave-route selection. It compares termination, endpoint, cost and exact selected edge, + uses a bounded one-worker/one-queued-request executor, invalidates stale route generations and exposes both + the latest structured comparison and process-lifetime match/divergence/failure/discard counters. The latest + semantic divergence, planner failure and route-shape-only difference survive route teardown as coordinate- + free diagnostics. Completed + outcomes are tagged by active/query/ordinary-replan/recovery origin, surface/underground coordinates, cave + walking-only selection, selected transport executor/type, explicit bank-workflow leg and live collision + actually consulted by the local search. Equal-cost alternate walking shapes are counted separately as a + diagnostic. `microbot-cli walker shadow` exposes this evidence without coordinates, and + `scripts/evaluate-walker-shadow-evidence.py` enforces stratified recovery, bank, collision and transport + coverage rather than treating enabled settings as behavioral evidence. Blocking-walk terminal outcomes + also prove that recovery-triggered routes arrived instead of merely producing a matching replan. Their + accounting is bound to the logical route's actual comparison eligibility: members-policy walks in the F2P + canary and all local-only walks cannot inflate execution totals, while shadow and eligible F2P canary + routes retain their outcome after arrival clears the active route. The F2P + harness can explicitly enable shadowing, waits for the worker to settle and embeds the same schema-v2 + coordinate-free snapshot in its result. Twelve accepted fresh sessions now aggregate to 141/141 matches + and 71/71 exact arrivals with no divergence, failure, pending, discarded, unreachable or exit outcome. + Every live minimum passes: 75 active routes, 15 active replans, 11 recovery replans, 102 surface and 39 + underground comparisons, 18 walking-only cave selections, 97 transport selections, 59 item- or fare-gated + selections and ten explicit item-gated bank-to-target comparisons. Recovery, bank-workflow, collision, + arrival, executor-group and four-executor diversity gates all pass. Two completed results were stale after + deliberate route replacement. Seventy-six equal-cost route-shape differences remain diagnostic: 31 came + from transport-free surface replan/recovery sessions, 33 from the bank/canoe session and 12 from mixed + surface active slices; none of the 39 underground comparisons differed and no class coincided with a + non-arrival or recovery regression. A prior rejected canoe session's one non- + canoe divergence did not reproduce in the rebuilt 7/7 comparison, 6/6 arrival slice; future terminal + outcomes retain their mismatch category across teardown. + The evaluator can combine independently validated fresh-session slices only when their schema + and pinned engine match, their queues are settled and their process start identities are unique. This + avoids coupling underground, teleport, network and bank prerequisites into one brittle mega-harness while + preserving every aggregate threshold. +4. **Complete for F2P selection:** the corpus and focused adapter tests include real underground routes, + restrictions, an immutable collision override and representative executable-network slices. Repeated + Varrock Sewers sessions close the underground and walking-only live minimums, and the full 12-session + aggregate closes every F2P live-shadow threshold. Members-policy selection still requires its own + representative members-only executor, requirement and network evidence. +5. **Selector and rollback complete; durable performance evidence pending:** planner selection is one explicit + `LOCAL`, `SHADOW` or `UPSTREAM_F2P_CANARY` mode. `LOCAL` remains the default. The canary is eligible only + for resolved non-members policy, waits for both candidates before atomically publishing the route, and + selects upstream only on a semantic match. Divergence or planner failure retains local while recording a + dedicated fallback; local is conservative containment rather than a correctness oracle. Exact upstream + routes are temporarily materialized through the legacy completed-pathfinder view, which is removed with + the local core after the fallback sunset. A live F2P-17 underground run made 10/10 upstream selections and + arrivals with no divergence/failure. A separate test-only forced-failure run made 0 upstream selections, + 10 local failure fallbacks and 10 arrivals. Both runs were repeated after terminal evidence was bound to + the generation-matched ready route; each retained all 10 eligible arrivals, while members-canary and local + regression routes leave execution totals unchanged. `evaluate-walker-rollout-evidence.py` now treats the + normal and forced-failure artifacts as one release gate and accepts the fresh pair only when engine/session, + route, accounting, selection, fallback, arrival, coverage and failure-opacity invariants all hold. Five + clean samples from an ephemeral detached revision + containing the exact current tree also pass every correctness and performance threshold (`0.392` + upstream/local comparable-suite median ratio). Repeat or formally adopt that report under the final + reachable review revision before changing the default or cutting the selection release. Members-policy + selection still requires representative members-only evidence. + +### 1. Keep planner data current + +1. Run the drift checker during walker work and before releases. +2. When upstream moves, classify changed files as collision, transport data or planner logic. +3. For collision changes, run `ShortestPathCoreTest`, `WalkerRouteCorpusTest` and + `PathfinderBenchmarkTest` against the candidate map before import. +4. For transport changes, convert by semantic identity (origin, destination, requirements), then add a + regression assertion for every intentionally imported or rejected behavior. +5. Reduce pinned semantic debt only in an adapter-, gate- or incident-driven slice. Within that slice, first + remove comparison noise, then classify every remaining upstream-only identity as import, programmatic + equivalent, intentional exclusion or unresolved; do not resume family copying solely to lower the total. +6. Extend the pinned upstream-schema adapter one symbolic family at a time. Rune/staff/tome semantics and + the shipped spell requirements are complete; remaining unsupported slot/category collections must + continue to fail closed until their runtime semantics are explicit. Keep Microbot-only policy overlays + separate so upstream refreshes cannot erase them. +7. Continue scheduled semantic review of upstream transport and teleport changes while the engine decision + is pending. Import adapter- or incident-relevant behavior with executable coverage, and classify other + user-visible changes without broad copying merely to reduce drift counts. A baseline update may not + increase unexplained upstream-only or field-drift debt without a release-note rationale. +8. Treat agility courses separately from ordinary shortcuts. Do not infer general-walker safety from an + executable object action alone: import course traversal only after a route-level use case proves that + entering and leaving the sequence cannot strand or loop an ordinary walk. + +### 2. Grow the route safety net + +The corpus should cover route properties, not exact tile sequences. Existing coverage includes +underground routing and all three terminal-travel families. Continue adding cases for: + +- underground entrances and exits, including a surface-to-underground-to-surface journey; +- each terminal-travel family (`SHIP`, `NPC`, `BOAT`); +- newly added map regions and high-change regions from each upstream collision update; +- known incident routes before changing their recovery or collision behavior. + +Every corpus case must prove arrival or an intentional partial path and pin the transport/landmark that +makes the route valid. + +### 3. Improve executor behavior from incidents + +Prioritize reproducible symptoms over a wholesale `Rs2Walker` rewrite: + +1. Complete one unlocked hot-air-balloon flight and require logs to show the exact selected edge, one + basket interaction, the exact destination component and observed arrival. Locked-account fail-closed + evidence is complete; do not infer successful UI execution from it. +2. Extend terminal-travel live evidence beyond the completed Port Sarim/Musa Point `SHIP` incident to + one representative `NPC` and `BOAT` edge. The direct Al Kharid/Tempoross Ferry has static selection, + semantic object matching and free-world fail-closed evidence, but still needs a members-world round trip. + The dedicated shadow harness now also proves two outbound and one reverse ship selection: 3/3 exact + `TERMINAL_TRAVEL` comparisons and 4/4 blocking-walk arrivals. Both directions use one interaction per + invocation and accept the current landing without a false timeout; static planner and scan-classification + coverage remains green for all three families. +3. Add a route and live reproduction for Misthalin Mystery door transitions before changing the door + cascade. +4. Complete the remaining Barrows runtime slice. Static mound digs and individual-crypt exits are + represented and corpus-pinned; run one live mound round trip, then add randomized tunnel entry only + through observed empty-sarcophagus state. Do not encode a fixed sarcophagus-to-tunnel edge. +5. Extract a component only when a pinned incident requires changing that behavior; keep the existing + stateful ordering until its replacement has equivalent harness and live evidence. + +### 4. Observability + +Retain a compact decision trail for path recalculation, transport attempts, obstacle resolution and +learned collision. A failed live harness run must make the last planned edge, selected transport and +recovery decision discoverable without reproducing under a debugger. + +Per-edge transport rejection reasons are TRACE-only because a refresh evaluates every expanded catalog +edge. DEBUG retains aggregate per-type counts and timing, while slow refreshes remain visible at INFO; +verbose diagnostics must not make an otherwise healthy refresh exceed the walker timeout. + +### 5. Tighten the planner boundary + +Direct `ShortestPathPlugin` state access outside `Rs2PathApi` has been removed and +`scripts/check-shortest-path-boundary.py` enforces that invariant; the plugin class literal used for +plugin selection is the only non-state exception. Continue replacing the compatibility seam's concrete +internals with Microbot-owned operations and immutable values: + +1. introduce route request/result values that capture targets and planning policy without exposing + `PathfinderConfig`; the first slice is complete: immutable `Rs2RouteRequest`/`Rs2RouteResult` values + and `Rs2PathApi.plan(...)` now own synchronous refresh, search timing and honest-partial results. A fully + resolved immutable `Rs2RoutePolicy` and `Rs2RoutePlanner` engine interface now sit at dispatch; the local + engine and local side of the dual-engine harness both use them, and unresolved requests fail before search; +2. move pathfinder creation, refresh, cancellation and executor ownership behind the seam; + bank discovery, deposit-box discovery and destination transport planning no longer construct + `Pathfinder` directly, and temporary bank-item policy is serialized on the walker mutex and restored + inside the API. The synchronous `Rs2Walker` query helpers (`getTotalTiles`, `canReach`, `getWalkPath`, + multi-target reachability/distance and nearest-accessible-target selection) now use the same immutable + request/result operation; the boundary checker rejects new local-core construction in that class. + Bank-aware queries restore both the previous bank policy and the caller's refresh target. Active route + restart, cancellation, configuration refresh, cave walking-only selection and executor ownership now + also live behind `Rs2PathApi`; the lifecycle package has no concrete planner/config dependency and the + boundary checker enforces that state. Cave selection evaluates every requested target rather than the + iteration order's first target. NPC location selection and direct-vs-bank route comparison no longer + mutate the shared bank-item flag: each route leg declares its policy in its request, restoration avoids + a redundant refresh when the policy was already active, and banking diagnostics consume the exact typed + edges selected by that leg. Active-route consumers outside the shortest-path implementation now read a + generation-tagged immutable `Rs2ActiveRouteStatus` with explicit absent/calculating/ready phases, copied + raw and smoothed paths, owned termination and metrics. `Rs2Walker`, Quest Helper and obstacle handling no + longer import or inspect `Pathfinder`; generation-aware waits cannot accidentally accept a superseded + calculation. Slayer item preparation no longer flips shared bank policy and has an exact immutable-edge + replacement for its deprecated concrete-transport API. The boundary checker now rejects direct active + planner reads everywhere outside the facade, and rejects mutable planner configuration in bank, + deposit-box, NPC, Slayer, banking, lifecycle, walker and Leagues scopes. The walker's collision + preflight, dangerous-NPC recovery policy, restriction refresh, learned-edge recording, spirit-tree + policy, Wilderness classification, teleport-item classification and post-bank inventory-only switch + are named facade operations rather than configuration reads. Leagues invalidation uses the same seam, + while its catalog injection receives only a transport-usability predicate from the local engine instead + of the mutable config. No production code outside the facade or shortest-path implementation imports + `PathfinderConfig`. Remaining work is to decide which concrete-transport overlays are implementation + internals and migrate only the execution-facing contracts that need planner independence; +3. expose immutable transport-edge views needed by execution and banking rather than the mutable catalog; + the banking slice is complete: local path reconstruction retains exact transport identity in both search + directions, `Rs2RouteResult` exposes a contiguous immutable step list, and destination banking consumes + `Rs2TransportEdge` directly. `TransportRouteAnalysis` now snapshots the exact direct, start-to-bank and + bank-to-target step sequences; withdrawal planning consumes the compared bank-to-target sequence instead + of performing a second search from the pre-bank player location. `LegacyRoutePlan` is removed and CI + rejects both its return and the compare-then-replan banking pattern. Active completed + routes now publish an identity-checked immutable snapshot, and both raw-segment dispatch and nearby + current-tile recovery take only the exact transport selected for that route. They no longer rescan the + mutable origin catalog or infer a choice from destination membership. Each edge now carries an explicit + Microbot executor capability, and unregistered catalog rows fail closed before planning. Existing + interaction handlers receive the exact local `Transport` only as an opaque package-private execution + payload where behavior requires it; a blanket conversion of handler signatures to `Rs2TransportEdge` + is explicitly not the architecture target. Replace a payload only when its executor has an equivalent + Microbot-owned command/context interface; +4. prohibit new direct `Pathfinder`, `PathfinderConfig` and mutable transport dependencies only after each + corresponding consumer slice has migrated and equivalent route/runtime evidence exists. + +#### Concrete transport dependency inventory + +The remaining production imports are classified rather than treated as one undifferentiated migration: + +| Ownership | Remaining consumers | Decision | +|---|---|---| +| Planner implementation | `shortestpath/pathfinder/**`, `Rs2PathApi` | Allowed behind the facade. The facade maps selected/catalog entries to immutable Microbot values. | +| Behavior-bearing execution payload | `Rs2Walker`, `PohTransport`, seasonal handlers | Keep only while behavior cannot be represented losslessly by an owned command. POH subtype execution is the explicit current exception. | +| Planner overlay and evidence ingestion | Leagues injection, attempts and observations | Treat as local-engine internals for now; redesign the overlay input/output contract before attempting an engine replacement. Mutable config access is already removed. | +| Deprecated compatibility surface | `Rs2Walker.getTransportsForPath`, concrete-returning `TransportRouteAnalysis` methods, Slayer and banking concrete overloads | Preserve for Hub binary compatibility and keep out of active planning/execution flows. `TransportRouteAnalysis` itself is now an owned immutable analysis value with exact-step getters. | +| Migrated execution/catalog consumers | hot-air balloon, door probing, unified obstacle scene and route recovery | Concrete imports are prohibited by `check-shortest-path-boundary.py`. Presence checks use `hasCatalogTransportOrigin`; classification uses immutable catalog edges. | + +The bank-route distance heuristic and withdrawal selector both consume the exact ordered `Rs2RouteStep` +selected by the compared bank search; neither rematches the mutable catalog nor replans from a different +origin to guess the transport. The next execution-boundary +candidate is a seasonal or POH command contract, selected only after its full runtime inputs and success +conditions are captured in tests and live evidence. + +### 6. Evaluate planner-core convergence + +The ownership split does not justify maintaining a second planner forever. Once the request/result seam +can represent typed route edges and explicit termination without exposing either engine's classes: + +1. **Complete:** package an adapter for the reviewed upstream core and run it beside the local planner from the + same immutable requests and policy snapshots. Static collision, exact explicit-catalog network selection, + start-at-bank availability and separate-bank workflow slices are covered by + `scripts/compare-shortest-path-planners.py`. The gate also compiles an independent pinned checkout and + proves its semantic output matches the production-packaged adapter; +2. compare exact reachability, termination, selected transport identities, path cost, nodes explored, + elapsed time and peak search memory across overland, surface-to-underground-to-surface, unreachable, + wilderness, network-transport and banked/unbanked routes; +3. require preservation of Microbot-only restrictions and prove that upstream path steps contain enough + edge identity for the existing executor, recovery and observability contracts; +4. replace the local core only after the headless corpus is equivalent and the live harness passes the + interaction routes affected by the candidate. Route execution remains Microbot-owned whichever core + wins; +5. record the benchmark and acceptance decision against an exact upstream commit. A failed candidate is + evidence for a specific local requirement, not permission for untracked architectural drift. + +The reviewed upstream `PathStep` still carries only packed position and bank-visited state in its source +tree. Upstream's own display fallback documents that reconstructing a transport from adjacent steps is +ambiguous when more than one valid transport shares an edge. The evaluation checkout therefore applies +`upstream-exact-transport-identity.patch` at the exact reviewed commit: it retains the selected object in +the primitive node graph without changing search ordering or cost. A same-origin/destination corpus case +proves that the faster of two alternatives is reported exactly. This is sufficient for evaluation, but a +production planner replacement still requires equivalent metadata to land upstream or be maintained as +an explicitly reviewed adapter patch. Origin/destination rematching is not an acceptable executor contract. + +Run the current comparison against an existing exact upstream checkout with: + +```bash +scripts/compare-shortest-path-planners.py \ + --upstream-checkout /path/to/Skretzo-shortest-path \ + --require-all +``` + +Without `--upstream-checkout`, the harness clones the reviewed commit into a temporary worktree. The +`--require-all` option exits `2` as soon as a future corpus case requests a capability either engine +explicitly cannot represent; it must be used before any planner replacement decision. + +## Validation gates + +A walker change is ready to merge only when all applicable gates pass: + +1. focused unit/regression tests for the changed behavior; +2. planner core + route corpus for collision or transport-data changes; +3. `./ci/build.sh`; +4. a live harness walk when behavior touches runtime interaction; +5. upstream baseline updated only after the reviewed changes and import/rejection decisions are recorded. +6. semantic transport baseline unchanged, or deliberately updated with the reviewed route identities and + differing fields documented. + +## Historical plans + +- `runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/UPSTREAM_COMPARISON.md` + contains the detailed upstream archaeology. +- `WEBWALKER_IMPROVEMENT_PLAN.md`, `docs/walker-audit.md` and `docs/walker-p2-unification.md` preserve + earlier audits and completed/abandoned design stages. Where they conflict with this file, this roadmap + is authoritative. diff --git a/microbot-cli b/microbot-cli index 1831dbfe23c..8219e3fa396 100755 --- a/microbot-cli +++ b/microbot-cli @@ -73,6 +73,7 @@ Movement: Walk to coordinates (non-blocking by default; --wait blocks until arrival or timeout, default 30s; --distance defaults to 0 for exact coordinates) + walker shadow Planner shadow totals and route-class coverage Banking: bank Bank status (open/closed, items) @@ -368,6 +369,11 @@ case "$1" in esac ;; + walker) + [[ $# -eq 2 && "$2" == "shadow" ]] || { echo '{"error":"Usage: microbot-cli walker shadow"}'; exit 1; } + do_get "${BASE}/walker/shadow" + ;; + walk) [[ $# -lt 3 ]] && { echo '{"error":"Usage: microbot-cli walk [plane] [--wait] [--timeout SECONDS] [--distance TILES]"}'; exit 1; } walk_x="$2" diff --git a/runelite-client/build.gradle.kts b/runelite-client/build.gradle.kts index 10ba41663d9..939a3075990 100644 --- a/runelite-client/build.gradle.kts +++ b/runelite-client/build.gradle.kts @@ -54,6 +54,26 @@ plugins { } +sourceSets.named("main") { + java.srcDir("src/upstreamPlanner/src/main/java") +} + +// The pinned upstream core loads the same reviewed collision archive from its original root path. +// Keep one source artifact and copy it into both runtime resource namespaces during assembly. +tasks.named("processResources") { + from("src/main/resources/net/runelite/client/plugins/microbot/shortestpath/collision-map.zip") { + into("") + } +} + +tasks.withType().configureEach { + exclude("shortestpath/**") +} + +tasks.withType().configureEach { + exclude("shortestpath/**") +} + // Module-system flags required to extend com.apple.eawt.FullScreenAdapter on macOS // (OSXFullScreenAdapter). Without these, the JVM throws IllegalAccessError at class load. val macEawtJvmArgs = listOf( @@ -211,6 +231,51 @@ tasks.register("runUnitTests") { } } +tasks.register("exportLocalPlannerComparison") { + group = "verification" + description = "Export deterministic local planner results for the opt-in upstream comparison harness" + + dependsOn(":client:compileJava", ":client:compileTestJava") + classpath = sourceSets.test.get().runtimeClasspath + mainClass.set("net.runelite.client.plugins.microbot.util.walker.LocalPlannerComparisonMain") + + val corpus = providers.gradleProperty("plannerCorpus") + val output = providers.gradleProperty("plannerOutput") + doFirst { + require(corpus.isPresent && output.isPresent) { + "exportLocalPlannerComparison requires -PplannerCorpus= and -PplannerOutput=" + } + args(rootProject.file(corpus.get()).absolutePath, rootProject.file(output.get()).absolutePath) + systemProperty("microbot.planner.revision", + providers.gradleProperty("plannerRevision").getOrElse("unknown")) + } + + outputs.upToDateWhen { false } +} + +tasks.register("exportEmbeddedUpstreamPlannerComparison") { + group = "verification" + description = "Export results from the production-packaged pinned upstream planner adapter" + + dependsOn(":client:compileJava", ":client:compileTestJava") + classpath = sourceSets.test.get().runtimeClasspath + mainClass.set("net.runelite.client.plugins.microbot.util.walker.LocalPlannerComparisonMain") + + val corpus = providers.gradleProperty("plannerCorpus") + val output = providers.gradleProperty("plannerOutput") + doFirst { + require(corpus.isPresent && output.isPresent) { + "exportEmbeddedUpstreamPlannerComparison requires -PplannerCorpus= and -PplannerOutput=" + } + args(rootProject.file(corpus.get()).absolutePath, rootProject.file(output.get()).absolutePath) + systemProperty("microbot.planner.revision", + providers.gradleProperty("plannerRevision").getOrElse("unknown")) + systemProperty("microbot.planner.embedded-upstream", "true") + } + + outputs.upToDateWhen { false } +} + tasks.register("regenerateClientThreadGuardrailBaseline") { group = "verification" description = "Regenerate src/test/resources/threadsafety/client-thread-guardrail-baseline.txt from current sources" 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 9994f5eb40f..d9a14828768 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 @@ -3,12 +3,12 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; 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.agentserver.handler.ScriptHeartbeatRegistry; import net.runelite.client.plugins.microbot.util.antiban.SessionFatigue; 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.Rs2PathApi; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import org.jetbrains.annotations.NotNull; @@ -56,7 +56,7 @@ public void shutdown() { ScriptHeartbeatRegistry.remove(this.getClass().getName()); if (mainScheduledFuture != null && !mainScheduledFuture.isDone()) { mainScheduledFuture.cancel(true); - ShortestPathPlugin.exit(); + Rs2PathApi.exit(); if (Microbot.getClientThread().scheduledFuture != null) Microbot.getClientThread().scheduledFuture.cancel(true); initialPlayerLocation = null; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/AgentServerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/AgentServerPlugin.java index 607bc2c9904..9399aa130e2 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/AgentServerPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/AgentServerPlugin.java @@ -136,6 +136,7 @@ private List buildHandlers(int maxResults) { new NpcHandler(gson, maxResults), new ObjectHandler(gson, maxResults), new WalkHandler(gson), + new WalkerShadowHandler(gson), new BankHandler(gson), new DialogueHandler(gson), new GroundItemHandler(gson, maxResults), diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WalkerShadowHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WalkerShadowHandler.java new file mode 100644 index 00000000000..bfca5e2830d --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/agentserver/handler/WalkerShadowHandler.java @@ -0,0 +1,167 @@ +package net.runelite.client.plugins.microbot.agentserver.handler; + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import net.runelite.client.plugins.microbot.util.walker.Rs2PathApi; +import net.runelite.client.plugins.microbot.util.walker.Rs2PlannerShadowComparison; +import net.runelite.client.plugins.microbot.util.walker.Rs2PlannerShadowContext; +import net.runelite.client.plugins.microbot.util.walker.Rs2PlannerShadowCoverageStats; +import net.runelite.client.plugins.microbot.util.walker.Rs2PlannerShadowStats; +import net.runelite.client.plugins.microbot.util.walker.Rs2TransportExecutor; +import net.runelite.client.plugins.microbot.util.walker.Rs2TransportType; +import net.runelite.client.plugins.microbot.util.walker.Rs2WalkerShadowExecutionStats; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; + +/** Read-only, coordinate-free production planner shadow evidence. */ +public final class WalkerShadowHandler extends AgentHandler +{ + public WalkerShadowHandler(Gson gson) + { + super(gson); + } + + @Override + public String getPath() + { + return "/walker/shadow"; + } + + @Override + protected void handleRequest(HttpExchange exchange) throws IOException + { + try + { + requireGet(exchange); + } + catch (HttpMethodException error) + { + sendJson(exchange, 405, errorResponse(error.getMessage())); + return; + } + sendJson(exchange, 200, snapshot()); + } + + /** Coordinate-free schema-v2 snapshot shared by the read-only endpoint and live test harnesses. */ + public static Map snapshot() + { + Rs2PlannerShadowStats stats = Rs2PathApi.getShadowStats(); + Map response = new LinkedHashMap<>(); + response.put("schemaVersion", 2); + response.put("enabled", Rs2PathApi.isUpstreamPlannerShadowEnabled()); + response.put("plannerMode", Rs2PathApi.getPlannerSelectionMode().name()); + response.put("candidateEngineId", Rs2PathApi.getUpstreamPlannerEngineId()); + response.put("startedAtEpochMillis", stats.getStartedAtEpochMillis()); + Map totals = new LinkedHashMap<>(); + totals.put("submitted", stats.getSubmitted()); + totals.put("completed", stats.getCompleted()); + totals.put("matches", stats.getMatches()); + totals.put("divergences", stats.getDivergences()); + totals.put("failures", stats.getFailures()); + totals.put("staleResults", stats.getStaleResults()); + totals.put("discarded", stats.getDiscarded()); + totals.put("pending", stats.getPending()); + totals.put("routeShapeDifferences", stats.getRouteShapeDifferences()); + totals.put("upstreamCanarySelections", stats.getUpstreamCanarySelections()); + totals.put("localFallbackDivergences", stats.getLocalFallbackDivergences()); + totals.put("localFallbackFailures", stats.getLocalFallbackFailures()); + response.put("totals", totals); + + Map coverage = new LinkedHashMap<>(); + for (Map.Entry + entry : stats.getCoverage().entrySet()) + { + Rs2PlannerShadowCoverageStats value = entry.getValue(); + Map outcomes = new LinkedHashMap<>(); + outcomes.put("completed", value.getCompleted()); + outcomes.put("matches", value.getMatches()); + outcomes.put("divergences", value.getDivergences()); + outcomes.put("failures", value.getFailures()); + coverage.put(entry.getKey().name(), outcomes); + } + response.put("coverage", coverage); + Map transportExecutors = new LinkedHashMap<>(); + for (Map.Entry + entry : stats.getTransportExecutors().entrySet()) + { + transportExecutors.put(entry.getKey().name(), outcomes(entry.getValue())); + } + response.put("transportExecutors", transportExecutors); + Map transportTypes = new LinkedHashMap<>(); + for (Map.Entry + entry : stats.getTransportTypes().entrySet()) + { + transportTypes.put(entry.getKey().name(), outcomes(entry.getValue())); + } + response.put("transportTypes", transportTypes); + Rs2WalkerShadowExecutionStats executionStats = stats.getExecution(); + Map execution = new LinkedHashMap<>(); + execution.put("terminal", executionStats.getTerminal()); + execution.put("arrived", executionStats.getArrived()); + execution.put("unreachable", executionStats.getUnreachable()); + execution.put("exited", executionStats.getExited()); + execution.put("recoveryTerminal", executionStats.getRecoveryTerminal()); + execution.put("recoveryArrived", executionStats.getRecoveryArrived()); + execution.put("recoveryUnreachable", executionStats.getRecoveryUnreachable()); + execution.put("recoveryExited", executionStats.getRecoveryExited()); + response.put("execution", execution); + response.put("latest", Rs2PathApi.getLastShadowComparison() + .map(WalkerShadowHandler::comparison).orElse(null)); + response.put("latestRouteShapeDifference", Rs2PathApi.getLastRouteShapeDifference() + .map(WalkerShadowHandler::comparison).orElse(null)); + response.put("latestDivergence", Rs2PathApi.getLastDivergence() + .map(WalkerShadowHandler::comparison).orElse(null)); + response.put("latestFailure", Rs2PathApi.getLastPlannerFailure() + .map(WalkerShadowHandler::comparison).orElse(null)); + return response; + } + + private static Map outcomes(Rs2PlannerShadowCoverageStats value) + { + Map outcomes = new LinkedHashMap<>(); + outcomes.put("completed", value.getCompleted()); + outcomes.put("matches", value.getMatches()); + outcomes.put("divergences", value.getDivergences()); + outcomes.put("failures", value.getFailures()); + return outcomes; + } + + private static Map comparison(Rs2PlannerShadowComparison comparison) + { + Map result = new LinkedHashMap<>(); + result.put("status", comparison.getStatus().name()); + result.put("shadowEngineId", comparison.getShadowEngineId()); + result.put("invocation", comparison.getContext().getInvocation().name()); + ArrayList coverage = new ArrayList<>(); + for (Rs2PlannerShadowContext.Coverage value : comparison.getContext().getCoverage()) + { + coverage.add(value.name()); + } + result.put("coverage", coverage); + result.put("terminationMatches", comparison.isTerminationMatches()); + result.put("endpointMatches", comparison.isEndpointMatches()); + result.put("costComparable", comparison.isCostComparable()); + result.put("costMatches", comparison.isCostMatches()); + result.put("selectedTransportsMatch", comparison.isSelectedTransportsMatch()); + result.put("pathMatches", comparison.isPathMatches()); + ArrayList transportExecutors = new ArrayList<>(); + for (Rs2TransportExecutor value : comparison.getContext().getTransportExecutors()) + { + transportExecutors.add(value.name()); + } + result.put("transportExecutors", transportExecutors); + ArrayList transportTypes = new ArrayList<>(); + for (Rs2TransportType value : comparison.getContext().getTransportTypes()) + { + transportTypes.add(value.name()); + } + result.put("transportTypes", transportTypes); + result.put("localSearchNanos", comparison.getLocalSearchNanos()); + result.put("shadowSearchNanos", comparison.getShadowSearchNanos()); + result.put("failureType", comparison.getFailureType()); + return result; + } +} 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 1ccfffa4041..2768b5d8ce3 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 @@ -18,7 +18,6 @@ import net.runelite.client.plugins.microbot.questhelper.requirements.item.ItemRequirement; import net.runelite.client.plugins.microbot.questhelper.steps.*; import net.runelite.client.plugins.microbot.questhelper.steps.widget.WidgetHighlight; -import net.runelite.client.plugins.microbot.shortestpath.ShortestPathPlugin; import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; import net.runelite.client.plugins.microbot.util.combat.Rs2Combat; @@ -33,6 +32,7 @@ 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.tile.Rs2Tile; +import net.runelite.client.plugins.microbot.util.walker.Rs2PathApi; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; @@ -234,8 +234,8 @@ public boolean run(QuestHelperConfig config, QuestHelperPlugin mQuestPlugin) { boolean isInCutscene = Microbot.getVarbitValue(4606) > 0; if (isInCutscene) { - if (ShortestPathPlugin.getMarker() != null) - ShortestPathPlugin.exit(); + if (Rs2PathApi.getMarker() != null) + Rs2PathApi.exit(); return; } @@ -1385,11 +1385,11 @@ public boolean applyObjectStep(ObjectStep step) { for (var tile : Rs2Tile.getWalkableTilesAroundTile(object.getWorldLocation(), unreachableTargetCheckDist)) { if (tileObjects.stream().noneMatch(x -> x.getWorldLocation().equals(tile))) { - if (!Rs2Walker.walkTo(tile) && ShortestPathPlugin.getPathfinder() == null) + if (!Rs2Walker.walkTo(tile) && !Rs2PathApi.getActiveRouteStatus().isPresent()) return false; - sleepUntil(() -> ShortestPathPlugin.getPathfinder() == null || ShortestPathPlugin.getPathfinder().isDone()); - if (ShortestPathPlugin.getPathfinder() == null || ShortestPathPlugin.getPathfinder().isDone()) { + sleepUntil(() -> !Rs2PathApi.getActiveRouteStatus().isCalculating()); + if (!Rs2PathApi.getActiveRouteStatus().isCalculating()) { unreachableTarget = false; unreachableTargetCheckDist = 1; } @@ -1424,9 +1424,11 @@ public boolean applyObjectStep(ObjectStep step) { Rs2Walker.walkTo(targetTile, 3); - if (ShortestPathPlugin.getPathfinder() != null) { - var path = ShortestPathPlugin.getPathfinder().getPath(); - if (path.get(path.size() - 1).distanceTo(step.getDefinedPoint().getWorldPoint()) <= 1) + var activeRoute = Rs2PathApi.getActiveRouteStatus(); + if (activeRoute.isPresent()) { + var endpoint = activeRoute.getEndpoint(); + if (endpoint.isPresent() + && endpoint.get().distanceTo(step.getDefinedPoint().getWorldPoint()) <= 1) return false; } else return false; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/PlannerSelectionMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/PlannerSelectionMode.java new file mode 100644 index 00000000000..d089857657a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/PlannerSelectionMode.java @@ -0,0 +1,48 @@ +package net.runelite.client.plugins.microbot.shortestpath; + +/** + * Explicit production planner rollout state. + * + *

A single mode prevents contradictory combinations such as selecting the upstream planner while + * comparison telemetry is disabled. The canary is deliberately limited to resolved F2P policy; members + * routes remain local until their own evidence gate is accepted.

+ */ +public enum PlannerSelectionMode +{ + /** Run only the local Microbot planner. */ + LOCAL, + /** Keep the local planner authoritative and compare the pinned upstream planner asynchronously. */ + SHADOW, + /** Select a semantically matching upstream result for F2P routes, with an automatic local fallback. */ + UPSTREAM_F2P_CANARY; + + public boolean comparisonEnabled() + { + return this != LOCAL; + } + + public boolean f2pCanaryEnabled() + { + return this == UPSTREAM_F2P_CANARY; + } + + public static PlannerSelectionMode fromConfigValue(Object value, PlannerSelectionMode defaultValue) + { + if (value instanceof PlannerSelectionMode) + { + return (PlannerSelectionMode) value; + } + if (value instanceof String) + { + try + { + return PlannerSelectionMode.valueOf(((String) value).trim().toUpperCase()); + } + catch (IllegalArgumentException ignored) + { + // Invalid test/plugin-message overrides fail closed to the persisted/default mode. + } + } + return defaultValue; + } +} 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 f718e343095..d6612a06d4e 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 @@ -904,4 +904,17 @@ default boolean useLiveCollision() { default boolean resetLearnedCollision() { return false; } + + @ConfigItem( + keyName = "plannerSelectionMode", + name = "Planner rollout mode", + description = "Local is the production default. Shadow compares the pinned upstream planner. " + + "The F2P canary selects only semantically matching upstream routes and automatically " + + "falls back to local; members routes remain local.", + position = 3, + section = sectionDeveloper + ) + default PlannerSelectionMode plannerSelectionMode() { + return PlannerSelectionMode.LOCAL; + } } 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 073c4825dc0..a86cc2b66e8 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 @@ -62,6 +62,7 @@ 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.walker.Rs2Walker; +import net.runelite.client.plugins.microbot.util.walker.Rs2TransportPlanningPolicy; import net.runelite.client.ui.ClientToolbar; import net.runelite.client.ui.JagexColors; import net.runelite.client.ui.NavigationButton; @@ -227,7 +228,9 @@ protected void startUp() { Map> transports = Transport.loadAllFromResources(); List restrictions = Restriction.loadAllFromResources(); - pathfinderConfig = new PathfinderConfig(map, transports, restrictions, client, config); + pathfinderConfig = new PathfinderConfig( + map, transports, restrictions, client, config, + Rs2TransportPlanningPolicy.INSTANCE); panel = injector.getInstance(ShortestPathPanel.class); pohPanel = new PohPanel(config); @@ -992,6 +995,15 @@ public static TeleportationItem override(String configOverrideKey, Teleportation return defaultValue; } + public static PlannerSelectionMode override( + String configOverrideKey, PlannerSelectionMode defaultValue) { + if (!configOverride.isEmpty()) { + return PlannerSelectionMode.fromConfigValue( + configOverride.get(configOverrideKey), defaultValue); + } + return defaultValue; + } + private TileCounter override(String configOverrideKey, TileCounter defaultValue) { if (!configOverride.isEmpty()) { Object value = configOverride.get(configOverrideKey); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/Transport.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/Transport.java index db3d2900553..2b33e9069fa 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/Transport.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/Transport.java @@ -16,6 +16,11 @@ */ @Slf4j public class Transport { + public static final int TOTAL_LEVEL_INDEX = Skill.values().length; + public static final int COMBAT_LEVEL_INDEX = TOTAL_LEVEL_INDEX + 1; + public static final int QUEST_POINTS_INDEX = COMBAT_LEVEL_INDEX + 1; + public static final int REQUIREMENT_LEVEL_COUNT = QUEST_POINTS_INDEX + 1; + //START microbot variables @Getter @Setter @@ -46,7 +51,7 @@ public class Transport { * The skill levels required to use this transport */ @Getter - private final int[] skillLevels = new int[Skill.values().length]; + private final int[] skillLevels = new int[REQUIREMENT_LEVEL_COUNT]; /** * The quests required to use this transport @@ -55,14 +60,19 @@ public class Transport { private Map quests = new HashMap<>(); /** - * The ids of items required to use this transport. - * If the player has **any** of the matching list of items, - * this transport is valid + * Compatibility view of the item IDs required to use this transport. New code should use + * {@link #getItemRequirements()} so AND groups and quantities are not discarded. */ @Getter - @Setter private Set> itemIdRequirements = new HashSet<>(); + /** + * Lossless item requirements. Entries are AND-ed; alternatives within an entry are OR-ed. + * {@link #itemIdRequirements} remains as the compatibility view used by older callers. + */ + @Getter + private List itemRequirements = new ArrayList<>(); + /** * The type of transport */ @@ -138,6 +148,8 @@ public Transport(Transport origin, Transport destination) { this.itemIdRequirements.addAll(origin.itemIdRequirements); this.itemIdRequirements.addAll(destination.itemIdRequirements); + this.itemRequirements.addAll(origin.itemRequirements); + this.itemRequirements.addAll(destination.itemRequirements); this.type = origin.type; @@ -186,7 +198,13 @@ public Transport(WorldPoint origin, WorldPoint destination, String displayInfo, * Object interaction Transport constructor */ public Transport(WorldPoint origin, WorldPoint destination, String displayInfo, TransportType transportType, boolean isMember, String action, String target, int objectId) { - this(origin, destination, displayInfo, transportType, isMember, 1); + this(origin, destination, displayInfo, transportType, isMember, action, target, objectId, 1); + } + + /** Object interaction transport with an explicit planner cost in ticks. */ + public Transport(WorldPoint origin, WorldPoint destination, String displayInfo, TransportType transportType, + boolean isMember, String action, String target, int objectId, int duration) { + this(origin, destination, displayInfo, transportType, isMember, duration); this.action = action; this.name = target; this.objectId = objectId; @@ -198,7 +216,7 @@ public Transport(WorldPoint origin, WorldPoint destination, String displayInfo, public Transport(WorldPoint destination, String displayInfo, TransportType transportType, boolean isMember, int maxWildernessLevel, Set> itemIdRequirements) { this(null, destination, displayInfo, transportType, isMember, 1); this.maxWildernessLevel = maxWildernessLevel; - this.itemIdRequirements = itemIdRequirements != null ? new HashSet<>(itemIdRequirements) : new HashSet<>(); + setItemIdRequirements(itemIdRequirements); } /** @@ -244,10 +262,17 @@ public Transport(WorldPoint destination, String displayInfo, TransportType trans if ((value = fieldMap.get("menuOption menuTarget objectID")) != null && !value.trim().isEmpty()) { value = value.trim(); // Remove leading/trailing spaces - // Regex pattern for semicolon-separated values - String regex = "^([^;]+);([^;]+);(\\d+)$"; - java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(regex); - java.util.regex.Matcher matcher = pattern.matcher(value); + // Microbot historically used semicolons while upstream uses whitespace. In the + // whitespace form the option is one token, the object id is the final numeric token, + // and the target may contain spaces. + java.util.regex.Matcher matcher = java.util.regex.Pattern + .compile("^([^;]+);([^;]+);(\\d+)$") + .matcher(value); + if (!matcher.matches()) { + matcher = java.util.regex.Pattern + .compile("^(\\S+)\\s+(.+?)\\s+(\\d+)$") + .matcher(value); + } if (matcher.matches()) { // Extract matched groups @@ -276,14 +301,14 @@ public Transport(WorldPoint destination, String displayInfo, TransportType trans String[] skillRequirements = value.split(DELIM_MULTI); for (String requirement : skillRequirements) { - String[] levelAndSkill = requirement.split(DELIM); + String[] levelAndSkill = requirement.trim().split("\\s+", 2); if (levelAndSkill.length < 2) { continue; } int level = Integer.parseInt(levelAndSkill[0]); - String skillName = levelAndSkill[1]; + String skillName = levelAndSkill[1].trim(); Skill[] skills = Skill.values(); for (int i = 0; i < skills.length; i++) { @@ -292,19 +317,36 @@ public Transport(WorldPoint destination, String displayInfo, TransportType trans break; } } + String normalizedSkillName = skillName.toLowerCase(Locale.ROOT); + if (normalizedSkillName.startsWith("total")) { + skillLevels[TOTAL_LEVEL_INDEX] = level; + } else if (normalizedSkillName.startsWith("combat")) { + skillLevels[COMBAT_LEVEL_INDEX] = level; + } else if (normalizedSkillName.startsWith("quest")) { + skillLevels[QUEST_POINTS_INDEX] = level; + } } } - if ((value = fieldMap.get("Item IDs")) != null && !value.trim().isEmpty()) { - String[] itemIdsList = value.split(DELIM_MULTI); - for (String listIds : itemIdsList) { - Set multiitemList = new HashSet<>(); - String[] itemIds = listIds.split(DELIM); - for (String item : itemIds) { - int itemId = Integer.parseInt(item); - multiitemList.add(itemId); + if ((value = fieldMap.get("Items")) != null && !value.trim().isEmpty()) { + setItemRequirements(TransportItemRequirement.parseRequirements(value)); + } else if ((value = fieldMap.get("Item IDs")) != null && !value.trim().isEmpty()) { + if (value.contains("=") || value.contains("&") || value.contains("|")) { + setItemRequirements(TransportItemRequirement.parseRequirements(value)); + } else { + Set> legacyGroups = new LinkedHashSet<>(); + for (String listIds : value.split(DELIM_MULTI)) { + Set group = new LinkedHashSet<>(); + for (String item : listIds.trim().split("\\s+")) { + if (!item.isEmpty()) { + group.add(Integer.parseInt(item)); + } + } + if (!group.isEmpty()) { + legacyGroups.add(group); + } } - itemIdRequirements.add(multiitemList); + setItemIdRequirements(legacyGroups); } } @@ -376,7 +418,11 @@ public Transport(WorldPoint destination, String displayInfo, TransportType trans } } - if ((value = fieldMap.get("Varplayers")) != null && !value.trim().isEmpty()) { + value = fieldMap.get("Varplayers"); + if ((value == null || value.trim().isEmpty())) { + value = fieldMap.get("VarPlayers"); + } + if (value != null && !value.trim().isEmpty()) { for (String varplayerCheck : value.split(DELIM_MULTI)) { if (varplayerCheck.isBlank()) { continue; @@ -428,6 +474,53 @@ private int getRequiredLevel(Skill skill) { return skillLevels[skill.ordinal()]; } + public int getRequiredTotalLevel() { + return skillLevels[TOTAL_LEVEL_INDEX]; + } + + public int getRequiredCombatLevel() { + return skillLevels[COMBAT_LEVEL_INDEX]; + } + + public int getRequiredQuestPoints() { + return skillLevels[QUEST_POINTS_INDEX]; + } + + /** + * Updates the legacy compatibility view. Historically every ID in this structure was treated as + * an alternative, regardless of its nested set, so preserve that behavior as one OR requirement. + */ + public void setItemIdRequirements(Set> requirements) { + Set> copied = new LinkedHashSet<>(); + Set alternatives = new LinkedHashSet<>(); + if (requirements != null) { + for (Set group : requirements) { + if (group == null || group.isEmpty()) { + continue; + } + Set copiedGroup = new LinkedHashSet<>(group); + copied.add(Collections.unmodifiableSet(copiedGroup)); + alternatives.addAll(copiedGroup); + } + } + this.itemIdRequirements = copied; + this.itemRequirements = alternatives.isEmpty() + ? new ArrayList<>() + : new ArrayList<>(Collections.singletonList( + TransportItemRequirement.legacyAlternatives(alternatives))); + } + + private void setItemRequirements(List requirements) { + this.itemRequirements = requirements == null + ? new ArrayList<>() + : new ArrayList<>(requirements); + Set> compatibility = new LinkedHashSet<>(); + for (TransportItemRequirement requirement : this.itemRequirements) { + compatibility.add(Collections.unmodifiableSet(new LinkedHashSet<>(requirement.getAllItemIds()))); + } + this.itemIdRequirements = compatibility; + } + /** * Whether the transport has one or more quest requirements */ @@ -639,6 +732,7 @@ public String toString() { ", skillLevels=" + Arrays.toString(skillLevels) + ", quests=" + quests + ", itemIdRequirements=" + itemIdRequirements + + ", itemRequirements=" + itemRequirements + ", type=" + type + ", duration=" + duration + ", displayInfo='" + displayInfo + '\'' + diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/TransportExecutionRegistry.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/TransportExecutionRegistry.java new file mode 100644 index 00000000000..5e0a2a8e834 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/TransportExecutionRegistry.java @@ -0,0 +1,341 @@ +package net.runelite.client.plugins.microbot.shortestpath; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.gameval.ItemID; +import net.runelite.client.plugins.microbot.util.poh.PohTransport; +import net.runelite.client.plugins.skillcalculator.skills.MagicAction; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Pure planner-side description of the Microbot executor capabilities. + * + *

A transport must not be offered to automated pathfinding unless the walker has a concrete + * execution branch for it. Keeping that decision here prevents catalog convergence from silently + * turning data coverage into routes that the runtime cannot complete.

+ */ +public final class TransportExecutionRegistry +{ + public enum Executor + { + BARROWS_DIG, + CANOE, + CHARTER_SHIP, + FAIRY_RING, + GNOME_GLIDER, + HOT_AIR_BALLOON, + ITEM_TELEPORT, + MAGIC_CARPET, + MAGIC_MUSHTREE, + MINIGAME_TELEPORT, + OBJECT, + POH, + QUETZAL, + SEASONAL, + SPELL_TELEPORT, + SPIRIT_TREE, + TERMINAL_TRAVEL, + WILDERNESS_OBELISK + } + + /** Planner-visible interaction sequence supported by the terminal-travel executor. */ + public enum TerminalTravelMode + { + DIRECT, + DIALOGUE_DESTINATION + } + + /** Exact destination labels presented by the unlocked balloon network map. */ + public enum BalloonDestination + { + CASTLE_WARS("Castle Wars"), + GRAND_TREE("Grand Tree"), + CRAFTING_GUILD("Crafting Guild"), + ENTRANA("Entrana"), + TAVERLEY("Taverley"), + VARROCK("Varrock"); + + private final String displayName; + + BalloonDestination(String displayName) + { + this.displayName = displayName; + } + + public String getDisplayName() + { + return displayName; + } + } + + /** Home teleports are zero-rune spellbook widgets rather than ordinary {@link MagicAction}s. */ + public enum HomeTeleport + { + LUMBRIDGE("Lumbridge Home Teleport"), + EDGEVILLE("Edgeville Home Teleport"), + LUNAR("Lunar Home Teleport"), + ARCEUUS("Arceuus Home Teleport"); + + private final String displayName; + + HomeTeleport(String displayName) + { + this.displayName = displayName; + } + + public String getDisplayName() + { + return displayName; + } + } + + private static final Set GENERIC_OBJECT_EXECUTORS = EnumSet.of( + TransportType.TRANSPORT, + TransportType.AGILITY_SHORTCUT, + TransportType.GRAPPLE_SHORTCUT, + TransportType.MINECART, + TransportType.TELEPORTATION_LEVER, + TransportType.TELEPORTATION_PORTAL, + TransportType.MAGIC_MUSHTREE); + + /** + * Barrows mound digs are inventory-item interactions, not scene-object interactions. Keep the + * six deterministic surface-to-crypt mappings exact so an arbitrary object-less transport row + * cannot acquire the spade executor by naming itself "Dig". + */ + private static final Map + BARROWS_DIG_DESTINATIONS = Map.of( + new WorldPoint(3564, 3291, 0), new WorldPoint(3559, 9703, 3), + new WorldPoint(3575, 3299, 0), new WorldPoint(3558, 9718, 3), + new WorldPoint(3578, 3281, 0), new WorldPoint(3534, 9706, 3), + new WorldPoint(3567, 3274, 0), new WorldPoint(3546, 9686, 3), + new WorldPoint(3553, 3281, 0), new WorldPoint(3566, 9683, 3), + new WorldPoint(3556, 3297, 0), new WorldPoint(3578, 9704, 3)); + + private TransportExecutionRegistry() + { + } + + public static boolean canExecute(Transport transport) + { + return executorFor(transport).isPresent(); + } + + /** Resolve the walker branch without reading live client state. */ + public static Optional executorFor(Transport transport) + { + if (transport == null || transport.getType() == null || transport.getDestination() == null) + { + return Optional.empty(); + } + + TransportType type = transport.getType(); + if (isBarrowsDig(transport)) + { + return Optional.of(Executor.BARROWS_DIG); + } + if (type == TransportType.TELEPORTATION_SPELL) + { + return hasRegisteredSpell(transport.getDisplayInfo()) + ? Optional.of(Executor.SPELL_TELEPORT) + : Optional.empty(); + } + if (type == TransportType.POH) + { + return transport instanceof PohTransport + ? Optional.of(Executor.POH) + : Optional.empty(); + } + if (type == TransportType.HOT_AIR_BALLOON) + { + return hasRegisteredBalloon(transport) + ? Optional.of(Executor.HOT_AIR_BALLOON) + : Optional.empty(); + } + if (isTerminalTravelType(type)) + { + return terminalTravelModeFor(transport).isPresent() + ? Optional.of(Executor.TERMINAL_TRAVEL) + : Optional.empty(); + } + if (GENERIC_OBJECT_EXECUTORS.contains(type)) + { + return hasObjectInteraction(transport) + ? Optional.of(type == TransportType.MAGIC_MUSHTREE + ? Executor.MAGIC_MUSHTREE + : Executor.OBJECT) + : Optional.empty(); + } + + return Optional.ofNullable(specializedExecutor(type)); + } + + private static boolean isBarrowsDig(Transport transport) + { + if (transport.getType() != TransportType.TRANSPORT + || transport.getObjectId() != 0 + || !"Dig".equalsIgnoreCase(transport.getAction()) + || !"Barrow".equalsIgnoreCase(transport.getName()) + || !transport.getDestination().equals(BARROWS_DIG_DESTINATIONS.get(transport.getOrigin())) + || transport.getItemRequirements().size() != 1) + { + return false; + } + TransportItemRequirement spade = transport.getItemRequirements().get(0); + return spade.getAllItemIds().equals(Set.of(ItemID.SPADE)) + && spade.getRequiredQuantity(ItemID.SPADE) == 1; + } + + private static Executor specializedExecutor(TransportType type) + { + switch (type) + { + case CANOE: + return Executor.CANOE; + case CHARTER_SHIP: + return Executor.CHARTER_SHIP; + case FAIRY_RING: + return Executor.FAIRY_RING; + case GNOME_GLIDER: + return Executor.GNOME_GLIDER; + case MAGIC_CARPET: + return Executor.MAGIC_CARPET; + case QUETZAL: + return Executor.QUETZAL; + case SPIRIT_TREE: + return Executor.SPIRIT_TREE; + case TELEPORTATION_ITEM: + return Executor.ITEM_TELEPORT; + case TELEPORTATION_MINIGAME: + return Executor.MINIGAME_TELEPORT; + case WILDERNESS_OBELISK: + return Executor.WILDERNESS_OBELISK; + case SEASONAL_TRANSPORT: + return Executor.SEASONAL; + default: + return null; + } + } + + /** + * Resolve the complete interaction flow, not merely the catalog family. + * + *

The SHIP/NPC/BOAT files describe journeys and contain both NPC and scene-object targets. + * Target kind is therefore resolved live. Interaction sequence is catalog policy, however, and must + * be known before planning. Unknown or currently unimplemented sequences fail closed here.

+ */ + public static Optional terminalTravelModeFor(Transport transport) + { + if (transport == null + || !isTerminalTravelType(transport.getType()) + || transport.getOrigin() == null + || transport.getDestination() == null + || transport.getObjectId() <= 0 + || isBlank(transport.getName()) + || isBlank(transport.getAction())) + { + return Optional.empty(); + } + + if (requiresUnsupportedTerminalDestinationSelection(transport)) + { + return Optional.empty(); + } + if ("Mountain Guide".equalsIgnoreCase(transport.getName())) + { + return isBlank(transport.getDisplayInfo()) + ? Optional.empty() + : Optional.of(TerminalTravelMode.DIALOGUE_DESTINATION); + } + return Optional.of(TerminalTravelMode.DIRECT); + } + + private static boolean isTerminalTravelType(TransportType type) + { + return type == TransportType.SHIP || type == TransportType.NPC || type == TransportType.BOAT; + } + + private static boolean requiresUnsupportedTerminalDestinationSelection(Transport transport) + { + String action = transport.getAction(); + String target = transport.getName(); + if (transport.getType() == TransportType.BOAT && !isBlank(transport.getDisplayInfo())) + { + return ("Board".equalsIgnoreCase(action) + && ("Boaty".equalsIgnoreCase(target) || "Boat".equalsIgnoreCase(target))) + || ("Travel".equalsIgnoreCase(action) && "Rowboat".equalsIgnoreCase(target)); + } + return "Talk-to".equalsIgnoreCase(action) + && ("Captain Shanks".equalsIgnoreCase(target) || "Pirate Pete".equalsIgnoreCase(target)); + } + + private static boolean hasObjectInteraction(Transport transport) + { + return transport.getOrigin() != null + && transport.getObjectId() > 0 + && !isBlank(transport.getAction()); + } + + private static boolean hasRegisteredSpell(String displayInfo) + { + if (isBlank(displayInfo)) + { + return false; + } + if (homeTeleportFor(displayInfo).isPresent()) + { + return true; + } + String spellName = displayInfo.contains(":") + ? displayInfo.substring(0, displayInfo.indexOf(':')).trim() + : displayInfo.trim(); + return Arrays.stream(MagicAction.values()) + .anyMatch(action -> action.getName().toLowerCase(Locale.ROOT) + .contains(spellName.toLowerCase(Locale.ROOT))); + } + + /** Resolve the exact home-teleport widget family shared by planning and execution. */ + public static Optional homeTeleportFor(String displayInfo) + { + if (isBlank(displayInfo)) + { + return Optional.empty(); + } + String normalized = displayInfo.trim().toLowerCase(Locale.ROOT); + return Arrays.stream(HomeTeleport.values()) + .filter(teleport -> teleport.getDisplayName().toLowerCase(Locale.ROOT).equals(normalized)) + .findFirst(); + } + + /** Resolve an exact balloon-map destination shared by capability filtering and runtime dispatch. */ + public static Optional balloonDestinationFor(String displayInfo) + { + if (isBlank(displayInfo)) + { + return Optional.empty(); + } + String normalized = displayInfo.trim().toLowerCase(Locale.ROOT); + return Arrays.stream(BalloonDestination.values()) + .filter(destination -> destination.getDisplayName().toLowerCase(Locale.ROOT).equals(normalized)) + .findFirst(); + } + + private static boolean hasRegisteredBalloon(Transport transport) + { + return hasObjectInteraction(transport) + && (transport.getObjectId() == 19128 || transport.getObjectId() == 19129) + && "Use".equalsIgnoreCase(transport.getAction()) + && "Basket".equalsIgnoreCase(transport.getName()) + && balloonDestinationFor(transport.getDisplayInfo()).isPresent(); + } + + private static boolean isBlank(String value) + { + return value == null || value.trim().isEmpty(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/TransportItemRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/TransportItemRequirement.java new file mode 100644 index 00000000000..d25d88163a2 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/TransportItemRequirement.java @@ -0,0 +1,280 @@ +package net.runelite.client.plugins.microbot.shortestpath; + +import lombok.Getter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.IntPredicate; +import java.util.function.IntUnaryOperator; + +/** + * One item requirement for a transport. + * + *

The alternatives are OR-ed and retain their individual quantities. A transport may contain + * multiple instances of this class; those requirements are AND-ed. The upstream parser normalizes + * every alternative in an OR group to that group's maximum quantity; {@link #parseRequirements} + * deliberately applies the same rule.

+ */ +public final class TransportItemRequirement { + @Getter + private final Map alternatives; + @Getter + private final Set staffAlternatives; + @Getter + private final Set offhandAlternatives; + @Getter + private final boolean runeOnly; + + public TransportItemRequirement(Map alternatives) { + this(alternatives, Collections.emptySet(), Collections.emptySet(), false); + } + + public TransportItemRequirement(Map alternatives, + Set staffAlternatives, Set offhandAlternatives, boolean runeOnly) { + if (alternatives == null || alternatives.isEmpty()) { + throw new IllegalArgumentException("item requirement must contain an alternative"); + } + Map copy = new LinkedHashMap<>(); + for (Map.Entry entry : alternatives.entrySet()) { + Integer itemId = entry.getKey(); + Integer quantity = entry.getValue(); + if (itemId == null || itemId <= 0) { + throw new IllegalArgumentException("item id must be positive: " + itemId); + } + if (quantity == null || quantity < 0) { + throw new IllegalArgumentException("item quantity must be non-negative: " + quantity); + } + if (copy.put(itemId, quantity) != null) { + throw new IllegalArgumentException("duplicate item alternative: " + itemId); + } + } + this.alternatives = Collections.unmodifiableMap(copy); + this.staffAlternatives = immutablePositiveIds(staffAlternatives, "staff"); + this.offhandAlternatives = immutablePositiveIds(offhandAlternatives, "offhand"); + this.runeOnly = runeOnly; + } + + private static Set immutablePositiveIds(Set itemIds, String label) { + if (itemIds == null || itemIds.isEmpty()) { + return Collections.emptySet(); + } + LinkedHashSet copy = new LinkedHashSet<>(); + for (Integer itemId : itemIds) { + if (itemId == null || itemId <= 0) { + throw new IllegalArgumentException(label + " item id must be positive: " + itemId); + } + copy.add(itemId); + } + return Collections.unmodifiableSet(copy); + } + + public static TransportItemRequirement legacyAlternatives(Set itemIds) { + Map alternatives = new LinkedHashMap<>(); + for (Integer itemId : itemIds) { + alternatives.put(itemId, 1); + } + return new TransportItemRequirement(alternatives); + } + + /** + * Parses the numeric subset of the upstream item grammar. Symbolic item collections must be + * resolved by the pinned schema adapter before reaching Microbot resources. + */ + public static List parseNumericRequirements(String value) { + return parseRequirements(value, false); + } + + /** + * Parses numeric item ids plus explicitly supported symbolic collections from the pinned adapter. + * Unsupported collections fail closed rather than being omitted from the transport. + */ + public static List parseRequirements(String value) { + return parseRequirements(value, true); + } + + private static List parseRequirements(String value, boolean allowSymbols) { + if (value == null || value.trim().isEmpty()) { + return Collections.emptyList(); + } + String normalized = value.replace(" ", "") + .replace("&&", "&") + .replace("||", "|"); + List requirements = new ArrayList<>(); + for (String andPart : normalized.split("&", -1)) { + if (andPart.isEmpty()) { + throw new IllegalArgumentException("empty AND item requirement in: " + value); + } + Map parsedAlternatives = new LinkedHashMap<>(); + Set staffAlternatives = new LinkedHashSet<>(); + Set offhandAlternatives = new LinkedHashSet<>(); + boolean runeOnly = true; + int maximumQuantity = -1; + for (String orPart : andPart.split("\\|", -1)) { + String[] itemAndQuantity = orPart.split("=", -1); + if (itemAndQuantity.length != 2) { + throw new IllegalArgumentException("invalid item requirement: " + orPart); + } + final int quantity; + try { + quantity = Integer.parseInt(itemAndQuantity[1]); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "unresolved symbolic or invalid item requirement: " + orPart, e); + } + Set itemIds; + try { + itemIds = Collections.singleton(Integer.parseInt(itemAndQuantity[0])); + runeOnly = false; + } catch (NumberFormatException e) { + TransportItemResolver.Resolution resolution = allowSymbols + ? TransportItemResolver.resolve(itemAndQuantity[0]) : null; + if (resolution == null) { + throw new IllegalArgumentException( + "unresolved symbolic or invalid item requirement: " + orPart, e); + } + itemIds = resolution.getItemIds(); + staffAlternatives.addAll(resolution.getStaffIds()); + offhandAlternatives.addAll(resolution.getOffhandIds()); + runeOnly &= resolution.isRune(); + } + for (Integer itemId : itemIds) { + parsedAlternatives.merge(itemId, quantity, Math::max); + } + maximumQuantity = Math.max(maximumQuantity, quantity); + } + Map alternatives = new LinkedHashMap<>(); + for (Integer itemId : parsedAlternatives.keySet()) { + alternatives.put(itemId, maximumQuantity); + } + requirements.add(new TransportItemRequirement( + alternatives, staffAlternatives, offhandAlternatives, runeOnly)); + } + return Collections.unmodifiableList(requirements); + } + + public Set getItemIds() { + return Collections.unmodifiableSet(new LinkedHashSet<>(alternatives.keySet())); + } + + public int getRequiredQuantity(int itemId) { + return alternatives.getOrDefault(itemId, -1); + } + + public boolean isSatisfiedBy(IntUnaryOperator availableQuantity) { + for (Map.Entry alternative : alternatives.entrySet()) { + int required = alternative.getValue(); + int available = Math.max(0, availableQuantity.applyAsInt(alternative.getKey())); + if ((required == 0 && available == 0) || (required > 0 && available >= required)) { + return true; + } + } + return false; + } + + boolean isSatisfiedBy(IntUnaryOperator availableQuantity, int staffItemId, int offhandItemId) { + return isSatisfiedBy(availableQuantity) + || staffAlternatives.contains(staffItemId) + || offhandAlternatives.contains(offhandItemId); + } + + /** + * Select at most one future weapon and one future offhand that make every AND-clause true. + * The same combination staff may satisfy multiple elemental rune clauses, matching the game. + */ + public static Optional selectProviders( + List requirements, + IntUnaryOperator availableQuantity, + IntPredicate staffAvailable, + IntPredicate offhandAvailable) { + if (requirements == null || requirements.isEmpty()) { + return Optional.of(ProviderSelection.NONE); + } + TreeSet staffs = new TreeSet<>(); + TreeSet offhands = new TreeSet<>(); + for (TransportItemRequirement requirement : requirements) { + requirement.staffAlternatives.stream().filter(staffAvailable::test).forEach(staffs::add); + requirement.offhandAlternatives.stream().filter(offhandAvailable::test).forEach(offhands::add); + } + List staffCandidates = new ArrayList<>(); + staffCandidates.add(ProviderSelection.NO_ITEM); + staffCandidates.addAll(staffs); + List offhandCandidates = new ArrayList<>(); + offhandCandidates.add(ProviderSelection.NO_ITEM); + offhandCandidates.addAll(offhands); + for (Integer staff : staffCandidates) { + for (Integer offhand : offhandCandidates) { + boolean satisfied = true; + for (TransportItemRequirement requirement : requirements) { + if (!requirement.isSatisfiedBy(availableQuantity, staff, offhand)) { + satisfied = false; + break; + } + } + if (satisfied) { + return Optional.of(new ProviderSelection(staff, offhand)); + } + } + } + return Optional.empty(); + } + + public Set getAllItemIds() { + LinkedHashSet itemIds = new LinkedHashSet<>(alternatives.keySet()); + itemIds.addAll(staffAlternatives); + itemIds.addAll(offhandAlternatives); + return Collections.unmodifiableSet(itemIds); + } + + public static final class ProviderSelection { + static final int NO_ITEM = -1; + static final ProviderSelection NONE = new ProviderSelection(NO_ITEM, NO_ITEM); + + private final int staffItemId; + private final int offhandItemId; + + private ProviderSelection(int staffItemId, int offhandItemId) { + this.staffItemId = staffItemId; + this.offhandItemId = offhandItemId; + } + + public int getStaffItemId() { return staffItemId; } + public int getOffhandItemId() { return offhandItemId; } + public boolean hasStaff() { return staffItemId > 0; } + public boolean hasOffhand() { return offhandItemId > 0; } + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof TransportItemRequirement)) { + return false; + } + TransportItemRequirement that = (TransportItemRequirement) other; + return alternatives.equals(that.alternatives) + && staffAlternatives.equals(that.staffAlternatives) + && offhandAlternatives.equals(that.offhandAlternatives) + && runeOnly == that.runeOnly; + } + + @Override + public int hashCode() { + int result = alternatives.hashCode(); + result = 31 * result + staffAlternatives.hashCode(); + result = 31 * result + offhandAlternatives.hashCode(); + return 31 * result + Boolean.hashCode(runeOnly); + } + + @Override + public String toString() { + return alternatives + " staff=" + staffAlternatives + " offhand=" + offhandAlternatives; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/TransportItemResolver.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/TransportItemResolver.java new file mode 100644 index 00000000000..25f07bc04b6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/TransportItemResolver.java @@ -0,0 +1,169 @@ +package net.runelite.client.plugins.microbot.shortestpath; + +import net.runelite.api.gameval.ItemID; +import net.runelite.client.plugins.microbot.util.magic.Rs2Staff; +import net.runelite.client.plugins.microbot.util.magic.Rs2Tome; +import net.runelite.client.plugins.microbot.util.magic.Runes; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * Pinned adapter for symbolic item collections used by Shortest Path transport resources. + * + *

Only collections whose semantics can be represented by {@link TransportItemRequirement} belong + * here. Rune symbols delegate to Microbot's canonical rune, staff and tome catalogs so pathfinding and + * actual casting cannot acquire separate provider lists. Unknown or unsupported symbols fail closed.

+ */ +final class TransportItemResolver { + private static final Map SYMBOLS = buildSymbols(); + + private TransportItemResolver() { + } + + static Resolution resolve(String symbol) { + if (symbol == null) { + return null; + } + return SYMBOLS.get(symbol.trim().toUpperCase(Locale.ROOT)); + } + + private static Map buildSymbols() { + Map symbols = new LinkedHashMap<>(); + addRune(symbols, "AIR_RUNE", Runes.AIR); + addRune(symbols, "ASTRAL_RUNE", Runes.ASTRAL); + add(symbols, "AXE", + ItemID.BRONZE_AXE, ItemID.IRON_AXE, ItemID.STEEL_AXE, ItemID.BLACK_AXE, + ItemID.MITHRIL_AXE, ItemID.ADAMANT_AXE, ItemID.RUNE_AXE, ItemID.DRAGON_AXE, + ItemID.CRYSTAL_AXE, ItemID.TRAIL_GILDED_AXE, ItemID.INFERNAL_AXE, ItemID._3A_AXE); + add(symbols, "BANANA", ItemID.BANANA); + addRune(symbols, "BLOOD_RUNE", Runes.BLOOD); + add(symbols, "BROWN_APRON", + ItemID.BROWN_APRON, ItemID.GOLDEN_APRON, ItemID.SKILLCAPE_CRAFTING, + ItemID.SKILLCAPE_CRAFTING_TRIMMED, ItemID.SKILLCAPE_CRAFTING_HOOD); + add(symbols, "CLIMBING_BOOTS", ItemID.DEATH_CLIMBINGBOOTS, ItemID.CLIMBING_BOOTS_G); + add(symbols, "COINS", ItemID.COINS); + add(symbols, "CROSSBOW", + ItemID.CROSSBOW, ItemID.PHOENIX_CROSSBOW, ItemID.DTTD_BONE_CROSSBOW, + ItemID.HUNTING_CROSSBOW, ItemID.XBOWS_CROSSBOW_BRONZE, ItemID.XBOWS_CROSSBOW_IRON, + ItemID.XBOWS_CROSSBOW_STEEL, ItemID.XBOWS_CROSSBOW_MITHRIL, + ItemID.XBOWS_CROSSBOW_ADAMANTITE, ItemID.XBOWS_CROSSBOW_RUNITE, + ItemID.XBOWS_CROSSBOW_DRAGON, ItemID.DRAGONHUNTER_XBOW, + ItemID.BARROWS_KARIL_WEAPON, ItemID.BARROWS_KARIL_WEAPON_BROKEN, + ItemID.BARROWS_KARIL_WEAPON_25, ItemID.BARROWS_KARIL_WEAPON_50, + ItemID.BARROWS_KARIL_WEAPON_75, ItemID.BARROWS_KARIL_WEAPON_100, + ItemID.ACB, ItemID.ZARYTE_XBOW); + add(symbols, "DUSTY_KEY", ItemID.DUSTY_KEY); + addRune(symbols, "DUST_RUNE", Runes.DUST); + addRune(symbols, "EARTH_RUNE", Runes.EARTH); + add(symbols, "ECTO_TOKEN", ItemID.ECTOTOKEN); + add(symbols, "GLOWING_FUNGUS", ItemID.GLOWING_FUNGUS); + addRune(symbols, "FIRE_RUNE", Runes.FIRE); + addRune(symbols, "LAVA_RUNE", Runes.LAVA); + addRune(symbols, "LAW_RUNE", Runes.LAW); + add(symbols, "MACHETE", + ItemID.MACHETTE, ItemID.MACHETTE_OPAL, ItemID.MACHETTE_JADE, ItemID.MACHETTE_REDTOPAZ); + add(symbols, "MAX_CAPE", + ItemID.SKILLCAPE_MAX, ItemID.SKILLCAPE_MAX_WORN, ItemID.SKILLCAPE_MAX_FIRECAPE, + ItemID.SKILLCAPE_MAX_FIRECAPE_DUMMY, ItemID.SKILLCAPE_MAX_FIRECAPE_TROUVER, + ItemID.SKILLCAPE_MAX_SARADOMIN, ItemID.SKILLCAPE_MAX_ZAMORAK, + ItemID.SKILLCAPE_MAX_GUTHIX, ItemID.SKILLCAPE_MAX_ANMA, ItemID.SKILLCAPE_MAX_ARDY, + ItemID.SKILLCAPE_MAX_INFERNALCAPE, ItemID.SKILLCAPE_MAX_INFERNALCAPE_DUMMY, + ItemID.SKILLCAPE_MAX_INFERNALCAPE_TROUVER, ItemID.SKILLCAPE_MAX_SARADOMIN2, + ItemID.SKILLCAPE_MAX_SARADOMIN2_TROUVER, ItemID.SKILLCAPE_MAX_ZAMORAK2, + ItemID.SKILLCAPE_MAX_ZAMORAK2_TROUVER, ItemID.SKILLCAPE_MAX_GUTHIX2, + ItemID.SKILLCAPE_MAX_GUTHIX2_TROUVER, ItemID.SKILLCAPE_MAX_ASSEMBLER, + ItemID.SKILLCAPE_MAX_ASSEMBLER_TROUVER, ItemID.SKILLCAPE_MAX_MYTHICAL, + ItemID.SKILLCAPE_MAX_ASSEMBLER_MASORI, ItemID.SKILLCAPE_MAX_ASSEMBLER_MASORI_TROUVER, + ItemID.SKILLCAPE_MAX_DIZANAS, ItemID.SKILLCAPE_MAX_DIZANAS_TROUVER); + add(symbols, "MAX_HOOD", + ItemID.SKILLCAPE_MAX_HOOD, ItemID.SKILLCAPE_MAX_HOOD_FIRECAPE, + ItemID.SKILLCAPE_MAX_HOOD_SARADOMIN, ItemID.SKILLCAPE_MAX_HOOD_ZAMORAK, + ItemID.SKILLCAPE_MAX_HOOD_GUTHIX, ItemID.SKILLCAPE_MAX_HOOD_ANMA, + ItemID.SKILLCAPE_MAX_HOOD_ARDY, ItemID.SKILLCAPE_MAX_HOOD_INFERNALCAPE, + ItemID.SKILLCAPE_MAX_HOOD_SARADOMIN2, ItemID.SKILLCAPE_MAX_HOOD_ZAMORAK2, + ItemID.SKILLCAPE_MAX_HOOD_GUTHIX2, ItemID.SKILLCAPE_MAX_HOOD_ASSEMBLER, + ItemID.SKILLCAPE_MAX_HOOD_MYTHICAL, ItemID.SKILLCAPE_MAX_HOOD_ASSEMBLER_MASORI, + ItemID.SKILLCAPE_MAX_HOOD_DIZANAS); + add(symbols, "MAZE_KEY", ItemID.MELZARKEY); + addRune(symbols, "MIND_RUNE", Runes.MIND); + addRune(symbols, "MIST_RUNE", Runes.MIST); + add(symbols, "MITH_GRAPPLE", ItemID.XBOWS_GRAPPLE_TIP_BOLT_MITHRIL_ROPE); + addRune(symbols, "MUD_RUNE", Runes.MUD); + addRune(symbols, "NATURE_RUNE", Runes.NATURE); + add(symbols, "PICKAXE", + ItemID.BRONZE_PICKAXE, ItemID.IRON_PICKAXE, ItemID.STEEL_PICKAXE, + ItemID.BLACK_PICKAXE, ItemID.MITHRIL_PICKAXE, ItemID.ADAMANT_PICKAXE, + ItemID.RUNE_PICKAXE, ItemID.DRAGON_PICKAXE, ItemID.CRYSTAL_PICKAXE, + ItemID.TRAIL_GILDED_PICKAXE, ItemID._3A_PICKAXE, ItemID.DRAGON_PICKAXE_PRETTY, + ItemID.ZALCANO_PICKAXE, ItemID.TRAILBLAZER_PICKAXE_NO_INFERNAL, + ItemID.TRAILBLAZER_RELOADED_PICKAXE_NO_INFERNAL, ItemID.INFERNAL_PICKAXE); + add(symbols, "ROPE", ItemID.ROPE); + add(symbols, "SHANTAY_PASS", ItemID.SHANTAY_PASS); + add(symbols, "SKAVID_MAP", ItemID.SKAVIDMAP); + addRune(symbols, "SMOKE_RUNE", Runes.SMOKE); + addRune(symbols, "SOUL_RUNE", Runes.SOUL); + addRune(symbols, "STEAM_RUNE", Runes.STEAM); + addRune(symbols, "WATER_RUNE", Runes.WATER); + return Collections.unmodifiableMap(symbols); + } + + private static void add(Map symbols, String name, Integer... itemIds) { + put(symbols, name, new Resolution(ids(itemIds), Collections.emptySet(), + Collections.emptySet(), false)); + } + + private static void addRune(Map symbols, String name, Runes rune) { + LinkedHashSet itemIds = new LinkedHashSet<>(); + itemIds.add(rune.getItemId()); + Arrays.stream(Runes.getComboRunes(rune)) + .map(Runes::getItemId) + .forEach(itemIds::add); + put(symbols, name, new Resolution( + itemIds, + Rs2Staff.itemIdsProviding(rune), + Rs2Tome.itemIdsProviding(rune), + true)); + } + + private static Set ids(Integer... itemIds) { + return Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(itemIds))); + } + + private static void put(Map symbols, String name, Resolution resolution) { + if (resolution.itemIds.isEmpty() + || resolution.itemIds.stream().anyMatch(id -> id == null || id <= 0) + || resolution.staffIds.stream().anyMatch(id -> id == null || id <= 0) + || resolution.offhandIds.stream().anyMatch(id -> id == null || id <= 0)) { + throw new IllegalArgumentException("invalid item collection: " + name); + } + if (symbols.put(name, resolution) != null) { + throw new IllegalArgumentException("duplicate item collection: " + name); + } + } + + static final class Resolution { + private final Set itemIds; + private final Set staffIds; + private final Set offhandIds; + private final boolean rune; + + private Resolution(Set itemIds, Set staffIds, + Set offhandIds, boolean rune) { + this.itemIds = Set.copyOf(itemIds); + this.staffIds = Set.copyOf(staffIds); + this.offhandIds = Set.copyOf(offhandIds); + this.rune = rune; + } + + Set getItemIds() { return itemIds; } + Set getStaffIds() { return staffIds; } + Set getOffhandIds() { return offhandIds; } + boolean isRune() { return rune; } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/UPSTREAM_COMPARISON.md b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/UPSTREAM_COMPARISON.md index 6201c47efb1..fbb89a97a3b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/UPSTREAM_COMPARISON.md +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/UPSTREAM_COMPARISON.md @@ -3,6 +3,242 @@ Comparison of [Skretzo/shortest-path](https://github.com/Skretzo/shortest-path) (upstream) against the Microbot fork. Original baseline: `07fca57` ("Data fixes and minor cleanups (#400)"). **Everything below the "Re-baseline" section refers to that old baseline and is partly superseded — read the re-baseline first.** +The maintained plan and ownership boundary are in `docs/walker-roadmap.md`. The machine-readable +baseline is `scripts/shortest-path-upstream-baseline.json` and can be checked with +`scripts/check-shortest-path-upstream.py`. + +--- + +## Re-baseline 2026-08-05 → upstream `ff8e961b32` + +Upstream moved by two collision-map commits since the 2026-07-20 review. The exact reviewed commit and +resource blobs are now recorded outside this narrative document so drift is machine-detectable. + +- Imported upstream `collision-map.zip` (`sha256:3a99d42fec10e12dbda96bbaae45b354d8e2270c4c1a453d033e95b7da2670d2`). + The map grew from 2,724 to 2,726 regions. Before import, the candidate passed `ShortestPathCoreTest`, + `WalkerRouteCorpusTest` and `PathfinderBenchmarkTest` in an isolated worktree. +- Closed the home-teleport coverage gap with Edgeville, Lunar and Arceuus destinations. Microbot uses one + semantic row per spellbook and intentionally rejects upstream's animation-duration variants because a + display/animation setting must not gate planner availability. +- Added `scripts/compare-shortest-path-transports.py` plus an exact reviewed semantic-debt baseline. The + comparator matches named network endpoints when boarding/landing coordinates differ, compares fares and + requirement dimensions, and makes identity swaps visible through content digests. It is enforced for + affected pull requests and by the weekly upstream workflow. +- Brought `minecarts.tsv` to exact compared parity. The old local rows charged 20 coins after The Forsaken + Tower; paid variants now require `7796<11` and free variants require `7796=11`. +- Closed all minigame-teleport identity gaps (Guardians of the Rift and the Varrock/Keldagrim Rat Pits + landings) and added Total-level, Combat-level and Quest-points gating to the local transport model. The + Pest Control minigame teleport now actually enforces its upstream 40 Combat requirement. +- Added a structured item-requirement compatibility layer. Numeric upstream expressions retain AND, + OR and quantity semantics, including upstream's maximum quantity within an OR group; legacy + `Item IDs` rows remain one OR group. Pathfinder, + transport-refresh caching, bank planning and Slayer transport preparation now consume that model. +- Imported the direct Max-cape and Quest-point-cape family from the pinned teleport-item artifact: + 16 route identities converged, four previously missing Max-cape destinations were restored, and the + duplicate Black chinchompa row was removed. Multi-level labels now resolve their leaf item sub-action; + POH-home variants remain programmatic Microbot behavior. +- Added a pinned symbolic collection adapter for walking tools, grapple gear, keys, passes, currencies + and cape/apron families. Unknown symbols fail closed. Rune symbols now delegate to Microbot's canonical + rune, staff and tome catalogs, preserve equipment-provider semantics through the immutable route edge, + and produce one atomic bank withdrawal/equipment loadout. All 43 shipped non-home spell rows carry the + reviewed upstream item requirements while retaining the existing Microbot landing coordinates; disputed + coordinate changes remain deferred for route/live evidence. All 45 River Lum and River Dougne canoe + routes now have exact compared field parity; the executor chooses the chain-specific map interface and + route-corpus coverage pins the new western network. Live River Dougne execution remains pending. +- Brought all 28 Quetzal network identities to exact compared parity and added a dedicated semantic + mapping from upstream `quetzal_whistle.tsv` to Microbot's inline teleport-item rows. All 14 whistle + destinations retain the current Quetzacalli Gorge landing, Cam Torum map label, canonical unlock + bitmasks and Twilight's Promise gate. Microbot intentionally records 14 item/consumability differences: + charged whistles remain consumable while perfected-infinite item `33120` remains available to the + Inventory (perm) policy, rather than inheriting upstream's family-level `Consumable=T` flag. +- Corrected the executor's wilderness boundary check to the same inclusive maximum used by the planner. + The old executor-only `+1` admitted a teleport one Wilderness level beyond the planned limit. +- The comparator now ignores a comparable field only when its column is absent from one entire schema. + This reduced false field drift while preserving exact identity/content digests for real changes. +- Closed the lossless agility-shortcut requirement slice: all twelve reviewed grapple edges now require + both a crossbow family and a mith grapple, and the Trollheim rope edge carries its rope plus unlock + varbit in executable fields. Corrected current landings and durations for the Lumbridge-farm fence and + northern Varlamore rocks; real pathfinder corpus cases select both edges. The comparator now uses + RuneLite's explicit course-obstacle catalog to classify 114 known course identities without removing + them from total debt. Three Trollheim climbing-rock ascents moved from generic transports to exact + boots-gated agility edges while their descents remain generic; a pathfinder case proves the ascent. + The complete 88-edge Isafdar forest family now matches upstream landings, levels and durations: 66 + unconditional generic rows became Agility shortcuts, 22 missing edges were added and four stale local + landing variants were removed. The route corpus proves the three-edge dense-forest chain. The other 28 + exact cross-file identities that were still unrestricted generic transports now retain upstream's + Agility requirements across Brimhaven Dungeon (6), the Lumbridge cellar (2), Karamja rocks (6), Slayer + Tower (8) and Darkmeyer (6). Catalog tests pin their levels, durations and unlock varbits, and one real + pathfinder route per family proves graph selection. The semantic comparator now exposes any upstream + agility identity represented only as a local generic transport and pins that bypass class at zero. The + two diagonal Darkmeyer approaches that upstream intentionally models both ways remain exactly once as + generic transports alongside their gated shortcut variants. The current semantic inventory is 6,601 + shared, 1,016 upstream-only and 952 Microbot-only route identities, with 1,379 comparable field drifts; + agility debt is 231 identities, split into 114 course obstacles and 117 ordinary-world or unresolved + routes. Live interaction evidence for this slice remains pending. +- Added route-corpus coverage for Draynor's east sewer transition and for planner selection of `SHIP`, + `NPC` and `BOAT` travel families while retaining Microbot's explicit ship-deck/gangplank model. +- Added twelve intentional Microbot-only Barrows edges absent from the reviewed upstream artifact: six + exact spade-gated mound digs into the individual crypts and six object-backed crypt exits to + representative anchors on their matching surface mounds. A dedicated registry capability prevents + arbitrary object-less `Dig` rows from becoming executable. Static route coverage pins all six pairs and + rejects every sarcophagus object as a deterministic tunnel edge; the empty crypt is randomized and must + be observed by a future state-aware executor. Live mound round-trip evidence remains pending. +- Completed the static Laguna Aurorae spirit-tree perimeter from the pinned artifact. Nine object-`26262` + origins now feed the existing `Travel` executor and the already-present Pandemonium-gated destination; + route coverage proves the north-west origin selects the network. Spirit-tree shared identities rise from + 145 to 154 and upstream-only debt falls from 11 to the two POH directions already owned by Microbot's + programmatic POH integration. Tests reject adding those POH routes to the static TSV. A live Laguna + round trip remains required. +- Closed the executable part of ordinary `transports.tsv` debt. The two Elemental Workshop wall directions + now use current object `26115`, require the concrete battered key and sit behind a curated collision-edge + override so the planner cannot walk through the closed wall. The upstream steel-key-ring alternative is + intentionally stricter locally: ring possession does not prove that the battered key is stored. The + remaining 15 upstream-only identities are fully classified and digest-pinned (four superseded Piscatoris + anchors, eight id-less Marim stairs, one interaction-less Daero jump and two unsafe Varrock trellis rows), + leaving zero unexplained ordinary route identities. A route regression proves battered-key selection and + key-ring-only rejection; live wall execution remains pending. +- Imported the four genuinely missing Pandemonium ship directions between Port Sarim, Musa Point and the + island using the reviewed Captain Tobias, Customs officer and Seaman Morris ids/actions. All four retain + the quest gate and 30-coin fare, resolve to direct terminal travel, and have real-pathfinder selection plus + fail-closed prerequisite coverage. The remaining six upstream-only ship identities are exact-classified + representations of Microbot's current Corsair Cove, Ardougne and Void Outpost deck/landing coordinates; + a live Pandemonium round trip remains pending. +- Added a pinned dual-engine evaluation harness. A declared adapter patch retains upstream's exact selected + transport object through `NodeGraph`/`PathStep`, avoiding the ambiguous endpoint rematch documented by + upstream itself. Static, real White Wolf surface-tunnel-surface, same-edge network alternatives and + bank-disabled/start-at-bank policies agree on reachability, termination, exact selected corpus IDs and + route cost. A separate-bank detour and four source-aware spell slices (carried/banked raw runes, + separate staff/tome, and a missing ordinary item) also agree. The corpus explicitly pins one reviewed, game-semantic + divergence: upstream rejects one Twinflame staff as the provider for both fire and water clauses because + its requirement evaluator consumes the staff substitution after the first clause; Microbot reuses the + same selected combination staff across every compatible clause. The 16-case gate requires 15 exact + parity results plus this one documented expected divergence, and fails if either an unexpected difference + appears or the reviewed difference disappears. The local adapter performs several workflow searches while + upstream carries bank state through one graph, so node/time metrics for that case are diagnostic rather + than core-performance parity. The gate runs the local core, the production-packaged pinned upstream adapter + and an independently compiled temporary checkout. It requires the two upstream executions to agree on all + semantic result fields unless the corpus already declares an input-policy divergence. +- Converted the local side of that harness and synchronous production planning to one `Rs2RoutePlanner` + boundary. `Rs2PathApi` now resolves an immutable policy snapshot before engine dispatch, including bank, + Wilderness, dangerous-NPC, teleport, membership, live-collision, cutoff, enabled-family and restriction + state; unresolved requests fail instead of consulting mutable globals. Exact local transport identity is + retained only as an opaque package-private payload on the immutable edge. Microbot executor admission and + zero-rune home-teleport capability are injected at plugin composition through `TransportPlanningPolicy`, + and the pathfinder core is CI-guarded against importing the executor registry. ADR 0006 established the + production-capable upstream shadow adapter as the next milestone before further broad family imports. +- Packaged the reviewed non-UI upstream core in an isolated source set and added a default-off production + adapter. Both engines consume the same resolved request and immutable planning snapshot; upstream maps a + selected transport back to the exact already-admitted Microbot edge by object identity. Shadow execution is + bounded to one worker and one queued request, never publishes an execution route, rejects stale active-route + generations and covers synchronous queries, ordinary active walker routes and cave-route selection. The + facade exposes the latest structured comparison plus aggregate match, divergence, failure, stale, discard + and exact-route-shape-difference counters. Completed outcomes distinguish ordinary replans from recovery, + classify explicit bank-workflow legs, retain selected transport executor/type families and count live + collision only when the overlay answers a search edge. The schema-versioned Agent Server endpoint exposes + this coordinate-free evidence through `microbot-cli walker shadow`, and + `evaluate-walker-shadow-evidence.py` enforces recovery, bank-workflow, collision and transport-family + diversity plus terminal blocking-walk/recovered-arrival outcomes rather than treating enabled settings or + a matching replan as behavioral evidence. + Twelve accepted live-shadow sessions now provide 141/141 semantic matches and 71/71 walker arrivals. The + aggregate closes every F2P live minimum, including 75 active routes, 15 active replans, 11 recovery replans, + 39 underground comparisons, 18 walking-only cave selections and ten explicit item-gated bank-to-target + comparisons. There is no semantic divergence, planner failure, pending/discarded work, unreachable result or + exit. Seventy-six exact-shape differences are equal-cost alternatives with matching selected transports; + retained diagnostics classify them across transport-free replan/recovery, bank/canoe and mixed surface + slices, with none in the underground comparisons and none associated with a non-arrival. Five clean + samples from an ephemeral detached revision containing the exact current tree also pass the + timing gate at a `0.392` upstream/local comparable-suite median ratio. Preserve or repeat that evidence on + the final reachable review revision. The explicit F2P selector and rollback test are now complete; + members-policy selection requires separate representative members-world evidence. + `check-shortest-path-vendored-core.py` pins all source/metadata digests and can prove every undeclared file + byte-identical to the reviewed checkout. +- Added an explicit `LOCAL`, `SHADOW` and `UPSTREAM_F2P_CANARY` selection state. `LOCAL` remains the default; + `SHADOW` cannot select a route; and the canary is eligible only for resolved non-members policy. The canary + keeps active publication in the calculating phase until both candidates finish, selects upstream only for + a semantic match, and otherwise retains local with separate divergence/failure fallback counters. This is + conservative containment rather than treating local as a correctness oracle. Exact upstream selections + are temporarily materialized into the legacy completed-pathfinder view for existing runtime consumers. + Remove that shell with the local planner after the two-release/1,000-comparison fallback sunset. A live + F2P-17 underground run made ten upstream selections and ten arrivals without divergence or failure. A + separate test-only forced-failure run made zero upstream selections, ten local failure fallbacks and ten + arrivals. This validates the opt-in selector; the default remains local until the performance result is + tied to the final reachable review revision and the final evidence bundle is approved. +- Migrated active destination bank-item discovery to exact immutable `Rs2TransportEdge` values. Fare, + rune, fairy-ring, purchasable-item and structured AND/OR requirement selection no longer require a + concrete selected `Transport`; the transitional `LegacyRoutePlan` handoff is removed and CI prevents it + returning. The deprecated concrete helper remains only as a Hub compatibility API, outside the active + banking and executor contracts. +- Bound active runtime transport discovery to the exact selected route edge. Completed pathfinders publish + an immutable, source-identity-checked route snapshot; raw-segment dispatch, ranged classification and + nearby current-tile recovery no longer rescan all catalog rows at an origin. Immutable edges carry an + explicit Microbot executor capability, while the exact local concrete object is retained only as an opaque + package-private payload for behavior-bearing handlers such as POH. Catalog rows without a registered + executor fail closed before planning. All four home teleports use one exact-name, zero-rune widget + executor shared by planner capability and runtime dispatch. The 225 directed hot-air-balloon edges now + use a dedicated exact-destination map executor and observed-landing contract. Static and dual-engine + selection coverage is green; live evidence proves all edges remain unavailable when station unlocks are + absent, while a successful flight still needs an unlocked-account run. +- Closed the live Port Sarim/Musa Point terminal-ship incident without changing upstream planning data. + Current NPC menus expose `Travel` while the reviewed catalog retains destination labels, and current + travel lands directly on the ground after auto-completing the catalogued deck/gangplank pair. The executor + now preserves the configured action first, applies a conservative `SHIP`-only `Travel` fallback, limits an + exact selected edge to one interaction per top-level walk, and accepts only the immediate planned landing + continuation. Live walks in both directions produced one click, an observed handoff and no timeout. +- Tightened the Microbot-owned Al Kharid toll executor after live investigation exposed both a false landing + and stale object-id collision. The raw door scanner now defers the catalog edge to one selected-transport + owner; that owner resolves the transformed live Gate by configured action and exact edge geometry, with no + historical-id fallback. Completion requires the exact opposite-side destination and an unresolved + interaction bubbles back without a handoff. Rebuilt live walks in both directions selected the Gate, + issued one toll interaction, reached the exact selected landing and emitted the expected handoff without + raw-obstacle interception or timeout. +- Removed the local `Open;Manhole;881` Varrock Sewers row after a live walk proved that it modeled cover + preparation as though it were a surface-to-underground transition. The reviewed upstream catalog contains + only `Climb-down;Manhole;882`; Microbot's closed-object handling can still open `881`, refind `882` and + execute that exact edge. A static catalog regression pins the distinction, and five consecutive F2P live + walks arrived on the exact sewer tile without a trapdoor timeout or route stall. +- Replaced the terminal-travel type assumption with a row-level execution contract. `SHIP`, `NPC` and + `BOAT` describe journeys whose configured target may be an NPC or scene object; immutable route edges now + carry a direct or dialogue-destination mode in addition to `TERMINAL_TRAVEL`. Semantic live matching + admits the direct Al Kharid/Tempoross `Board;Ferry` edge without trusting its historical object id. + Forty-one multi-step terminal rows remain deliberately fail-closed and are pinned by interaction group + until their destination-selection flows are implemented. The Ferry is statically selected by the route + corpus, while successful members-world outbound/reverse execution remains pending; the rebuilt free-world + run correctly admitted zero boat edges and therefore did not produce false runtime evidence. +- The July architectural conclusion still holds: use upstream as a tracked planner/data reference and + retain Microbot ownership of runtime execution and automation policy. +- Advanced the production planner boundary without selecting a replacement engine: synchronous walker + queries and active route restart/cancellation now enter through immutable `Rs2RouteRequest` policy and + `Rs2PathApi`. Configuration refresh, cave walking-only selection, executor ownership and local + `Pathfinder` construction are confined to that seam, with CI rejecting reintroduction in the walker and + lifecycle packages. NPC target selection and bank-route comparison also use request-scoped policy; bank + diagnostics retain the exact typed edges chosen by search rather than rematching the mutable catalog. + The next migration slice is also complete: a generation-tagged immutable active-route status now serves + walker progress/recovery, Quest Helper and obstacle consumers, while CI rejects concrete active planner + reads outside the facade. Slayer bank-item preparation is request-scoped and exposes an exact immutable + edge replacement for its deprecated concrete-transport helper. Explicit walker policy/config operations + are now named facade calls, and CI rejects mutable configuration in the walker. Leagues cache invalidation + also enters through the facade, while its catalog injection receives a narrow transport-usability + predicate instead of `PathfinderConfig`; no production consumer outside the facade or shortest-path + implementation imports the mutable config. Concrete transport payloads and overlay ownership are the next + boundary decision, not another blanket type migration. The first classified payload slice is now complete: + hot-air-balloon execution consumes the immutable selected edge, recovery and obstacle code ask only for + transport-origin presence, door catalog classification uses immutable edge views, and bank-route distance + scoring follows the exact ordered route steps rather than rematching a same-endpoint catalog entry. + `TransportRouteAnalysis` now retains every compared leg's exact steps, and withdrawal planning consumes + the selected bank-to-target edges instead of running a second search from the pre-bank location. CI + prohibits concrete transport imports from migrated packages and rejects that compare-then-replan pattern. + +Next work is to repeat or adopt the performance evidence under the final reachable review revision, approve +the F2P-scoped release decision, and collect representative members-only evidence before any members-policy +selection. Broad family-by-family transport convergence remains paused except for incident-driven fixes. +Runtime interaction changes still require live harness evidence. Do not use the old priority list at the +bottom of this historical document as the active queue. + +The opt-in F2P harness exports the endpoint's coordinate-free schema-v2 snapshot and rejects empty, unsettled, +divergent or failed ordinary shadow runs. The full accepted aggregate now covers the required surface, +recovery, bank, teleport, network and terminal-travel mix. The fixed Varrock manhole route also serves as the +selection/rollback release case described above. + --- ## Re-baseline 2026-07-20 → upstream `7e7e5bf94b` @@ -44,14 +280,13 @@ Microbot loads 22 TSVs (see `Transport.java`). File-level diff vs `skretzo/maste - Of the 778: only **14 named** transports; **764 anonymous route objects** (372 Climb, 137 Ladder, 123 Stairs, 43 Staircase, gates/doors/caves…). - **Not drift:** 737 distinct origins in the 778, of which only 24 overlap a Microbot origin — **713 are genuinely new origin tiles**. Concentrated central 2500–2999 (399), Varlamore/Kebos 1500–1999 (133), Misthalin 3000–3499 (131). - **Verdict:** real coverage gap (new route objects + new areas Microbot's baseline predates). -- **✅ DONE (2026-07-20):** imported **768** of the 778 (`transports.tsv` 4949→5717), scripted + validated. Conversion: action `space`→`;` form; inserted `Currency`/`isMembers` columns; upstream named item variations + `|` OR-sets → Microbot numeric id-sets (`AXE`/`MACHETE`/`PICKAXE`/`ROPE` via `ItemVariations`→`ItemID`; `COINS=N`→Currency); fixed upstream typo `Shadows`→`Shadow of the Storm` (else the `Quest` gate silently drops). **Excluded:** 15 id-less rows (bare `Climb-up Staircase`, name-only walls/gates) unmatchable in Microbot's id-based format, and **2 Garden of Tranquillity trellis rows (obj 2149)** Microbot intentionally omits — caught by `testVarrockSewerPathAvoidsDisabledPalaceTrellisShortcut`. Paired with the **updated collision map** (`collision-map.zip` 2663→2724 regions) so new-area routes have collision coverage. Validated: shortestpath suite green (73 tests, incl. real cross-region pathfinding). +- **✅ DONE (2026-07-20):** imported **768** of the 778 (`transports.tsv` 4949→5717), scripted + validated. Conversion: action `space`→`;` form; inserted `Currency`/`isMembers` columns; upstream named item variations + `|` OR-sets → Microbot numeric id-sets (`AXE`/`MACHETE`/`PICKAXE`/`ROPE` via `ItemVariations`→`ItemID`; `COINS=N`→Currency); fixed upstream typo `Shadows`→`Shadow of the Storm` (else the `Quest` gate silently drops). The original import excluded 15 id-less rows and **2 Garden of Tranquillity trellis rows (obj 2149)**. On 2026-08-05 the two Elemental Workshop wall directions were recovered with current object `26115`; the remaining 15 identities are now explicitly classified rather than unexplained. The trellis remains intentionally omitted and is caught by `testVarrockSewerPathAvoidsDisabledPalaceTrellisShortcut`. Paired with the **updated collision map** (`collision-map.zip` 2663→2724 regions) so new-area routes have collision coverage. Validated: shortestpath suite green (73 tests, incl. real cross-region pathfinding). - **Caveat:** imported members-area routes carry an empty `isMembers` (upstream lacked that column) — same as upstream's own behaviour; harmless since F2P can't reach those origins anyway. ### Recommended next Stage-4 target -**Home-teleport coverage** is the next confirmed upstream data gap: compare upstream -`teleportation_spells_home.tsv` (16 variants) with Microbot's two home-teleport rows and backfill only -the missing, valid variants. #2 and #3 are complete, #11 is a stale premise, and #10/#12/#30 have no -confirmed upstream implementation to backport. +✅ Completed 2026-08-04. Edgeville, Lunar and Arceuus were added with spellbook, quest, membership and +cooldown requirements. Animation-setting duplicates were intentionally not imported. See the newer +re-baseline above for the next work. --- diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/WEBWALKER_IMPROVEMENT_PLAN.md b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/WEBWALKER_IMPROVEMENT_PLAN.md index 5806f82f2d7..32a939cffb4 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/WEBWALKER_IMPROVEMENT_PLAN.md +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/WEBWALKER_IMPROVEMENT_PLAN.md @@ -60,7 +60,7 @@ The "completeness of navigating the world" half. | 19 | DONE | After a door interact, if the player didn't move and `isQuestLockedDoorDialogue()` matches ("quest" / "you need to" / "you must" / "cannot enter" / "requires you" / …), log `warn` with door details + dialogue text, add the tile to `sessionBlacklistedDoors`, close the dialogue, refresh `PathfinderConfig` (re-read quest/varbit state) and `recalculatePath()`. Entry of `handleDoors` short-circuits on blacklisted tiles to break the retry loop. | `Rs2Walker.java:1256–1275, 1376–1396` | M | | 20 | DONE | POH `convertInstancedWorldPoint()` null-path diagnostics added: `handleDoors` null log now includes rawFrom/rawTo/fromWp/toWp and `idx/pathSize`; `setTarget` POH instance start now null-checks `WorldPoint.fromLocalInstance` (falls back to raw world location with a `warn` when it returns null) | `Rs2Walker.java:1206–1213, 1473–1486` | M | | 21 | DONE | Minimap click now scans forward from first past-threshold tile to the furthest same-plane, non-transport-origin tile within ~14-tile Chebyshev reach, then advances the loop index past the intermediate tiles. Cuts tick count on long diagonal runs by ~30-40% since Chebyshev reach is 1.4× the cardinal step count. | `Rs2Walker.java:476–531` | M | -| 22 | DONE | `Telemetry.recordUnreachable(cause, player, target, pathEndpoint, pathSize, threshold, pathfinder)` logs at `warn` with pathfinder stats; wired into both UNREACHABLE exits (no-walkable-path and partial-retries-exhausted) with `unreachableCount` counter exposed to probes | `Rs2Walker.java:108, 128–141, 158, 306–308, 532–533` | S | +| 22 | DONE | `Telemetry.recordUnreachable(cause, player, target, pathEndpoint, pathSize, threshold, routeMetrics)` logs at `warn` with planner-independent route metrics; wired into both UNREACHABLE exits (no-walkable-path and partial-retries-exhausted) with `unreachableCount` counter exposed to probes | `Rs2Walker.java` | S | --- @@ -128,6 +128,14 @@ Items already catalogued in `UPSTREAM_COMPARISON.md` are surfaced here only wher ## Facade migration (2026-07-20) +> **2026-08-05 boundary review:** direct `ShortestPathPlugin` state access has been migrated back behind +> `Rs2PathApi` and is now CI-enforced by `scripts/check-shortest-path-boundary.py`. The class remains a +> compatibility seam rather than the final stable API because it still exposes concrete `Pathfinder`, +> mutable `PathfinderConfig` and `Transport` values. The canonical next steps are in +> `docs/walker-roadmap.md` “Tighten the planner boundary.” The first operation-level slice now provides +> immutable route requests/results and has migrated bank, deposit-box and banked-destination searches off +> direct `Pathfinder` construction. + **Goal:** decouple automation from the shortest-path *internals* so future upstream backports stop rippling into `Rs2Walker` and the other consumers. The fork stays a fork; this is a boundary, not a rewrite. Once the boundary exists, backporting an upstream fix means changing code behind the facade only. ### Why first 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 edb7b7cdaea..e40d9123ebe 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 @@ -13,6 +13,7 @@ import net.runelite.client.plugins.microbot.util.player.Rs2Player; import java.util.*; +import java.util.function.IntSupplier; @Slf4j public class CollisionMap { @@ -29,23 +30,38 @@ public class CollisionMap { */ private final LiveCollisionOverlay overlay; + /** + * Supplies the live player region for instance-only obstacle policy. Static/offline maps use a + * sentinel supplier so pathfinding tests never reach into the RuneLite client thread. + */ + private final IntSupplier currentRegionIdSupplier; + /** * Live view pinned for the duration of one search, so a mid-search merge on the client thread cannot * mix two states into a single path. Refreshed via {@link #beginSearch()}. */ private LiveEdgeSource pinnedLive; + /** Number of edge reads answered by the pinned live overlay during the current search. */ + private long liveEdgeQueries; + public byte[] getPlanes() { return collisionData.getRegionMapPlaneCounts(); } public CollisionMap(SplitFlagMap collisionData) { - this(collisionData, new LiveCollisionOverlay()); + this(collisionData, new LiveCollisionOverlay(), () -> -1); } public CollisionMap(SplitFlagMap collisionData, LiveCollisionOverlay overlay) { + this(collisionData, overlay, CollisionMap::readLivePlayerRegionId); + } + + CollisionMap(SplitFlagMap collisionData, LiveCollisionOverlay overlay, + IntSupplier currentRegionIdSupplier) { this.collisionData = collisionData; this.overlay = overlay; + this.currentRegionIdSupplier = currentRegionIdSupplier; } /** @@ -55,6 +71,7 @@ public CollisionMap(SplitFlagMap collisionData, LiveCollisionOverlay overlay) { */ public void beginSearch() { pinnedLive = overlay.current(); + liveEdgeQueries = 0L; } private boolean get(int x, int y, int z, int flag) { @@ -62,12 +79,17 @@ private boolean get(int x, int y, int z, int flag) { if (live != null) { final Boolean liveEdge = live.edge(x, y, z, flag); if (liveEdge != null) { + liveEdgeQueries++; return liveEdge; } } return collisionData.get(x, y, z, flag); } + public long getLiveEdgeQueries() { + return liveEdgeQueries; + } + public boolean n(int x, int y, int z) { return get(x, y, z, 0); } @@ -219,8 +241,7 @@ private int getCachedRegionId() { long now = System.currentTimeMillis(); if (now - cachedRegionIdTime > REGION_CACHE_MS) { try { - WorldPoint loc = Rs2Player.getWorldLocation(); - cachedRegionId = loc != null ? loc.getRegionID() : -1; + cachedRegionId = currentRegionIdSupplier.getAsInt(); } catch (Exception e) { cachedRegionId = -1; } @@ -229,6 +250,11 @@ private int getCachedRegionId() { return cachedRegionId; } + private static int readLivePlayerRegionId() { + WorldPoint loc = Rs2Player.getWorldLocation(); + return loc != null ? loc.getRegionID() : -1; + } + public List getNeighbors(Node node, VisitedTiles visited, PathfinderConfig config, Set targets) { final int x = WorldPointUtil.unpackWorldX(node.packedPosition); final int y = WorldPointUtil.unpackWorldY(node.packedPosition); @@ -264,14 +290,15 @@ public List getNeighbors(Node node, VisitedTiles visited, PathfinderConfig continue; } int cost = config.getDistanceBeforeUsingTeleport() + transport.getDuration(); - neighbors.add(new TransportNode(transport.getDestination(), node, cost)); + neighbors.add(new TransportNode(transport.getDestination(), node, cost, transport)); if (isMoa) { moaAddedHere++; if (moaCosts == null) moaCosts = new ArrayList<>(); moaCosts.add(cost); } } else { - neighbors.add(new TransportNode(transport.getDestination(), node, transport.getDuration())); + neighbors.add(new TransportNode( + transport.getDestination(), node, transport.getDuration(), transport)); } //END microbot variables } @@ -371,9 +398,10 @@ public List getReverseNeighbors(Node node, VisitedTiles visitedBackward, P if (config.isIgnoreTeleportAndItems()) { continue; } - neighbors.add(new TransportNode(origin, node, config.getDistanceBeforeUsingTeleport() + transport.getDuration())); + neighbors.add(new TransportNode(origin, node, + config.getDistanceBeforeUsingTeleport() + transport.getDuration(), transport)); } else { - neighbors.add(new TransportNode(origin, node, transport.getDuration())); + neighbors.add(new TransportNode(origin, node, transport.getDuration(), transport)); } } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Node.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Node.java index b5235d3b9d3..258fcb6f214 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Node.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Node.java @@ -12,18 +12,12 @@ public class Node { public final int packedPosition; public final Node previous; public final int cost; - public int heuristic; // Per-node random value used as a secondary priority-queue comparator. Breaks ties - // between equal-fCost nodes in random order so the pathfinder explores equivalent + // between equal-cost nodes in random order so the pathfinder explores equivalent // routes in a different sequence each run, producing distinct (but still optimal) - // tile sequences between the same start/target pair. Prevents the "identical route - // every trip" fingerprint a deterministic A* would leave. + // tile sequences between the same start/target pair. public final int tiebreaker; - public int fCost() { - return cost + heuristic; - } - public Node(WorldPoint position, Node previous, int wait) { this.packedPosition = WorldPointUtil.packWorldPoint(position); this.previous = previous; @@ -47,10 +41,19 @@ public Node(int packedPosition, Node previous) { } public List getPath() { - List path = new ArrayList<>(); - for (Node n = this; n != null; n = n.previous) { + List nodes = getNodePath(); + List path = new ArrayList<>(nodes.size()); + for (Node n : nodes) { path.add(WorldPointUtil.unpackWorldPoint(n.packedPosition)); } + return path; + } + + List getNodePath() { + List path = new ArrayList<>(); + for (Node n = this; n != null; n = n.previous) { + path.add(n); + } Collections.reverse(path); return path; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathEdge.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathEdge.java new file mode 100644 index 00000000000..59d6b14d8ae --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathEdge.java @@ -0,0 +1,126 @@ +package net.runelite.client.plugins.microbot.shortestpath.pathfinder; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.shortestpath.Transport; +import net.runelite.client.plugins.microbot.shortestpath.WorldPointUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** One materialized edge in a chosen local pathfinder route. */ +public final class PathEdge +{ + private final WorldPoint from; + private final WorldPoint to; + private final Transport transport; + + PathEdge(WorldPoint from, WorldPoint to, Transport transport) + { + this.from = from; + this.to = to; + this.transport = transport; + } + + static List fromForwardChain(Node lastNode) + { + if (lastNode == null) + { + return Collections.emptyList(); + } + List nodes = lastNode.getNodePath(); + List edges = new ArrayList<>(Math.max(0, nodes.size() - 1)); + for (int i = 1; i < nodes.size(); i++) + { + Node from = nodes.get(i - 1); + Node to = nodes.get(i); + Transport transport = to instanceof TransportNode + ? ((TransportNode) to).getTransport() + : null; + edges.add(new PathEdge( + WorldPointUtil.unpackWorldPoint(from.packedPosition), + WorldPointUtil.unpackWorldPoint(to.packedPosition), + transport)); + } + return Collections.unmodifiableList(edges); + } + + /** + * Build the temporary local compatibility view for a completed engine-neutral route. + * The transport list is aligned with path edges and may contain {@code null} walking entries. + */ + static List fromMaterializedRoute( + List path, List transportsByStep) + { + if (path == null || transportsByStep == null) + { + throw new IllegalArgumentException("materialized path and transports are required"); + } + int expected = Math.max(0, path.size() - 1); + if (transportsByStep.size() != expected) + { + throw new IllegalArgumentException( + "materialized transport count must match path edge count"); + } + List edges = new ArrayList<>(expected); + for (int i = 0; i < expected; i++) + { + WorldPoint from = path.get(i); + WorldPoint to = path.get(i + 1); + if (from == null || to == null) + { + throw new IllegalArgumentException("materialized path points must be non-null"); + } + Transport transport = transportsByStep.get(i); + if (transport != null && !to.equals(transport.getDestination())) + { + throw new IllegalArgumentException( + "materialized transport destination must match its route step"); + } + edges.add(new PathEdge(from, to, transport)); + } + return Collections.unmodifiableList(edges); + } + + /** + * Combine a normal start-to-meeting chain with the reverse-search meeting-to-goal chain. + * Reverse transport metadata belongs to the {@code from} node, unlike a forward chain where it + * belongs to the {@code to} node. + */ + static List fromBidirectionalChains(Node forwardAtMeet, Node backwardAtMeet) + { + List edges = new ArrayList<>(fromForwardChain(forwardAtMeet)); + for (Node from = backwardAtMeet; from != null && from.previous != null; from = from.previous) + { + Node to = from.previous; + Transport transport = from instanceof TransportNode + ? ((TransportNode) from).getTransport() + : null; + edges.add(new PathEdge( + WorldPointUtil.unpackWorldPoint(from.packedPosition), + WorldPointUtil.unpackWorldPoint(to.packedPosition), + transport)); + } + return Collections.unmodifiableList(edges); + } + + public WorldPoint getFrom() + { + return from; + } + + public WorldPoint getTo() + { + return to; + } + + public Transport getTransport() + { + return transport; + } + + public boolean isTransport() + { + return transport != null; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathTerminationReason.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathTerminationReason.java new file mode 100644 index 00000000000..a2d4c5bebe6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathTerminationReason.java @@ -0,0 +1,17 @@ +package net.runelite.client.plugins.microbot.shortestpath.pathfinder; + +/** + * Why a pathfinder run stopped. + * + *

The first four values intentionally match the tracked shortest-path upstream contract. Microbot + * adds {@link #FAILED} because its legacy pathfinder catches runtime failures in order to keep the + * client alive; callers must be able to distinguish that case from an exhausted graph.

+ */ +public enum PathTerminationReason +{ + TARGET_REACHED, + SEARCH_EXHAUSTED, + CUTOFF_REACHED, + CANCELLED, + FAILED +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java index f7d32a4b1f9..7cd40039930 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/Pathfinder.java @@ -25,8 +25,7 @@ private static void pathfinderDiag(String format, Object... args) { } private static final Comparator NODE_ORDER = Comparator - .comparingInt(Node::fCost) - .thenComparingInt(n -> n.cost) + .comparingInt((Node n) -> n.cost) .thenComparingInt(n -> n.tiebreaker); /** @@ -39,6 +38,11 @@ private static void pathfinderDiag(String format, Object... args) { @Getter private volatile boolean done = false; private volatile boolean cancelled = false; + @Getter + private volatile PathTerminationReason terminationReason; + /** Search cost of the returned raw path, or {@code -1} when no path node was selected. */ + @Getter + private volatile long selectedPathCost = -1L; private final int start; private final Set targets; @@ -50,24 +54,18 @@ private static void pathfinderDiag(String format, Object... args) { private CollisionMap map; private final boolean targetInWilderness; - // Walking subgraph uses A* (boundary is a PQ keyed on f = g + Chebyshev heuristic), - // so among walking nodes the search picks the most promising direction first. - // Transports stay in a separate PQ keyed on g-cost only — they're picked when their - // travel cost is cheaper than any frontier walking node's g-cost, preserving the - // existing "try cheap transports before walking farther" selection behavior. + // Both walking and transport frontiers are ordered by travelled cost. A geometric + // heuristic is not admissible in a graph containing canoes, teleports and other + // long-distance edges: it can permanently visit a farther transport origin before + // a cheaper origin whose straight-line direction initially points away from the + // target. Cost ordering matches the reviewed upstream search semantics and keeps + // exact selected transport identity stable across the engine boundary. // - // Comparator chain is (fCost, gCost, tiebreaker): - // 1. fCost — standard A* primary ordering. - // 2. gCost — required for correctness under early-discovery. addNeighbors() marks - // a neighbor visited at insert time (not at pop), so a node only ever enters - // the PQ once. If two equal-fCost nodes have different gCost, popping the - // higher-gCost one first would fix their shared neighbor's gCost to a - // suboptimal value (because visited is already set when the lower-g node later - // tries to discover the same neighbor). Preferring lower gCost on ties keeps - // early-discovery optimal. - // 3. tiebreaker — per-node random. Among nodes with identical (f, g) — common in - // open-grid regions where many tiles share the same distance-from-start and - // distance-to-goal — this rotates the exploration order each run so paths + // Comparator chain is (gCost, tiebreaker): + // 1. gCost — required for correctness because addNeighbors() marks a neighbor + // visited at insert time and therefore never relaxes it later. + // 2. tiebreaker — per-node random. Among nodes with identical cost — common in + // open-grid regions — this rotates the exploration order each run so paths // diverge tile-by-tile between successive searches with the same endpoints. // Kills the deterministic "identical route every trip" fingerprint. private final Queue boundary = new PriorityQueue<>(4096, NODE_ORDER); @@ -78,11 +76,14 @@ private static void pathfinderDiag(String format, Object... args) { private volatile List path = Collections.emptyList(); private volatile List smoothedPath = Collections.emptyList(); - private volatile boolean pathNeedsUpdate = false; + /** Node identity represented by {@link #path}; avoids a lost-update race with live path readers. */ + private volatile Node materializedPathLastNode; private volatile boolean smoothed = false; private volatile Node bestLastNode; /** When set, {@link #getPath()} returns this list (bidirectional join or early exact hit). */ private volatile List joinedPath; + /** Edge-preserving counterpart to {@link #joinedPath}. */ + private volatile List joinedPathEdges; /** * Teleportation transports are updated when this changes. * Can be either: @@ -120,6 +121,67 @@ public Pathfinder(PathfinderConfig config, WorldPoint start, WorldPoint target) this(config, start, Set.of(target)); } + /** + * Materialize a completed planner-independent route behind the legacy concrete pathfinder surface. + * + *

This is a transitional adapter for the shortest-path overlays and out-of-tree callers that still + * consume {@code ShortestPathPlugin.pathfinder}. New walker code must consume the immutable route + * contract instead. Remove this factory with the local planner after the staged rollout sunset.

+ */ + public static Pathfinder completedRoute( + PathfinderConfig config, + WorldPoint start, + Set targets, + List path, + List transportsByStep, + PathTerminationReason terminationReason, + long selectedPathCost, + long searchNanos, + long nodesChecked, + long transportsChecked, + long liveCollisionEdgesChecked) { + Objects.requireNonNull(config, "config"); + Objects.requireNonNull(start, "start"); + Objects.requireNonNull(targets, "targets"); + Objects.requireNonNull(path, "path"); + Objects.requireNonNull(transportsByStep, "transportsByStep"); + Objects.requireNonNull(terminationReason, "terminationReason"); + if (!path.isEmpty() && !start.equals(path.get(0))) { + throw new IllegalArgumentException("materialized route must start at the requested start"); + } + if (selectedPathCost < -1L || searchNanos < -1L || nodesChecked < -1L + || transportsChecked < -1L || liveCollisionEdgesChecked < -1L) { + throw new IllegalArgumentException("materialized route metrics must be non-negative or unavailable"); + } + + Pathfinder completed = new Pathfinder(config, start, targets); + List immutablePath = Collections.unmodifiableList(new ArrayList<>(path)); + completed.map = config.getMap(); + completed.joinedPath = immutablePath; + completed.joinedPathEdges = PathEdge.fromMaterializedRoute(immutablePath, transportsByStep); + completed.terminationReason = terminationReason; + completed.selectedPathCost = selectedPathCost; + completed.cancelled = false; + completed.done = true; + completed.stats.complete( + metricOrZero(searchNanos), + metricAsInt(nodesChecked), + metricAsInt(transportsChecked), + metricOrZero(liveCollisionEdgesChecked)); + return completed; + } + + private static long metricOrZero(long metric) { + return metric < 0L ? 0L : metric; + } + + private static int metricAsInt(long metric) { + if (metric < 0L) { + return 0; + } + return (int) Math.min(Integer.MAX_VALUE, metric); + } + public WorldPoint getStart() { return WorldPointUtil.unpackWorldPoint(start); } @@ -151,13 +213,30 @@ public List getPath() { return path; } - if (pathNeedsUpdate) { - path = lastNode.getPath(); - pathNeedsUpdate = false; + List currentPath = path; + if (materializedPathLastNode != lastNode) { + // The walker may read a partial path while this search is still running. Identity-based + // invalidation is required here: a reader clearing a shared dirty flag can otherwise erase + // a newer pathfinder-thread update, leaving getPath() and getPathEdges() on different nodes. + currentPath = Collections.unmodifiableList(lastNode.getPath()); + path = currentPath; + materializedPathLastNode = lastNode; smoothed = false; } - return path; + return currentPath; + } + + /** + * Materialized edges for the current best route. Transport edges retain the exact catalog entry + * selected by the search; callers outside shortest-path should map them to owned immutable values. + */ + public List getPathEdges() { + List joined = joinedPathEdges; + if (joined != null) { + return joined; + } + return PathEdge.fromForwardChain(bestLastNode); } /** @@ -203,7 +282,6 @@ private Set buildTransportAnchors(List path) { private void addNeighbors(Node node) { List nodes = map.getNeighbors(node, visited, config, targets); - boolean afterTransport = node instanceof TransportNode; for (Node neighbor : nodes) { if (config.avoidWilderness(node.packedPosition, neighbor.packedPosition, targetInWilderness)) { continue; @@ -214,189 +292,12 @@ private void addNeighbors(Node node) { pending.add(neighbor); ++stats.transportsChecked; } else { - neighbor.heuristic = afterTransport ? 0 : heuristicToNearestTarget(neighbor.packedPosition); boundary.add(neighbor); ++stats.nodesChecked; } } } - // Admissible A* heuristic: Chebyshev 2D to the nearest target, with a modulo-6400 - // fallback for the surface↔underground Y-offset convention (OSRS shifts underground - // coords by +6400 on the Y axis, so Varrock sewers live at y≈9800 while Varrock sits - // at y≈3400). Plain Chebyshev would claim ~6200 tiles to any underground point, which - // misdirects A* into expanding the surface southward instead of routing through a - // nearby ladder/stairs transport. Taking min(direct, mod-6400) stays admissible - // because reaching a y-mirrored underground point still requires ≥ one transport - // (cost ≥ 0) on top of the mod-6400 walking distance. The band-aware distance lives in - // WorldPointUtil.undergroundAwareDistance so the walker uses the same metric. - - private int heuristicToNearestTarget(int packedPos) { - return applyLandmarks(packedPos, baseHeuristicToNearestTarget(packedPos), - fwdLandmark, fwdLandmarkResidual); - } - - private int baseHeuristicToNearestTarget(int packedPos) { - int posX = WorldPointUtil.unpackWorldX(packedPos); - int posY = WorldPointUtil.unpackWorldY(packedPos); - int best = Integer.MAX_VALUE; - for (int target : targetsPacked) { - int tx = WorldPointUtil.unpackWorldX(target); - int ty = WorldPointUtil.unpackWorldY(target); - int h = WorldPointUtil.undergroundAwareDistance(posX, posY, tx, ty); - if (h < best) { - best = h; - } - } - return best; - } - - private int heuristicFromStart(int packedPos) { - return applyLandmarks(packedPos, baseHeuristicFromStart(packedPos), - backLandmark, backLandmarkResidual); - } - - private int baseHeuristicFromStart(int packedPos) { - int posX = WorldPointUtil.unpackWorldX(packedPos); - int posY = WorldPointUtil.unpackWorldY(packedPos); - int sx = WorldPointUtil.unpackWorldX(start); - int sy = WorldPointUtil.unpackWorldY(start); - return WorldPointUtil.undergroundAwareDistance(posX, posY, sx, sy); - } - - // --- Network-transport-aware heuristic --------------------------------------------------- - // - // Network transports (fairy rings, spirit trees, gnome gliders, quetzals) are fully-connected - // hubs: reaching ANY origin lets you hop to ANY destination of that network for ~free. Plain - // Chebyshev is blind to this — a node next to the Ardougne fairy ring reads "~1350 tiles from - // the Farming Guild" by straight line, so A* buries the (optimal) cloak->fairy->CIR chain under - // a single direct teleport that the heuristic makes look closer. We fold the hubs into the - // heuristic as landmarks: for each enabled network whose destinations reach near the goal, every - // network origin is a landmark with residual = min(dest -> goal). Then - // h(node) = min(directWalk, dist(node, nearestOrigin) + residual). - // Each landmark term is a true lower bound (walking to the origin, a free-ish hop, then the - // residual walk to goal), so taking min with the admissible Chebyshev keeps the result both - // admissible AND consistent (the landmark set is fixed for the whole search). A* optimality is - // therefore preserved, while the search is now pulled toward useful hubs instead of ignoring - // them. The backward (bidirectional) arrays are symmetric: landmarks are destinations, residual - // is min(origin -> start). Unlike the reverted chain-bridge injection this adds no graph edges - // (so it can never teleport the player out of a building), and unlike the reverted post-transport - // cascade it never zeroes the heuristic (so it can never collapse into a whole-map Dijkstra). - private static final EnumSet NETWORK_HEURISTIC_TYPES = EnumSet.of( - TransportType.FAIRY_RING, TransportType.SPIRIT_TREE, - TransportType.GNOME_GLIDER, TransportType.QUETZAL); - - private int[] fwdLandmark = null; // packed network origins (reach a hub -> hop toward target) - private int[] fwdLandmarkResidual = null; // parallel: that network's min(dest -> nearest target) Chebyshev - private int[] backLandmark = null; // packed network destinations (symmetric, for backward search) - private int[] backLandmarkResidual = null; // parallel: that network's min(origin -> start) Chebyshev - - private int applyLandmarks(int packedPos, int base, int[] landmarks, int[] residuals) { - if (landmarks == null || landmarks.length == 0) { - return base; - } - int px = WorldPointUtil.unpackWorldX(packedPos); - int py = WorldPointUtil.unpackWorldY(packedPos); - int best = base; - for (int i = 0; i < landmarks.length; i++) { - int lx = WorldPointUtil.unpackWorldX(landmarks[i]); - int ly = WorldPointUtil.unpackWorldY(landmarks[i]); - int viaHub = Math.max(Math.abs(px - lx), Math.abs(py - ly)) + residuals[i]; - if (viaHub < best) { - best = viaHub; - } - } - return best; - } - - /** - * Builds {@link #fwdLandmark}/{@link #backLandmark} once per pathfind from the enabled network - * transports. A network only contributes landmarks if it gets you strictly closer to the goal - * (resp. start) than you already are — otherwise it is pure heuristic overhead with no benefit. - */ - private void computeNetworkLandmarks() { - Map> all = config.getTransports(); - if (all == null || all.isEmpty()) { - return; - } - - EnumMap> originsByType = new EnumMap<>(TransportType.class); - EnumMap> destsByType = new EnumMap<>(TransportType.class); - for (Set set : all.values()) { - if (set == null) { - continue; - } - for (Transport t : set) { - TransportType type = t.getType(); - if (type == null || !NETWORK_HEURISTIC_TYPES.contains(type)) { - continue; - } - WorldPoint o = t.getOrigin(); - WorldPoint d = t.getDestination(); - if (o == null || d == null) { - continue; - } - originsByType.computeIfAbsent(type, k -> new HashSet<>()).add(WorldPointUtil.packWorldPoint(o)); - destsByType.computeIfAbsent(type, k -> new HashSet<>()).add(WorldPointUtil.packWorldPoint(d)); - } - } - if (originsByType.isEmpty()) { - return; - } - - int startToGoal = minChebyshevStartToAnyTarget(); - List fwd = new ArrayList<>(); // {originPacked, residual} - List back = new ArrayList<>(); // {destPacked, residual} - for (Map.Entry> e : originsByType.entrySet()) { - Set origins = e.getValue(); - Set dests = destsByType.getOrDefault(e.getKey(), Collections.emptySet()); - if (origins.isEmpty() || dests.isEmpty()) { - continue; - } - - int residualFwd = Integer.MAX_VALUE; - for (int d : dests) { - residualFwd = Math.min(residualFwd, baseHeuristicToNearestTarget(d)); - } - if (residualFwd < startToGoal) { - for (int o : origins) { - fwd.add(new int[]{o, residualFwd}); - } - } - - int residualBack = Integer.MAX_VALUE; - for (int o : origins) { - residualBack = Math.min(residualBack, baseHeuristicFromStart(o)); - } - if (residualBack < startToGoal) { - for (int d : dests) { - back.add(new int[]{d, residualBack}); - } - } - } - - fwdLandmark = packLandmarkPositions(fwd); - fwdLandmarkResidual = packLandmarkResiduals(fwd); - backLandmark = packLandmarkPositions(back); - backLandmarkResidual = packLandmarkResiduals(back); - } - - private static int[] packLandmarkPositions(List landmarks) { - int[] out = new int[landmarks.size()]; - for (int i = 0; i < out.length; i++) { - out[i] = landmarks.get(i)[0]; - } - return out; - } - - private static int[] packLandmarkResiduals(List landmarks) { - int[] out = new int[landmarks.size()]; - for (int i = 0; i < out.length; i++) { - out[i] = landmarks.get(i)[1]; - } - return out; - } - private int minChebyshevStartToAnyTarget() { int best = Integer.MAX_VALUE; for (int t : targetsPacked) { @@ -436,16 +337,19 @@ private List combineBidirectionalPath(Node forwardAtMeet, Node backw List head = forwardAtMeet.getPath(); List full = new ArrayList<>(head.size() + 64); full.addAll(head); - for (Node n = backwardAtMeet.previous; n != null; n = n.previous) { - full.add(WorldPointUtil.unpackWorldPoint(n.packedPosition)); + + List edges = PathEdge.fromBidirectionalChains(forwardAtMeet, backwardAtMeet); + for (Node from = backwardAtMeet; from != null && from.previous != null; from = from.previous) { + Node to = from.previous; + full.add(WorldPointUtil.unpackWorldPoint(to.packedPosition)); } + joinedPathEdges = edges; return full; } private void addNeighborsForwardWithMeet(Node node, Map forwardAt, Map backwardAt, long[] bestMeetingCost, Node[] meetF, Node[] meetB) { List nodes = map.getNeighbors(node, visited, config, targets); - boolean afterTransport = node instanceof TransportNode; for (Node neighbor : nodes) { if (config.avoidWilderness(node.packedPosition, neighbor.packedPosition, targetInWilderness)) { continue; @@ -456,7 +360,6 @@ private void addNeighborsForwardWithMeet(Node node, Map forwardAt pending.add(neighbor); ++stats.transportsChecked; } else { - neighbor.heuristic = afterTransport ? 0 : heuristicToNearestTarget(neighbor.packedPosition); boundary.add(neighbor); ++stats.nodesChecked; } @@ -472,7 +375,6 @@ private void addNeighborsBackwardWithMeet(Node node, VisitedTiles visitedB, Map< Set puzzleAllow, Map forwardAt, Map backwardAt, long[] bestMeetingCost, Node[] meetF, Node[] meetB) { List nodes = map.getReverseNeighbors(node, visitedB, config, puzzleAllow, incoming); - boolean afterTransport = node instanceof TransportNode; for (Node pred : nodes) { if (config.avoidWilderness(pred.packedPosition, node.packedPosition, targetInWilderness)) { continue; @@ -483,7 +385,6 @@ private void addNeighborsBackwardWithMeet(Node node, VisitedTiles visitedB, Map< pendingBackward.add(pred); ++stats.transportsChecked; } else { - pred.heuristic = afterTransport ? 0 : heuristicFromStart(pred.packedPosition); boundaryBackward.add(pred); ++stats.nodesChecked; } @@ -497,7 +398,6 @@ private void addNeighborsBackwardWithMeet(Node node, VisitedTiles visitedB, Map< private void runUnidirectional() { Node startNode = new Node(start, null); - startNode.heuristic = heuristicToNearestTarget(start); boundary.add(startNode); int bestDistance = Integer.MAX_VALUE; @@ -542,7 +442,6 @@ private void runUnidirectional() { for (int target : targetsPacked) { if (nodePos == target) { bestLastNode = node; - pathNeedsUpdate = true; reached = true; break; } @@ -550,7 +449,6 @@ private void runUnidirectional() { long heuristic = distance + (long) WorldPointUtil.distanceBetween(nodePos, target, 2); if (heuristic < bestHeuristic || (heuristic <= bestHeuristic && distance < bestDistance)) { bestLastNode = node; - pathNeedsUpdate = true; bestDistance = distance; bestHeuristic = heuristic; cutoffTimeMillis = System.currentTimeMillis() + cutoffDurationMillis; @@ -585,6 +483,11 @@ private void runUnidirectional() { WebWalkLog.pf("uni_loop_exit cancelled={} bEmpty={} pEmpty={} bestLast={}", cancelled, boundary.isEmpty(), pending.isEmpty(), bestLastNode == null ? "null" : WorldPointUtil.toString(bestLastNode.packedPosition)); + + terminationReason = cancelled ? PathTerminationReason.CANCELLED + : reachedGoal ? PathTerminationReason.TARGET_REACHED + : timedOut ? PathTerminationReason.CUTOFF_REACHED + : PathTerminationReason.SEARCH_EXHAUSTED; } private void runBidirectional() { @@ -606,12 +509,10 @@ private void runBidirectional() { Node[] meetB = new Node[1]; Node startNode = new Node(start, null); - startNode.heuristic = heuristicToNearestTarget(start); boundary.add(startNode); forwardAt.put(start, startNode); Node goalNode = new Node(goalPacked, null); - goalNode.heuristic = heuristicFromStart(goalPacked); boundaryBackward.add(goalNode); backwardAt.put(goalPacked, goalNode); @@ -620,6 +521,7 @@ private void runBidirectional() { long cutoffDurationMillis = config.getCalculationCutoffMillis(); long cutoffTimeMillis = System.currentTimeMillis() + cutoffDurationMillis; config.refreshTeleports(start, 31); + boolean timedOut = false; while (!cancelled && (!boundary.isEmpty() || !pending.isEmpty() || !boundaryBackward.isEmpty() || !pendingBackward.isEmpty())) { if (!boundary.isEmpty() || !pending.isEmpty()) { @@ -653,8 +555,9 @@ private void runBidirectional() { final int nodePos = node.packedPosition; if (nodePos == goalPacked) { + joinedPathEdges = PathEdge.fromForwardChain(node); joinedPath = node.getPath(); - pathNeedsUpdate = false; + selectedPathCost = node.cost; bestLastNode = null; WebWalkLog.pf("bidir forward_hit_goal"); break; @@ -665,7 +568,6 @@ private void runBidirectional() { long heuristic = distance + (long) WorldPointUtil.distanceBetween(nodePos, target, 2); if (heuristic < bestHeuristic || (heuristic <= bestHeuristic && distance < bestDistance)) { bestLastNode = node; - pathNeedsUpdate = true; bestDistance = distance; bestHeuristic = heuristic; cutoffTimeMillis = System.currentTimeMillis() + cutoffDurationMillis; @@ -691,7 +593,7 @@ private void runBidirectional() { if (node.packedPosition == start) { joinedPath = combineBidirectionalPath(forwardAt.get(start), node); - pathNeedsUpdate = false; + selectedPathCost = node.cost; bestLastNode = null; WebWalkLog.pf("bidir backward_hit_start"); break; @@ -701,6 +603,7 @@ private void runBidirectional() { } if (System.currentTimeMillis() > cutoffTimeMillis) { + timedOut = true; WebWalkLog.pf("bidir_cutoff nodes={}", stats.getNodesChecked()); break; } @@ -708,7 +611,7 @@ private void runBidirectional() { if (joinedPath == null && meetF[0] != null && meetB[0] != null && bestMeetingCost[0] < Long.MAX_VALUE) { joinedPath = combineBidirectionalPath(meetF[0], meetB[0]); - pathNeedsUpdate = false; + selectedPathCost = bestMeetingCost[0]; bestLastNode = null; WebWalkLog.pf("bidir meet_at={} cost={}", WorldPointUtil.toString(meetF[0].packedPosition), bestMeetingCost[0]); @@ -726,24 +629,35 @@ private void runBidirectional() { WebWalkLog.pf("bidir_exit joined={} meetCost={}", joinedPath == null ? "null" : Integer.toString(joinedPath.size()), bestMeetingCost[0] == Long.MAX_VALUE ? "n/a" : Long.toString(bestMeetingCost[0])); + + terminationReason = cancelled ? PathTerminationReason.CANCELLED + : joinedPath != null ? PathTerminationReason.TARGET_REACHED + : timedOut ? PathTerminationReason.CUTOFF_REACHED + : PathTerminationReason.SEARCH_EXHAUSTED; } @Override public void run() { WebWalkLog.pf("run_start src={} dst={} cutoffMs={}", WorldPointUtil.toString(start), WorldPointUtil.toString(targets), config.getCalculationCutoffMillis()); + path = Collections.emptyList(); + smoothedPath = Collections.emptyList(); + materializedPathLastNode = null; + smoothed = false; joinedPath = null; - // Pathfinder instances are commonly constructed on the client thread and submitted to the - // shortest-path executor. Resolve both ThreadLocal-backed objects here so the collision map, - // visited state and pinned live snapshot all belong to the search thread for this run. - map = config.getMap(); - visited = new VisitedTiles(map); - // Pin the live-collision snapshot for this whole search so a mid-search swap on the client - // thread cannot mix two scenes into one path. No-op when live collision is disabled. - map.beginSearch(); + joinedPathEdges = null; + terminationReason = null; + selectedPathCost = -1L; try { + // Pathfinder instances are commonly constructed on the client thread and submitted to the + // shortest-path executor. Resolve both ThreadLocal-backed objects here so the collision map, + // visited state and pinned live snapshot all belong to the search thread for this run. + map = config.getMap(); + visited = new VisitedTiles(map); + // Pin the live-collision snapshot for this whole search so a mid-search swap on the client + // thread cannot mix two scenes into one path. No-op when live collision is disabled. + map.beginSearch(); stats.start(); - computeNetworkLandmarks(); int minCheb = minChebyshevStartToAnyTarget(); boolean useBidir = targetsPacked.length == 1 && minCheb >= BIDIRECTIONAL_MIN_CHEBYSHEV; @@ -761,26 +675,40 @@ public void run() { runUnidirectional(); } } catch (Exception e) { + terminationReason = PathTerminationReason.FAILED; log.error("[Pathfinder] Exception in run(): ", e); } finally { + if (terminationReason == null) { + terminationReason = cancelled + ? PathTerminationReason.CANCELLED + : PathTerminationReason.SEARCH_EXHAUSTED; + } + if (selectedPathCost < 0 && bestLastNode != null) { + selectedPathCost = bestLastNode.cost; + } done = !cancelled; boundary.clear(); pending.clear(); boundaryBackward.clear(); pendingBackward.clear(); - visited.clear(); + if (visited != null) { + visited.clear(); + } - stats.end(); + stats.end(map == null ? 0L : map.getLiveEdgeQueries()); - WebWalkLog.pf("run_done done={} cancelled={} stats={}", - done, cancelled, getStats() != null ? getStats().toString() : "null"); + WebWalkLog.pf("run_done done={} cancelled={} termination={} stats={}", + done, cancelled, terminationReason, + getStats() != null ? getStats().toString() : "null"); } } public static class PathfinderStats { @Getter private int nodesChecked = 0, transportsChecked = 0; + @Getter + private long liveCollisionEdgesChecked = 0L; private long startNanos, endNanos; private volatile boolean started = false, ended = false; @@ -799,14 +727,31 @@ private void start() { startNanos = System.nanoTime(); } - private void end() { + private void complete( + long elapsedNanos, + int nodesChecked, + int transportsChecked, + long liveCollisionEdgesChecked) { + this.started = true; + this.nodesChecked = nodesChecked; + this.transportsChecked = transportsChecked; + this.liveCollisionEdgesChecked = liveCollisionEdgesChecked; + this.startNanos = 0L; + this.endNanos = elapsedNanos; + this.ended = true; + } + + private void end(long liveCollisionEdgesChecked) { + this.liveCollisionEdgesChecked = liveCollisionEdgesChecked; endNanos = System.nanoTime(); ended = true; } @Override public String toString() { - return String.format("PathfinderStats(nodes=%d,transports=%d,time=%dms)", nodesChecked, transportsChecked, getElapsedTimeNanos() / 1_000_000); + return String.format("PathfinderStats(nodes=%d,transports=%d,liveEdges=%d,time=%dms)", + nodesChecked, transportsChecked, liveCollisionEdgesChecked, + getElapsedTimeNanos() / 1_000_000); } } } 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 80510af1637..d02209eaa6d 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 @@ -18,6 +18,7 @@ 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.magic.Rs2Magic; import net.runelite.client.plugins.microbot.util.magic.Rs2Spells; import net.runelite.client.plugins.microbot.util.magic.RuneFilter; @@ -93,6 +94,12 @@ public class PathfinderConfig { private final Map> allTransports; @Setter private volatile Set usableTeleports; + + /** Immutable exact-object snapshot for planner adapters after transport admission has run. */ + public Set getUsableTeleportsSnapshot() { + Set current = usableTeleports; + return current == null ? Collections.emptySet() : Set.copyOf(current); + } private final List filteredTargets = new CopyOnWriteArrayList<>(); @Getter @@ -128,6 +135,7 @@ public class PathfinderConfig { private final Client client; private final ShortestPathConfig config; + private final TransportPlanningPolicy transportPlanningPolicy; private final List questStateOrder = Arrays.asList( QuestState.NOT_STARTED, @@ -159,6 +167,8 @@ public class PathfinderConfig { private volatile boolean avoidWilderness; @Getter private volatile boolean avoidDangerousNpcs; + @Getter + private volatile PlannerSelectionMode plannerSelectionMode = PlannerSelectionMode.LOCAL; @Getter private volatile boolean useSpiritTrees; private volatile boolean useAgilityShortcuts, @@ -210,7 +220,8 @@ public class PathfinderConfig { // Used to include bank items when searching for item requirements private volatile boolean useBankItems = false; - private Set refreshAvailableItemIds; + private Map refreshAvailableItemQuantities; + private Map refreshAvailableRuneQuantities; private int[] refreshBoostedLevels; private Map refreshCurrencyCache; // Varplayer values snapshot for the current refreshTransports pass. Without it, every varp @@ -259,8 +270,17 @@ protected boolean removeEldestEntry(Map.Entry public PathfinderConfig(SplitFlagMap mapData, Map> transports, List restrictions, Client client, ShortestPathConfig config) { + this(mapData, transports, restrictions, client, config, TransportPlanningPolicy.ALLOW_ALL); + } + + public PathfinderConfig(SplitFlagMap mapData, Map> transports, + List restrictions, + Client client, ShortestPathConfig config, + TransportPlanningPolicy transportPlanningPolicy) { this.mapData = mapData; - this.map = ThreadLocal.withInitial(() -> new CollisionMap(this.mapData, this.liveCollisionOverlay)); + this.map = ThreadLocal.withInitial(() -> client == null + ? new CollisionMap(this.mapData, this.liveCollisionOverlay, () -> -1) + : new CollisionMap(this.mapData, this.liveCollisionOverlay)); this.allTransports = Collections.synchronizedMap(new HashMap<>()); replaceAllTransports(transports); this.usableTeleports = ConcurrentHashMap.newKeySet(allTransports.size() / 20); @@ -272,6 +292,8 @@ public PathfinderConfig(SplitFlagMap mapData, Map> tr loadLearnedBlockedEdges(); this.client = client; this.config = config; + this.transportPlanningPolicy = Objects.requireNonNull( + transportPlanningPolicy, "transportPlanningPolicy"); //START microbot variables this.resourceRestrictions = restrictions; this.customRestrictions = Collections.emptyList(); @@ -337,6 +359,8 @@ 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()); + plannerSelectionMode = ShortestPathPlugin.override( + "plannerSelectionMode", config.plannerSelectionMode()); useAgilityShortcuts = ShortestPathPlugin.override("useAgilityShortcuts", config.useAgilityShortcuts()); useGrappleShortcuts = ShortestPathPlugin.override("useGrappleShortcuts", config.useGrappleShortcuts()); useBoats = ShortestPathPlugin.override("useBoats", config.useBoats()); @@ -467,15 +491,15 @@ private void refreshTransports(WorldPoint target) { TransportRefreshSnapshot snap = transportRefreshSnapshots.get(refreshCacheKeyHash); if (snap != null && client != null) { - int[] boostedProbe = new int[SKILLS.length]; + int[] boostedProbe = new int[Transport.REQUIREMENT_LEVEL_COUNT]; final int[] probeOrdinals = snap.sortedSkillOrdinals; Microbot.getClientThread().runOnClientThreadOptional(() -> { // Only the skills some transport gates on; probing all 23 both cost client-thread // time and let hitpoints/prayer drift invalidate an otherwise valid cache. if (probeOrdinals != null) { for (int ordinal : probeOrdinals) { - if (ordinal >= 0 && ordinal < SKILLS.length) { - boostedProbe[ordinal] = client.getBoostedSkillLevel(SKILLS[ordinal]); + if (ordinal >= 0 && ordinal < Transport.REQUIREMENT_LEVEL_COUNT) { + boostedProbe[ordinal] = currentRequirementLevel(ordinal); } } } @@ -540,13 +564,20 @@ private void refreshTransports(WorldPoint target) { long mergeTime = System.currentTimeMillis() - mergeStart; long cacheStart = System.currentTimeMillis(); - refreshAvailableItemIds = new HashSet<>(); + refreshAvailableItemQuantities = new HashMap<>(); refreshCurrencyCache = new HashMap<>(); - Rs2Inventory.items().forEach(item -> refreshAvailableItemIds.add(item.getId())); - Rs2Equipment.all().forEach(item -> refreshAvailableItemIds.add(item.getId())); + Rs2Inventory.items().forEach(item -> refreshAvailableItemQuantities.merge( + item.getId(), Math.max(0, item.getQuantity()), Integer::sum)); + Rs2Equipment.all().forEach(item -> refreshAvailableItemQuantities.merge( + item.getId(), Math.max(0, item.getQuantity()), Integer::sum)); if (useBankItems) { - Rs2Bank.getAll().forEach(item -> refreshAvailableItemIds.add(item.getId())); + Rs2Bank.getAll().forEach(item -> refreshAvailableItemQuantities.merge( + item.getId(), Math.max(0, item.getQuantity()), Integer::sum)); } + refreshAvailableRuneQuantities = new HashMap<>(); + Rs2Magic.getRunes(RuneFilter.builder().includeBank(useBankItems).build()) + .forEach((rune, quantity) -> refreshAvailableRuneQuantities.put( + rune.getItemId(), quantity)); Set varbitIds = new HashSet<>(); List varbitConditions = new ArrayList<>(); @@ -622,12 +653,17 @@ private void refreshTransports(WorldPoint target) { ? Collections.unmodifiableSet(relevantItemIds) : null; - refreshBoostedLevels = new int[SKILLS.length]; + refreshBoostedLevels = new int[Transport.REQUIREMENT_LEVEL_COUNT]; Map varplayerValues = new HashMap<>(); Microbot.getClientThread().runOnClientThreadOptional(() -> { for (int i = 0; i < SKILLS.length; i++) { refreshBoostedLevels[i] = client.getBoostedSkillLevel(SKILLS[i]); } + refreshBoostedLevels[Transport.TOTAL_LEVEL_INDEX] = client.getTotalLevel(); + Player localPlayer = client.getLocalPlayer(); + refreshBoostedLevels[Transport.COMBAT_LEVEL_INDEX] = + localPlayer == null ? 0 : localPlayer.getCombatLevel(); + refreshBoostedLevels[Transport.QUEST_POINTS_INDEX] = client.getVarpValue(VarPlayer.QUEST_POINTS); for (int id : varbitIds) { Microbot.getVarbitValue(id); } @@ -693,7 +729,13 @@ private void refreshTransports(WorldPoint target) { } } - Rs2LeaguesTransport.injectLeaguesTransports(this, leaguesCtx, usableTeleports, transports, transportsPacked, typeStats); + Rs2LeaguesTransport.injectLeaguesTransports( + transport -> isTransportUsableWithLeaguesContext(transport, leaguesCtx), + leaguesCtx, + usableTeleports, + transports, + transportsPacked, + typeStats); long filterTime = System.currentTimeMillis() - filterStart; int[] sortedVarbitConditions = encodeSortedConditionTriples(varbitConditions); @@ -725,7 +767,8 @@ private void refreshTransports(WorldPoint target) { } long similarTime = System.currentTimeMillis() - similarStart; - refreshAvailableItemIds = null; + refreshAvailableItemQuantities = null; + refreshAvailableRuneQuantities = null; refreshBoostedLevels = null; refreshCurrencyCache = null; refreshVarplayerValues = null; @@ -1245,40 +1288,49 @@ private int getLiveVarplayerValue(int varplayerId) { } private boolean useTransport(Transport transport) { + // This runs once per expanded catalog edge during every refresh. Keep individual rejection + // reasons at TRACE; DEBUG already receives the per-type aggregate emitted by refreshTransports. + if (!transportPlanningPolicy.isAdmitted(transport)) { + log.trace("Transport ( O: {} D: {} type={} ) has no registered Microbot executor", + transport == null ? null : transport.getOrigin(), + transport == null ? null : transport.getDestination(), + transport == null ? null : transport.getType()); + return false; + } // Check if the feature flag is disabled if (!isFeatureEnabled(transport)) { - log.debug("Transport Type {} is disabled by feature flag", transport.getType()); + log.trace("Transport Type {} is disabled by feature flag", transport.getType()); return false; } // If the transport requires you to be in a members world (used for more granular member requirements) if (transport.isMembers() && !client.getWorldType().contains(WorldType.MEMBERS)) { - log.debug("Transport ( O: {} D: {} ) requires members world", transport.getOrigin(), transport.getDestination()); + log.trace("Transport ( O: {} D: {} ) requires members world", transport.getOrigin(), transport.getDestination()); return false; } if (transport.getType() == TransportType.SPIRIT_TREE && !isSpiritTreeRouteEnabled(transport)) { - log.debug("Transport ( O: {} D: {} ) is a spirit tree route but the tree is disabled", transport.getOrigin(), transport.getDestination()); + log.trace("Transport ( O: {} D: {} ) is a spirit tree route but the tree is disabled", transport.getOrigin(), transport.getDestination()); return false; } // If you don't meet level requirements if (!hasRequiredLevels(transport)) { - log.debug("Transport ( O: {} D: {} ) requires skill levels {}", transport.getOrigin(), transport.getDestination(), Arrays.toString(transport.getSkillLevels())); + log.trace("Transport ( O: {} D: {} ) requires skill levels {}", transport.getOrigin(), transport.getDestination(), Arrays.toString(transport.getSkillLevels())); return false; } // If the transport has quest requirements & the quest haven't been completed if (transport.isQuestLocked() && !completedQuests(transport)) { - log.debug("Transport ( O: {} D: {} ) requires quests {}", transport.getOrigin(), transport.getDestination(), transport.getQuests()); + log.trace("Transport ( O: {} D: {} ) requires quests {}", transport.getOrigin(), transport.getDestination(), transport.getQuests()); return false; } // If the transport has varbit requirements & the varbits do not match if (!varbitChecks(transport)) { - log.debug("Transport ( O: {} D: {} ) requires varbits {}", transport.getOrigin(), transport.getDestination(), transport.getVarbits()); + log.trace("Transport ( O: {} D: {} ) requires varbits {}", transport.getOrigin(), transport.getDestination(), transport.getVarbits()); return false; } // If the transport has varplayer requirements & the varplayers do not match if (!varplayerChecks(transport)) { - log.debug("Transport ( O: {} D: {} ) requires varplayers {}", transport.getOrigin(), transport.getDestination(), transport.getVarplayers()); + log.trace("Transport ( O: {} D: {} ) requires varplayers {}", transport.getOrigin(), transport.getDestination(), transport.getVarplayers()); return false; } @@ -1291,19 +1343,19 @@ private boolean useTransport(Transport transport) { return new int[]{invCount, bankCount}; }); if (cached[0] < transport.getCurrencyAmount() && cached[1] < transport.getCurrencyAmount()) { - log.debug("Transport ( O: {} D: {} ) requires {} x {}", transport.getOrigin(), transport.getDestination(), transport.getCurrencyAmount(), transport.getCurrencyName()); + log.trace("Transport ( O: {} D: {} ) requires {} x {}", transport.getOrigin(), transport.getDestination(), transport.getCurrencyAmount(), transport.getCurrencyName()); return false; } } else if (!Rs2Inventory.hasItemAmount(transport.getCurrencyName(), transport.getCurrencyAmount()) && !(useBankItems && Rs2Bank.count(transport.getCurrencyName()) >= transport.getCurrencyAmount())) { - log.debug("Transport ( O: {} D: {} ) requires {} x {}", transport.getOrigin(), transport.getDestination(), transport.getCurrencyAmount(), transport.getCurrencyName()); + log.trace("Transport ( O: {} D: {} ) requires {} x {}", transport.getOrigin(), transport.getDestination(), transport.getCurrencyAmount(), transport.getCurrencyName()); return false; } } // Check if Teleports are globally disabled if (TransportType.isTeleport(transport.getType(), transport.getOrigin()) && Rs2Walker.disableTeleports) { - log.debug("Transport ( O: {} D: {} ) is a teleport but teleports are globally disabled", transport.getOrigin(), transport.getDestination()); + log.trace("Transport ( O: {} D: {} ) is a teleport but teleports are globally disabled", transport.getOrigin(), transport.getDestination()); return false; } @@ -1311,7 +1363,7 @@ private boolean useTransport(Transport transport) { if (transport.getType() == TELEPORTATION_ITEM) { boolean isUsable = isTeleportationItemUsable(transport); if (!isUsable) { - log.debug("Transport ( O: {} D: {} ) is a teleport item but is not usable", transport.getOrigin(), transport.getDestination()); + log.trace("Transport ( O: {} D: {} ) is a teleport item but is not usable", transport.getOrigin(), transport.getDestination()); } return isUsable; } @@ -1319,7 +1371,7 @@ private boolean useTransport(Transport transport) { if (transport.getType() == TELEPORTATION_SPELL) { boolean isUsable = isTeleportationSpellUsable(transport); if (!isUsable) { - log.debug("Transport ( O: {} D: {} ) is a teleport spell but is not usable", transport.getOrigin(), transport.getDestination()); + log.trace("Transport ( O: {} D: {} ) is a teleport spell but is not usable", transport.getOrigin(), transport.getDestination()); } return isUsable; } @@ -1328,7 +1380,7 @@ private boolean useTransport(Transport transport) { if (!transport.getItemIdRequirements().isEmpty()) { boolean hasRequiredItems = hasRequiredItems(transport); if (!hasRequiredItems) { - log.debug("Transport ( O: {} D: {} ) requires items {}", transport.getOrigin(), transport.getDestination(), transport.getItemIdRequirements().stream().flatMap(Set::stream).collect(Collectors.toSet())); + log.trace("Transport ( O: {} D: {} ) requires items {}", transport.getOrigin(), transport.getDestination(), transport.getItemIdRequirements().stream().flatMap(Set::stream).collect(Collectors.toSet())); } return hasRequiredItems; } @@ -1357,14 +1409,40 @@ public boolean isTransportUsableWithLeaguesContext(Transport transport, Rs2Leagu private boolean hasRequiredLevels(Transport transport) { int[] requiredLevels = transport.getSkillLevels(); if (refreshBoostedLevels != null) { - for (int i = 0; i < requiredLevels.length; i++) { - if (requiredLevels[i] > 0 && refreshBoostedLevels[i] < requiredLevels[i]) return false; - } - return true; + return meetsRequiredLevels(requiredLevels, refreshBoostedLevels); } return IntStream.range(0, requiredLevels.length) .filter(i -> requiredLevels[i] > 0) - .allMatch(i -> Microbot.getClient().getBoostedSkillLevel(SKILLS[i]) >= requiredLevels[i]); + .allMatch(i -> currentRequirementLevel(i) >= requiredLevels[i]); + } + + static boolean meetsRequiredLevels(int[] requiredLevels, int[] currentLevels) { + if (requiredLevels == null || currentLevels == null || currentLevels.length < requiredLevels.length) { + return false; + } + for (int i = 0; i < requiredLevels.length; i++) { + if (requiredLevels[i] > 0 && currentLevels[i] < requiredLevels[i]) { + return false; + } + } + return true; + } + + private int currentRequirementLevel(int index) { + if (index >= 0 && index < SKILLS.length) { + return client.getBoostedSkillLevel(SKILLS[index]); + } + if (index == Transport.TOTAL_LEVEL_INDEX) { + return client.getTotalLevel(); + } + if (index == Transport.COMBAT_LEVEL_INDEX) { + Player localPlayer = client.getLocalPlayer(); + return localPlayer == null ? 0 : localPlayer.getCombatLevel(); + } + if (index == Transport.QUEST_POINTS_INDEX) { + return client.getVarpValue(VarPlayer.QUEST_POINTS); + } + return 0; } /** @@ -1447,6 +1525,29 @@ private boolean isFeatureEnabled(Transport transport) { } } + return isTransportTypeEnabled(type); + } + + /** Immutable feature-toggle snapshot for planner-independent request policy. */ + public Set getEnabledTransportTypes() { + EnumSet enabled = EnumSet.noneOf(TransportType.class); + for (TransportType type : TransportType.values()) { + if (isTransportTypeEnabled(type)) { + enabled.add(type); + } + } + return Collections.unmodifiableSet(enabled); + } + + public TeleportationItem getTeleportationItemPolicy() { + return useTeleportationItems == null ? TeleportationItem.NONE : useTeleportationItems; + } + + public boolean isMembersWorld() { + return client == null || client.getWorldType().contains(WorldType.MEMBERS); + } + + private boolean isTransportTypeEnabled(TransportType type) { switch (type) { case AGILITY_SHORTCUT: return useAgilityShortcuts; @@ -1503,30 +1604,70 @@ private boolean isFeatureEnabled(Transport transport) { * Checks if a teleportation item is usable */ private boolean isTeleportationItemUsable(Transport transport) { - if (useTeleportationItems == TeleportationItem.NONE) return false; - // Check consumable items configuration - if (useTeleportationItems == TeleportationItem.INVENTORY_NON_CONSUMABLE && transport.isConsumable()) + if (!isTeleportationItemAllowedByPolicy(useTeleportationItems, transport.isConsumable())) { return false; + } return hasRequiredItems(transport); } + static boolean isTeleportationItemAllowedByPolicy( + TeleportationItem policy, + boolean consumable) { + return policy != TeleportationItem.NONE + && (policy != TeleportationItem.INVENTORY_NON_CONSUMABLE || !consumable); + } + /** * Checks if the player has any of the required equipment and inventory items for the transport */ private boolean hasRequiredItems(Transport transport) { - if (requiresChronicle(transport)) return hasChronicleCharges(); + return TransportItemRequirement.selectProviders( + transport.getItemRequirements(), + this::availableRequirementItemQuantity, + itemId -> availableItemQuantity(itemId) > 0, + itemId -> availableItemQuantity(itemId) > 0).isPresent(); + } - if (refreshAvailableItemIds != null) { - return transport.getItemIdRequirements() - .stream() - .flatMap(Collection::stream) - .anyMatch(refreshAvailableItemIds::contains); + static boolean meetsItemRequirements( + List requirements, + java.util.function.IntUnaryOperator availableQuantity) { + if (requirements == null || requirements.isEmpty()) { + return true; } - return transport.getItemIdRequirements() - .stream() - .flatMap(Collection::stream) - .anyMatch(itemId -> Rs2Equipment.isWearing(itemId) || Rs2Inventory.hasItem(itemId) || (ShortestPathPlugin.getPathfinderConfig().useBankItems && Rs2Bank.hasItem(itemId))); + return requirements.stream().allMatch(requirement -> requirement.isSatisfiedBy(availableQuantity)); + } + + private int availableItemQuantity(int itemId) { + if (itemId == ItemID.CHRONICLE && !hasChronicleCharges()) { + return 0; + } + if (refreshAvailableItemQuantities != null) { + return refreshAvailableItemQuantities.getOrDefault(itemId, 0); + } + int quantity = Rs2Inventory.itemQuantity(itemId); + Rs2ItemModel equipped = Rs2Equipment.get(itemId); + if (equipped != null) { + quantity += Math.max(1, equipped.getQuantity()); + } + if (useBankItems) { + quantity += Rs2Bank.count(itemId); + } + return quantity; + } + + private int availableRequirementItemQuantity(int itemId) { + Map runeSnapshot = refreshAvailableRuneQuantities; + if (runeSnapshot != null) { + return Math.max(availableItemQuantity(itemId), runeSnapshot.getOrDefault(itemId, 0)); + } + Runes rune = Runes.byItemId(itemId); + if (rune == null) { + return availableItemQuantity(itemId); + } + int runeQuantity = Rs2Magic.getRunes( + RuneFilter.builder().includeBank(useBankItems).build()).getOrDefault(rune, 0); + return Math.max(availableItemQuantity(itemId), runeQuantity); } /** @@ -1540,7 +1681,16 @@ private boolean hasRequiredItems(Restriction restriction) { } - private boolean isTeleportationSpellUsable(Transport transport) { + boolean isTeleportationSpellUsable(Transport transport) { + if (transportPlanningPolicy.isZeroRuneSpell(transport)) { + // Every spellbook home teleport is a zero-rune widget action. Spellbook, membership, + // quest, Wilderness and cooldown requirements were checked earlier in useTransport(). + return true; + } + + if (!transport.getItemRequirements().isEmpty()) { + return hasRequiredItems(transport); + } boolean hasMultipleDestination = transport.getDisplayInfo().contains(":"); String displayInfo = hasMultipleDestination @@ -1552,16 +1702,6 @@ private boolean isTeleportationSpellUsable(Transport transport) { // return Rs2Magic.quickCanCast(displayInfo); } - /** - * Checks if the transport requires the Chronicle - */ - private boolean requiresChronicle(Transport transport) { - return transport.getItemIdRequirements() - .stream() - .flatMap(Collection::stream) - .anyMatch(itemId -> itemId == ItemID.CHRONICLE); - } - /** * Checks if the Chronicle has charges */ diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/TransportNode.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/TransportNode.java index 28b048e6c0b..4a2c0791858 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/TransportNode.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/TransportNode.java @@ -1,9 +1,12 @@ package net.runelite.client.plugins.microbot.shortestpath.pathfinder; import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.shortestpath.Transport; public class TransportNode extends Node implements Comparable { - public TransportNode(WorldPoint point, Node previous, int travelTime) { + private final Transport transport; + + public TransportNode(WorldPoint point, Node previous, int travelTime, Transport transport) { // Use Node(int, Node, int cost) which assigns cost directly. The WorldPoint // Node constructor re-adds previous.cost via its cost(previous, wait) method, // which caused (a) double-counting when we passed prev.cost + travelTime as @@ -12,6 +15,11 @@ public TransportNode(WorldPoint point, Node previous, int travelTime) { super(net.runelite.client.plugins.microbot.shortestpath.WorldPointUtil.packWorldPoint(point), previous, (previous != null ? previous.cost : 0) + travelTime); + this.transport = transport; + } + + public Transport getTransport() { + return transport; } @Override diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/TransportPlanningPolicy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/TransportPlanningPolicy.java new file mode 100644 index 00000000000..0edf4ba9863 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/TransportPlanningPolicy.java @@ -0,0 +1,24 @@ +package net.runelite.client.plugins.microbot.shortestpath.pathfinder; + +import net.runelite.client.plugins.microbot.shortestpath.Transport; + +/** + * Engine-side admission seam for an already parsed transport catalog. + * + *

The pathfinder owns graph search, not knowledge of which interactions Microbot can execute. + * Production therefore supplies a Microbot-owned policy, while headless planner tests may admit an + * explicitly constructed catalog without depending on runtime executor classes.

+ */ +public interface TransportPlanningPolicy +{ + TransportPlanningPolicy ALLOW_ALL = transport -> true; + + /** Whether this catalog row may enter the planner graph. */ + boolean isAdmitted(Transport transport); + + /** Whether a spell row is a registered zero-rune widget action. */ + default boolean isZeroRuneSpell(Transport transport) + { + return false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/TestRunnerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/TestRunnerPlugin.java index a598cf8cd58..50ad781823f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/TestRunnerPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/TestRunnerPlugin.java @@ -1,8 +1,11 @@ package net.runelite.client.plugins.microbot.testing; import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; import net.runelite.api.GameState; -import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.gameval.InterfaceID; +import net.runelite.api.widgets.Widget; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; @@ -19,7 +22,8 @@ @PluginDescriptor( name = "Test Runner", hidden = true, - alwaysOn = true + alwaysOn = true, + priority = true ) @Slf4j public class TestRunnerPlugin extends Plugin { @@ -31,6 +35,9 @@ public class TestRunnerPlugin extends Plugin { @Inject private PluginManager pluginManager; + @Inject + private Client client; + private ScheduledExecutorService executor; private boolean testStarted = false; @@ -41,6 +48,7 @@ protected void startUp() { log.info("[TestRunner] Test mode active. Target plugin: {}", getTargetPluginName()); executor = Executors.newSingleThreadScheduledExecutor(); + prepareTargetPlugin(); enableAutoLogin(); long timeout = getTimeout(); @@ -53,6 +61,24 @@ protected void startUp() { }, timeout, TimeUnit.MILLISECONDS); } + private void prepareTargetPlugin() { + Plugin target = findTargetPlugin(); + if (target == null) { + return; + } + + try { + if (pluginManager.isActive(target)) { + pluginManager.stopPlugin(target); + } + // Test targets are process-scoped. Leaving this persisted as enabled makes them start + // before the runner can observe a playable client on the next JVM launch. + pluginManager.setPluginEnabled(target, false); + } catch (PluginInstantiationException e) { + throw new IllegalStateException("Unable to prepare target plugin " + getTargetPluginName(), e); + } + } + private void enableAutoLogin() { for (Plugin plugin : pluginManager.getPlugins()) { if (plugin instanceof AutoLoginPlugin) { @@ -90,49 +116,59 @@ protected void shutDown() { } @Subscribe - public void onGameStateChanged(GameStateChanged event) { + public void onGameTick(GameTick event) { if (!isTestMode()) return; if (testStarted) return; - if (event.getGameState() != GameState.LOGGED_IN) return; + if (!isClientReady()) return; testStarted = true; - log.info("[TestRunner] Logged in. Starting target plugin in 2s..."); + log.info("[TestRunner] Client is playable. Starting target plugin..."); - executor.schedule(this::enableTargetPlugin, 2, TimeUnit.SECONDS); + executor.execute(this::enableTargetPlugin); + } + + private boolean isClientReady() { + Widget playWidget = client.getWidget(InterfaceID.WelcomeScreen.PLAY); + boolean welcomeScreenVisible = playWidget != null && !playWidget.isHidden(); + return isClientReady(client.getGameState(), client.getLocalPlayer() != null, welcomeScreenVisible); + } + + static boolean isClientReady(GameState gameState, boolean localPlayerAvailable, boolean welcomeScreenVisible) { + return gameState == GameState.LOGGED_IN && localPlayerAvailable && !welcomeScreenVisible; } private void enableTargetPlugin() { String targetName = getTargetPluginName(); log.info("[TestRunner] Looking for plugin: '{}'", targetName); - for (Plugin plugin : pluginManager.getPlugins()) { + Plugin plugin = findTargetPlugin(); + if (plugin != null) { PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor == null) continue; - - if (descriptor.name().contains(targetName)) { - log.info("[TestRunner] Found plugin: {} ({})", descriptor.name(), plugin.getClass().getSimpleName()); - pluginManager.setPluginEnabled(plugin, true); - - SwingUtilities.invokeLater(() -> { - try { - pluginManager.startPlugin(plugin); - log.info("[TestRunner] Started plugin: {}", descriptor.name()); - } catch (PluginInstantiationException e) { - log.error("[TestRunner] Failed to start plugin", e); - TestResult result = new TestResult(targetName); - result.addError("Failed to start plugin: " + e.getMessage()); - result.complete("crash"); - TestResultWriter.write(result); - System.exit(3); - } - }); - return; - } + log.info("[TestRunner] Found plugin: {} ({})", descriptor.name(), plugin.getClass().getSimpleName()); + pluginManager.setPluginEnabled(plugin, true); + + SwingUtilities.invokeLater(() -> { + try { + boolean started = pluginManager.startPlugin(plugin); + log.info("[TestRunner] {} plugin: {}", started ? "Started" : "Target already active", descriptor.name()); + } catch (PluginInstantiationException e) { + log.error("[TestRunner] Failed to start plugin", e); + TestResult result = new TestResult(targetName); + result.addError("Failed to start plugin: " + e.getMessage()); + result.complete("crash"); + TestResultWriter.write(result); + System.exit(3); + } finally { + // Keep the target active for this process without auto-starting it on the next run. + pluginManager.setPluginEnabled(plugin, false); + } + }); + return; } log.error("[TestRunner] Plugin '{}' not found! Available plugins:", targetName); - for (Plugin plugin : pluginManager.getPlugins()) { - PluginDescriptor d = plugin.getClass().getAnnotation(PluginDescriptor.class); + for (Plugin availablePlugin : pluginManager.getPlugins()) { + PluginDescriptor d = availablePlugin.getClass().getAnnotation(PluginDescriptor.class); if (d != null) log.error(" - {}", d.name()); } @@ -143,6 +179,21 @@ private void enableTargetPlugin() { System.exit(3); } + private Plugin findTargetPlugin() { + String targetName = getTargetPluginName(); + if (targetName.isBlank()) { + return null; + } + + for (Plugin plugin : pluginManager.getPlugins()) { + PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); + if (descriptor != null && descriptor.name().contains(targetName)) { + return plugin; + } + } + return null; + } + private static boolean isTestMode() { return "true".equals(System.getProperty(PROP_TEST_MODE)); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerHarnessPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerHarnessPlugin.java index e2c48d229ac..fa367820421 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerHarnessPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerHarnessPlugin.java @@ -13,8 +13,15 @@ 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.shortestpath.PlannerSelectionMode; +import net.runelite.client.plugins.microbot.agentserver.handler.WalkerShadowHandler; import net.runelite.client.plugins.microbot.testing.TestResult; import net.runelite.client.plugins.microbot.testing.TestResultWriter; +import net.runelite.client.plugins.microbot.util.walker.Rs2PathApi; +import net.runelite.client.plugins.microbot.util.walker.Rs2PlannerShadowContext; +import net.runelite.client.plugins.microbot.util.walker.Rs2PlannerShadowCoverageStats; +import net.runelite.client.plugins.microbot.util.walker.Rs2PlannerShadowStats; +import net.runelite.client.plugins.microbot.util.walker.Rs2TransportExecutor; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import net.runelite.client.plugins.microbot.util.walker.WalkerState; @@ -48,9 +55,17 @@ public class F2PWebWalkerHarnessPlugin extends Plugin { private static final String TEST_WALK_TIMEOUT_PROPERTY = "microbot.test.webwalker.walkTimeoutMs"; private static final String USE_TELEPORTATION_SPELLS_PROPERTY = "microbot.webwalker.useTeleportationSpells"; private static final String TEST_USE_TELEPORTATION_SPELLS_PROPERTY = "microbot.test.webwalker.useTeleportationSpells"; + private static final String UPSTREAM_PLANNER_SHADOW_PROPERTY = "microbot.webwalker.upstreamPlannerShadow"; + private static final String TEST_UPSTREAM_PLANNER_SHADOW_PROPERTY = "microbot.test.webwalker.upstreamPlannerShadow"; + private static final String PLANNER_MODE_PROPERTY = "microbot.webwalker.plannerMode"; + private static final String TEST_PLANNER_MODE_PROPERTY = "microbot.test.webwalker.plannerMode"; + private static final String EXPECT_LOCAL_FALLBACK_PROPERTY = "microbot.webwalker.expectLocalFallback"; + private static final String TEST_EXPECT_LOCAL_FALLBACK_PROPERTY = "microbot.test.webwalker.expectLocalFallback"; private static final String TEST_SCRIPT_PROPERTY = "microbot.test.script"; private static final String SCRIPT_NAME = "F2P Web Walker Harness"; private static final int DEFAULT_WALK_TIMEOUT_MS = 240000; + private static final int SHADOW_SETTLE_TIMEOUT_MS = 120000; + private static final int RECOVERY_REPLAN_ACK_TIMEOUT_MS = 30000; @Inject private EventBus eventBus; @@ -94,6 +109,12 @@ private void runHarness() { result.routeFilter = property(TEST_ROUTE_FILTER_PROPERTY, ROUTE_FILTER_PROPERTY, "all"); result.stopOnFailure = Boolean.parseBoolean(property(TEST_STOP_ON_FAILURE_PROPERTY, STOP_ON_FAILURE_PROPERTY, "true")); result.walkTimeoutMs = intProperty(TEST_WALK_TIMEOUT_PROPERTY, WALK_TIMEOUT_PROPERTY, DEFAULT_WALK_TIMEOUT_MS); + boolean legacyShadow = Boolean.parseBoolean(property( + TEST_UPSTREAM_PLANNER_SHADOW_PROPERTY, UPSTREAM_PLANNER_SHADOW_PROPERTY, "false")); + result.plannerMode = plannerModeProperty(legacyShadow).name(); + result.upstreamPlannerShadow = plannerMode(result.plannerMode).comparisonEnabled(); + result.expectLocalFallback = Boolean.parseBoolean(property( + TEST_EXPECT_LOCAL_FALLBACK_PROPERTY, EXPECT_LOCAL_FALLBACK_PROPERTY, "false")); int exitCode = 0; try { @@ -115,14 +136,58 @@ private void runHarness() { break; } - applyShortestPathOverrides(route); + applyShortestPathOverrides(route, result.plannerMode); + long expectedExecutorBefore = shadowExecutorCompleted(route.expectedShadowExecutor); + long activeReplansBefore = shadowCoverageCompleted( + Rs2PlannerShadowContext.Coverage.ACTIVE_REPLAN); + long recoveryReplansBefore = shadowCoverageCompleted( + Rs2PlannerShadowContext.Coverage.RECOVERY_REPLAN); + long recoveryArrivalsBefore = shadowRecoveryArrivals(); + long bankRouteFromBankBefore = shadowCoverageCompleted( + Rs2PlannerShadowContext.Coverage.BANK_ROUTE_FROM_BANK); + long bankRouteItemGatedBefore = shadowCoverageCompleted( + Rs2PlannerShadowContext.Coverage.BANK_ROUTE_FROM_BANK_SELECTS_ITEM_GATED_TRANSPORT); + runBankRouteComparisons(route); RouteOutcome outcome = runRoute(route, result.walkTimeoutMs); result.routes.add(outcome); result.addCheck(route.id + " setup", outcome.setupPassed, outcome.setupError); result.addCheck(route.id + " route", outcome.passed, outcome.error); - if (!outcome.passed) { + boolean executorEvidencePassed = verifyExpectedShadowExecutor( + route, outcome, expectedExecutorBefore, result.upstreamPlannerShadow); + if (route.expectedShadowExecutor != null) { + result.addCheck(route.id + " shadow executor", executorEvidencePassed, + outcome.shadowExecutorError); + } + + boolean activeReplanEvidencePassed = verifyExpectedActiveReplans( + route, outcome, activeReplansBefore, result.upstreamPlannerShadow); + if (route.minimumExpectedActiveReplanComparisons > 0) { + result.addCheck(route.id + " active replans", activeReplanEvidencePassed, + outcome.activeReplanError); + } + + boolean recoveryEvidencePassed = verifyExpectedRecoveryReplans( + route, outcome, recoveryReplansBefore, recoveryArrivalsBefore, + result.upstreamPlannerShadow); + if (route.minimumExpectedRecoveryReplanComparisons > 0 + || route.minimumExpectedRecoveryArrivals > 0) { + result.addCheck(route.id + " recovery replans", recoveryEvidencePassed, + outcome.recoveryReplanError); + } + + boolean bankRouteEvidencePassed = verifyExpectedBankRoutes( + route, outcome, bankRouteFromBankBefore, bankRouteItemGatedBefore, + result.upstreamPlannerShadow); + if (route.minimumExpectedBankRouteFromBankComparisons > 0 + || route.minimumExpectedBankRouteFromBankItemGatedComparisons > 0) { + result.addCheck(route.id + " bank route comparisons", bankRouteEvidencePassed, + outcome.bankRouteComparisonError); + } + + if (!outcome.passed || !executorEvidencePassed || !activeReplanEvidencePassed + || !recoveryEvidencePassed || !bankRouteEvidencePassed) { exitCode = 1; if (result.stopOnFailure) { log.warn("[F2PWebWalkerHarness] Stopping on first failed route: {}", route.id); @@ -131,6 +196,10 @@ private void runHarness() { } } + if (!captureShadowEvidence(result)) { + exitCode = 1; + } + result.complete("completed"); writeAndExit(result, exitCode == 0 ? result.exitCode : exitCode); } catch (Throwable t) { @@ -153,6 +222,23 @@ private RouteOutcome runRoute(F2PWebWalkerRoute route, int walkTimeoutMs) { outcome.requireF2PWorld = route.requireF2PWorld; outcome.forceNoAgilityShortcuts = route.forceNoAgilityShortcuts; outcome.forceNoTeleports = route.forceNoTeleports; + outcome.forceCanoes = route.forceCanoes; + outcome.forceShips = route.forceShips; + outcome.expectedShadowExecutor = route.expectedShadowExecutor == null + ? null : route.expectedShadowExecutor.name(); + outcome.minimumExpectedShadowExecutions = route.minimumExpectedShadowExecutions; + outcome.forcedActiveReplans = route.forcedActiveReplans; + outcome.minimumExpectedActiveReplanComparisons = route.minimumExpectedActiveReplanComparisons; + outcome.forcedRecoveryReplans = route.forcedRecoveryReplans; + outcome.forcedSetupRecoveryReplans = route.forcedSetupRecoveryReplans; + outcome.minimumExpectedRecoveryReplanComparisons = + route.minimumExpectedRecoveryReplanComparisons; + outcome.minimumExpectedRecoveryArrivals = route.minimumExpectedRecoveryArrivals; + outcome.forcedBankRouteComparisons = route.forcedBankRouteComparisons; + outcome.minimumExpectedBankRouteFromBankComparisons = + route.minimumExpectedBankRouteFromBankComparisons; + outcome.minimumExpectedBankRouteFromBankItemGatedComparisons = + route.minimumExpectedBankRouteFromBankItemGatedComparisons; outcome.startedAt = Instant.now().toString(); outcome.setupPassed = true; @@ -234,7 +320,8 @@ private RouteAttemptOutcome runAttempt(F2PWebWalkerRoute route, WorldPoint start attempt.initialDistanceToStart = distance(current, start); if (attempt.initialDistanceToStart > route.startTolerance) { long setupStart = System.currentTimeMillis(); - attempt.setupState = walk(start, route.startTolerance, walkTimeoutMs); + attempt.setupState = walk(start, route.startTolerance, walkTimeoutMs, 0, + route.forcedSetupRecoveryReplans); attempt.setupDurationMs = System.currentTimeMillis() - setupStart; } else { attempt.setupState = WalkerState.ARRIVED.name(); @@ -257,7 +344,8 @@ private RouteAttemptOutcome runAttempt(F2PWebWalkerRoute route, WorldPoint start } long walkStart = System.currentTimeMillis(); - attempt.walkerState = walk(route.destination, route.destinationTolerance, walkTimeoutMs); + attempt.walkerState = walk(route.destination, route.destinationTolerance, walkTimeoutMs, + route.forcedActiveReplans, route.forcedRecoveryReplans); attempt.walkDurationMs = System.currentTimeMillis() - walkStart; WorldPoint end = safeLocation(); @@ -276,11 +364,33 @@ private RouteAttemptOutcome runAttempt(F2PWebWalkerRoute route, WorldPoint start return attempt; } - private String walk(WorldPoint destination, int tolerance, int timeoutMs) { + private String walk( + WorldPoint destination, + int tolerance, + int timeoutMs, + int forcedActiveReplans, + int forcedRecoveryReplans + ) { ExecutorService walkExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() .setNameFormat("F2PWebWalkerLeg-%d") .build()); Future future = walkExecutor.submit(() -> Rs2Walker.walkWithState(destination, tolerance)); + ExecutorService activeReplanExecutor = null; + if (forcedActiveReplans > 0) { + activeReplanExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() + .setNameFormat("F2PWebWalkerReplan-%d") + .build()); + activeReplanExecutor.submit(() -> forceActiveReplans( + future, destination, tolerance, forcedActiveReplans)); + } + ExecutorService recoveryReplanExecutor = null; + if (forcedRecoveryReplans > 0) { + recoveryReplanExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() + .setNameFormat("F2PWebWalkerRecovery-%d") + .build()); + recoveryReplanExecutor.submit(() -> forceRecoveryReplans( + future, destination, tolerance, forcedRecoveryReplans)); + } try { return future.get(timeoutMs, TimeUnit.MILLISECONDS).name(); @@ -292,17 +402,312 @@ private String walk(WorldPoint destination, int tolerance, int timeoutMs) { log.warn("[F2PWebWalkerHarness] Webwalker leg failed for destination {}", destination, e); return "ERROR:" + e.getClass().getSimpleName(); } finally { + if (activeReplanExecutor != null) { + activeReplanExecutor.shutdownNow(); + } + if (recoveryReplanExecutor != null) { + recoveryReplanExecutor.shutdownNow(); + } walkExecutor.shutdownNow(); } } + private void forceActiveReplans( + Future walkFuture, + WorldPoint destination, + int tolerance, + int count + ) { + WorldPoint lastReplanLocation = null; + for (int index = 1; index <= count && !Thread.currentThread().isInterrupted(); index++) { + WorldPoint previous = lastReplanLocation; + boolean ready = sleepUntil(() -> { + WorldPoint current = safeLocation(); + return !walkFuture.isDone() + && current != null + && current.distanceTo2D(destination) > tolerance + 10 + && Rs2PathApi.getActiveRouteStatus().isReady() + && (previous == null || current.distanceTo2D(previous) >= 4); + }, 30000); + if (!ready || walkFuture.isDone()) { + log.warn("[F2PWebWalkerHarness] Unable to trigger active replan {}/{} before route ended", + index, count); + return; + } + + long generation = Rs2PathApi.getActiveRouteStatus().getGeneration(); + lastReplanLocation = safeLocation(); + Rs2Walker.recalculatePath(); + log.info("[F2PWebWalkerHarness] Triggered active replan {}/{} at generation {}", + index, count, generation); + sleepUntil(() -> walkFuture.isDone() + || Rs2PathApi.getActiveRouteStatus().getGeneration() > generation, 10000); + } + } + + private void forceRecoveryReplans( + Future walkFuture, + WorldPoint destination, + int tolerance, + int count + ) { + WorldPoint lastReplanLocation = safeLocation(); + for (int index = 1; index <= count && !Thread.currentThread().isInterrupted(); index++) { + WorldPoint previous = lastReplanLocation; + boolean ready = sleepUntil(() -> { + WorldPoint current = safeLocation(); + return !walkFuture.isDone() + && current != null + && current.distanceTo2D(destination) > tolerance + 10 + && Rs2PathApi.getActiveRouteStatus().isReady() + && (previous == null || current.distanceTo2D(previous) >= 4); + }, 30000); + if (!ready || walkFuture.isDone()) { + log.warn("[F2PWebWalkerHarness] Unable to trigger recovery replan {}/{} before route ended", + index, count); + return; + } + + long generation = Rs2PathApi.getActiveRouteStatus().getGeneration(); + lastReplanLocation = safeLocation(); + if (!Rs2Walker.requestRecoveryReplanForTest()) { + log.warn("[F2PWebWalkerHarness] Recovery replan {}/{} was rejected at generation {}", + index, count, generation); + return; + } + log.info("[F2PWebWalkerHarness] Queued recovery replan {}/{} at generation {}", + index, count, generation); + boolean consumed = sleepUntil(() -> walkFuture.isDone() + || Rs2PathApi.getActiveRouteStatus().getGeneration() > generation, + RECOVERY_REPLAN_ACK_TIMEOUT_MS); + if (!consumed || (walkFuture.isDone() + && Rs2PathApi.getActiveRouteStatus().getGeneration() <= generation)) { + log.warn("[F2PWebWalkerHarness] Recovery replan {}/{} was not acknowledged", + index, count); + return; + } + // Measure the next progress interval from the point where this replan was consumed, + // not from where it was queued while the previous route was still moving. + lastReplanLocation = safeLocation(); + } + } + private WorldPoint safeLocation() { return lastLocation; } - private void applyShortestPathOverrides(F2PWebWalkerRoute route) { + private boolean captureShadowEvidence(WebWalkerTestResult result) { + if (!result.upstreamPlannerShadow) { + return true; + } + + result.shadowSettled = sleepUntil(() -> Rs2PathApi.getShadowStats().getPending() == 0, + SHADOW_SETTLE_TIMEOUT_MS); + Rs2PlannerShadowStats stats = Rs2PathApi.getShadowStats(); + result.shadowEvidence = WalkerShadowHandler.snapshot(); + + boolean expectedFallbackObserved = !result.expectLocalFallback + || (stats.getFailures() > 0 + && stats.getLocalFallbackFailures() > 0 + && stats.getUpstreamCanarySelections() == 0); + boolean unexpectedFailure = !result.expectLocalFallback && stats.getFailures() != 0; + boolean canarySelectionObserved = plannerMode(result.plannerMode).f2pCanaryEnabled() + && !result.expectLocalFallback + ? stats.getUpstreamCanarySelections() > 0 + : true; + boolean passed = result.shadowSettled + && stats.getSubmitted() > 0 + && stats.getCompleted() > 0 + && stats.getPending() == 0 + && stats.getDivergences() == 0 + && !unexpectedFailure + && expectedFallbackObserved + && canarySelectionObserved; + if (!passed) { + result.shadowError = "Planner shadow evidence failed: settled=" + result.shadowSettled + + ", submitted=" + stats.getSubmitted() + + ", completed=" + stats.getCompleted() + + ", pending=" + stats.getPending() + + ", divergences=" + stats.getDivergences() + + ", failures=" + stats.getFailures() + + ", upstreamCanarySelections=" + stats.getUpstreamCanarySelections() + + ", localFallbackFailures=" + stats.getLocalFallbackFailures() + + ", expectLocalFallback=" + result.expectLocalFallback; + } + result.addCheck("planner comparison and selection evidence", passed, result.shadowError); + return passed; + } + + private boolean verifyExpectedShadowExecutor( + F2PWebWalkerRoute route, + RouteOutcome outcome, + long before, + boolean upstreamPlannerShadow + ) { + if (route.expectedShadowExecutor == null) { + return true; + } + outcome.shadowExecutorSettled = upstreamPlannerShadow + && sleepUntil(() -> Rs2PathApi.getShadowStats().getPending() == 0, + SHADOW_SETTLE_TIMEOUT_MS); + long after = shadowExecutorCompleted(route.expectedShadowExecutor); + outcome.observedShadowExecutions = Math.max(0L, after - before); + boolean passed = upstreamPlannerShadow + && outcome.shadowExecutorSettled + && outcome.observedShadowExecutions >= route.minimumExpectedShadowExecutions; + if (!passed) { + outcome.shadowExecutorError = "Expected at least " + route.minimumExpectedShadowExecutions + + " completed " + route.expectedShadowExecutor + " shadow comparison(s), observed " + + outcome.observedShadowExecutions + "; shadowEnabled=" + upstreamPlannerShadow + + ", settled=" + outcome.shadowExecutorSettled; + } + return passed; + } + + private static long shadowExecutorCompleted(Rs2TransportExecutor executor) { + if (executor == null) { + return 0L; + } + return Rs2PathApi.getShadowStats().getTransportExecutors().get(executor).getCompleted(); + } + + private boolean verifyExpectedActiveReplans( + F2PWebWalkerRoute route, + RouteOutcome outcome, + long before, + boolean upstreamPlannerShadow + ) { + if (route.minimumExpectedActiveReplanComparisons == 0) { + return true; + } + outcome.activeReplansSettled = upstreamPlannerShadow + && sleepUntil(() -> Rs2PathApi.getShadowStats().getPending() == 0, + SHADOW_SETTLE_TIMEOUT_MS); + long after = shadowCoverageCompleted(Rs2PlannerShadowContext.Coverage.ACTIVE_REPLAN); + outcome.observedActiveReplanComparisons = Math.max(0L, after - before); + boolean passed = upstreamPlannerShadow + && outcome.activeReplansSettled + && outcome.observedActiveReplanComparisons + >= route.minimumExpectedActiveReplanComparisons; + if (!passed) { + outcome.activeReplanError = "Expected at least " + + route.minimumExpectedActiveReplanComparisons + + " completed ACTIVE_REPLAN shadow comparison(s), observed " + + outcome.observedActiveReplanComparisons + + "; shadowEnabled=" + upstreamPlannerShadow + + ", settled=" + outcome.activeReplansSettled; + } + return passed; + } + + private static long shadowCoverageCompleted(Rs2PlannerShadowContext.Coverage coverage) { + Rs2PlannerShadowCoverageStats stats = Rs2PathApi.getShadowStats().getCoverage().get(coverage); + return stats == null ? 0L : stats.getCompleted(); + } + + private boolean verifyExpectedRecoveryReplans( + F2PWebWalkerRoute route, + RouteOutcome outcome, + long replansBefore, + long arrivalsBefore, + boolean upstreamPlannerShadow + ) { + if (route.minimumExpectedRecoveryReplanComparisons == 0 + && route.minimumExpectedRecoveryArrivals == 0) { + return true; + } + outcome.recoveryReplansSettled = upstreamPlannerShadow + && sleepUntil(() -> Rs2PathApi.getShadowStats().getPending() == 0, + SHADOW_SETTLE_TIMEOUT_MS); + outcome.observedRecoveryReplanComparisons = Math.max(0L, + shadowCoverageCompleted(Rs2PlannerShadowContext.Coverage.RECOVERY_REPLAN) + - replansBefore); + outcome.observedRecoveryArrivals = Math.max(0L, + shadowRecoveryArrivals() - arrivalsBefore); + boolean passed = upstreamPlannerShadow + && outcome.recoveryReplansSettled + && outcome.observedRecoveryReplanComparisons + >= route.minimumExpectedRecoveryReplanComparisons + && outcome.observedRecoveryArrivals >= route.minimumExpectedRecoveryArrivals; + if (!passed) { + outcome.recoveryReplanError = "Expected at least " + + route.minimumExpectedRecoveryReplanComparisons + + " completed RECOVERY_REPLAN comparison(s) and " + + route.minimumExpectedRecoveryArrivals + + " recovered arrival(s), observed " + + outcome.observedRecoveryReplanComparisons + " and " + + outcome.observedRecoveryArrivals + + "; shadowEnabled=" + upstreamPlannerShadow + + ", settled=" + outcome.recoveryReplansSettled; + } + return passed; + } + + private static long shadowRecoveryArrivals() { + return Rs2PathApi.getShadowStats().getExecution().getRecoveryArrived(); + } + + private void runBankRouteComparisons(F2PWebWalkerRoute route) { + for (int index = 1; index <= route.forcedBankRouteComparisons; index++) { + Rs2Walker.compareRoutes(route.start, route.destination); + boolean settled = sleepUntil(() -> Rs2PathApi.getShadowStats().getPending() == 0, + SHADOW_SETTLE_TIMEOUT_MS); + if (!settled) { + log.warn("[F2PWebWalkerHarness] Bank route comparison {}/{} did not settle", + index, route.forcedBankRouteComparisons); + return; + } + log.info("[F2PWebWalkerHarness] Completed bank route comparison {}/{} for {}", + index, route.forcedBankRouteComparisons, route.id); + } + } + + private boolean verifyExpectedBankRoutes( + F2PWebWalkerRoute route, + RouteOutcome outcome, + long beforeFromBank, + long beforeItemGated, + boolean upstreamPlannerShadow + ) { + if (route.minimumExpectedBankRouteFromBankComparisons == 0 + && route.minimumExpectedBankRouteFromBankItemGatedComparisons == 0) { + return true; + } + outcome.bankRouteComparisonsSettled = upstreamPlannerShadow + && sleepUntil(() -> Rs2PathApi.getShadowStats().getPending() == 0, + SHADOW_SETTLE_TIMEOUT_MS); + outcome.observedBankRouteFromBankComparisons = Math.max(0L, + shadowCoverageCompleted(Rs2PlannerShadowContext.Coverage.BANK_ROUTE_FROM_BANK) + - beforeFromBank); + outcome.observedBankRouteFromBankItemGatedComparisons = Math.max(0L, + shadowCoverageCompleted( + Rs2PlannerShadowContext.Coverage.BANK_ROUTE_FROM_BANK_SELECTS_ITEM_GATED_TRANSPORT) + - beforeItemGated); + boolean passed = upstreamPlannerShadow + && outcome.bankRouteComparisonsSettled + && outcome.observedBankRouteFromBankComparisons + >= route.minimumExpectedBankRouteFromBankComparisons + && outcome.observedBankRouteFromBankItemGatedComparisons + >= route.minimumExpectedBankRouteFromBankItemGatedComparisons; + if (!passed) { + outcome.bankRouteComparisonError = "Expected at least " + + route.minimumExpectedBankRouteFromBankComparisons + + " BANK_ROUTE_FROM_BANK comparison(s) and " + + route.minimumExpectedBankRouteFromBankItemGatedComparisons + + " item-gated bank comparison(s), observed " + + outcome.observedBankRouteFromBankComparisons + " and " + + outcome.observedBankRouteFromBankItemGatedComparisons + + "; shadowEnabled=" + upstreamPlannerShadow + + ", settled=" + outcome.bankRouteComparisonsSettled; + } + return passed; + } + + private void applyShortestPathOverrides(F2PWebWalkerRoute route, String plannerMode) { Map config = new HashMap<>(); + config.put("plannerSelectionMode", plannerMode); + String value = property(TEST_USE_TELEPORTATION_SPELLS_PROPERTY, USE_TELEPORTATION_SPELLS_PROPERTY, ""); if (!value.isBlank()) { config.put("useTeleportationSpells", Boolean.parseBoolean(value)); @@ -322,6 +727,14 @@ private void applyShortestPathOverrides(F2PWebWalkerRoute route) { config.put("useWildernessObelisks", false); } + if (route.forceCanoes) { + config.put("useCanoes", true); + } + + if (route.forceShips) { + config.put("useShips", true); + } + if (config.isEmpty()) { return; } @@ -352,6 +765,17 @@ private static String property(String preferred, String legacy, String defaultVa return defaultValue; } + private static PlannerSelectionMode plannerModeProperty(boolean legacyShadow) { + String value = property(TEST_PLANNER_MODE_PROPERTY, PLANNER_MODE_PROPERTY, ""); + return value.isBlank() + ? (legacyShadow ? PlannerSelectionMode.SHADOW : PlannerSelectionMode.LOCAL) + : plannerMode(value); + } + + private static PlannerSelectionMode plannerMode(String value) { + return PlannerSelectionMode.fromConfigValue(value, PlannerSelectionMode.LOCAL); + } + private static int intProperty(String preferred, String legacy, int defaultValue) { String value = property(preferred, legacy, String.valueOf(defaultValue)); try { @@ -384,6 +808,12 @@ public static class WebWalkerTestResult extends TestResult { public String routeFilter; public boolean stopOnFailure; public int walkTimeoutMs; + public String plannerMode; + public boolean upstreamPlannerShadow; + public boolean expectLocalFallback; + public boolean shadowSettled; + public String shadowError; + public Map shadowEvidence; public List selectedRoutes = new ArrayList<>(); public List routes = new ArrayList<>(); @@ -406,6 +836,33 @@ public static class RouteOutcome { public boolean membersWorld; public boolean forceNoAgilityShortcuts; public boolean forceNoTeleports; + public boolean forceCanoes; + public boolean forceShips; + public String expectedShadowExecutor; + public int minimumExpectedShadowExecutions; + public int forcedActiveReplans; + public int minimumExpectedActiveReplanComparisons; + public boolean activeReplansSettled; + public long observedActiveReplanComparisons; + public String activeReplanError; + public int forcedRecoveryReplans; + public int forcedSetupRecoveryReplans; + public int minimumExpectedRecoveryReplanComparisons; + public int minimumExpectedRecoveryArrivals; + public boolean recoveryReplansSettled; + public long observedRecoveryReplanComparisons; + public long observedRecoveryArrivals; + public String recoveryReplanError; + public int forcedBankRouteComparisons; + public int minimumExpectedBankRouteFromBankComparisons; + public int minimumExpectedBankRouteFromBankItemGatedComparisons; + public boolean bankRouteComparisonsSettled; + public long observedBankRouteFromBankComparisons; + public long observedBankRouteFromBankItemGatedComparisons; + public String bankRouteComparisonError; + public boolean shadowExecutorSettled; + public long observedShadowExecutions; + public String shadowExecutorError; public String startedAt; public String finishedAt; public String initialLocation; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerRoute.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerRoute.java index 82f00b72a1f..118984a89c8 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerRoute.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/F2PWebWalkerRoute.java @@ -1,14 +1,16 @@ package net.runelite.client.plugins.microbot.testing.webwalker; import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.util.walker.Rs2TransportExecutor; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.stream.Collectors; final class F2PWebWalkerRoute { - static final List ROUTES = List.of( + static final List REQUIRED_ROUTES = List.of( route("F2P-01", "Lumbridge courtyard to Lumbridge bank", point(3222, 3218, 0), point(3208, 3220, 2), 1, 1), route("F2P-02", "Lumbridge bank to cow pen", @@ -41,9 +43,28 @@ final class F2PWebWalkerRoute { point(3109, 3341, 0), point(3106, 3363, 0), 1, 1), route("F2P-16", "Draynor Manor interior to Draynor bank", point(3106, 3363, 0), point(3092, 3245, 0), 1, 2), - currentRoute("F2P-17", "Current location to Varrock Sewers", - point(3237, 9858, 0), 1, 1, 5, true, true) + repeatedRoute("F2P-17", "Varrock manhole to Varrock Sewers", + point(3236, 3458, 0), point(3237, 9858, 0), 1, 1, 5, true, true) ); + static final List SELECTION_GATE_ROUTES = List.of( + bankSelectionGateRoute("F2P-18", "Lumbridge to Champions' Guild by canoe", + point(3243, 3237, 0), point(3199, 3344, 0), 2, 2, 3, + true, false, Rs2TransportExecutor.CANOE, 5, 10), + selectionGateRoute("F2P-19", "Port Sarim to Musa Point by ship", + point(3029, 3217, 0), point(2956, 3146, 0), 1, 1, 2, + false, true, Rs2TransportExecutor.TERMINAL_TRAVEL, 3), + activeReplanSelectionGateRoute("F2P-20", "Port Sarim to Falador with active replans", + point(3029, 3217, 0), point(2946, 3368, 0), 2, 2, 12), + recoveryReplanSelectionGateRoute("F2P-21", "Port Sarim to Rimmington with recovery replans", + point(3029, 3217, 0), point(2957, 3214, 0), 2, 2, 3, 2) + ); + static final List ROUTES; + + static { + List routes = new ArrayList<>(REQUIRED_ROUTES); + routes.addAll(SELECTION_GATE_ROUTES); + ROUTES = List.copyOf(routes); + } final String id; final String name; @@ -56,6 +77,19 @@ final class F2PWebWalkerRoute { final boolean requireF2PWorld; final boolean forceNoAgilityShortcuts; final boolean forceNoTeleports; + final boolean forceCanoes; + final boolean forceShips; + final Rs2TransportExecutor expectedShadowExecutor; + final int minimumExpectedShadowExecutions; + final int forcedActiveReplans; + final int minimumExpectedActiveReplanComparisons; + final int forcedRecoveryReplans; + final int forcedSetupRecoveryReplans; + final int minimumExpectedRecoveryReplanComparisons; + final int minimumExpectedRecoveryArrivals; + final int forcedBankRouteComparisons; + final int minimumExpectedBankRouteFromBankComparisons; + final int minimumExpectedBankRouteFromBankItemGatedComparisons; private F2PWebWalkerRoute( String id, @@ -68,7 +102,20 @@ private F2PWebWalkerRoute( boolean currentLocationStart, boolean requireF2PWorld, boolean forceNoAgilityShortcuts, - boolean forceNoTeleports + boolean forceNoTeleports, + boolean forceCanoes, + boolean forceShips, + Rs2TransportExecutor expectedShadowExecutor, + int minimumExpectedShadowExecutions, + int forcedActiveReplans, + int minimumExpectedActiveReplanComparisons, + int forcedRecoveryReplans, + int forcedSetupRecoveryReplans, + int minimumExpectedRecoveryReplanComparisons, + int minimumExpectedRecoveryArrivals, + int forcedBankRouteComparisons, + int minimumExpectedBankRouteFromBankComparisons, + int minimumExpectedBankRouteFromBankItemGatedComparisons ) { this.id = id; this.name = name; @@ -81,11 +128,25 @@ private F2PWebWalkerRoute( this.requireF2PWorld = requireF2PWorld; this.forceNoAgilityShortcuts = forceNoAgilityShortcuts; this.forceNoTeleports = forceNoTeleports; + this.forceCanoes = forceCanoes; + this.forceShips = forceShips; + this.expectedShadowExecutor = expectedShadowExecutor; + this.minimumExpectedShadowExecutions = minimumExpectedShadowExecutions; + this.forcedActiveReplans = forcedActiveReplans; + this.minimumExpectedActiveReplanComparisons = minimumExpectedActiveReplanComparisons; + this.forcedRecoveryReplans = forcedRecoveryReplans; + this.forcedSetupRecoveryReplans = forcedSetupRecoveryReplans; + this.minimumExpectedRecoveryReplanComparisons = minimumExpectedRecoveryReplanComparisons; + this.minimumExpectedRecoveryArrivals = minimumExpectedRecoveryArrivals; + this.forcedBankRouteComparisons = forcedBankRouteComparisons; + this.minimumExpectedBankRouteFromBankComparisons = minimumExpectedBankRouteFromBankComparisons; + this.minimumExpectedBankRouteFromBankItemGatedComparisons = + minimumExpectedBankRouteFromBankItemGatedComparisons; } static List selected(String routeFilter) { if (routeFilter == null || routeFilter.isBlank() || "all".equalsIgnoreCase(routeFilter)) { - return ROUTES; + return REQUIRED_ROUTES; } List requested = Arrays.stream(routeFilter.split(",")) @@ -114,12 +175,14 @@ private static F2PWebWalkerRoute route( int destinationTolerance ) { return new F2PWebWalkerRoute(id, name, start, destination, startTolerance, destinationTolerance, - 1, false, false, false, false); + 1, false, false, false, false, false, false, null, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0); } - private static F2PWebWalkerRoute currentRoute( + private static F2PWebWalkerRoute repeatedRoute( String id, String name, + WorldPoint start, WorldPoint destination, int startTolerance, int destinationTolerance, @@ -127,8 +190,78 @@ private static F2PWebWalkerRoute currentRoute( boolean forceNoAgilityShortcuts, boolean forceNoTeleports ) { - return new F2PWebWalkerRoute(id, name, null, destination, startTolerance, destinationTolerance, - repetitions, true, true, forceNoAgilityShortcuts, forceNoTeleports); + return new F2PWebWalkerRoute(id, name, start, destination, startTolerance, destinationTolerance, + repetitions, false, true, forceNoAgilityShortcuts, forceNoTeleports, false, false, null, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0); + } + + private static F2PWebWalkerRoute selectionGateRoute( + String id, + String name, + WorldPoint start, + WorldPoint destination, + int startTolerance, + int destinationTolerance, + int repetitions, + boolean forceCanoes, + boolean forceShips, + Rs2TransportExecutor expectedShadowExecutor, + int minimumExpectedShadowExecutions + ) { + return new F2PWebWalkerRoute(id, name, start, destination, startTolerance, destinationTolerance, + repetitions, false, false, true, true, forceCanoes, forceShips, expectedShadowExecutor, + minimumExpectedShadowExecutions, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } + + private static F2PWebWalkerRoute bankSelectionGateRoute( + String id, + String name, + WorldPoint start, + WorldPoint destination, + int startTolerance, + int destinationTolerance, + int repetitions, + boolean forceCanoes, + boolean forceShips, + Rs2TransportExecutor expectedShadowExecutor, + int minimumExpectedShadowExecutions, + int bankRouteComparisons + ) { + return new F2PWebWalkerRoute(id, name, start, destination, startTolerance, destinationTolerance, + repetitions, false, false, true, true, forceCanoes, forceShips, expectedShadowExecutor, + minimumExpectedShadowExecutions, 0, 0, 0, 0, 0, 0, + bankRouteComparisons, bankRouteComparisons, bankRouteComparisons); + } + + private static F2PWebWalkerRoute activeReplanSelectionGateRoute( + String id, + String name, + WorldPoint start, + WorldPoint destination, + int startTolerance, + int destinationTolerance, + int forcedActiveReplans + ) { + return new F2PWebWalkerRoute(id, name, start, destination, startTolerance, destinationTolerance, + 1, false, false, true, true, false, true, null, 0, + forcedActiveReplans, forcedActiveReplans, 0, 0, 0, 0, 0, 0, 0); + } + + private static F2PWebWalkerRoute recoveryReplanSelectionGateRoute( + String id, + String name, + WorldPoint start, + WorldPoint destination, + int startTolerance, + int destinationTolerance, + int repetitions, + int forcedRecoveryReplans + ) { + int evidenceLegs = repetitions + Math.max(0, repetitions - 1); + return new F2PWebWalkerRoute(id, name, start, destination, startTolerance, destinationTolerance, + repetitions, false, true, true, true, false, false, null, 0, + 0, 0, forcedRecoveryReplans, forcedRecoveryReplans, + evidenceLegs * forcedRecoveryReplans, evidenceLegs, 0, 0, 0); } private static WorldPoint point(int x, int y, int plane) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/GeLumbridgeTeleportHarnessPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/GeLumbridgeTeleportHarnessPlugin.java index b3661476322..feb6bed0e59 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/GeLumbridgeTeleportHarnessPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/testing/webwalker/GeLumbridgeTeleportHarnessPlugin.java @@ -12,8 +12,11 @@ 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.agentserver.handler.WalkerShadowHandler; import net.runelite.client.plugins.microbot.testing.TestResult; import net.runelite.client.plugins.microbot.testing.TestResultWriter; +import net.runelite.client.plugins.microbot.util.walker.Rs2PathApi; +import net.runelite.client.plugins.microbot.util.walker.Rs2PlannerShadowStats; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import net.runelite.client.plugins.microbot.util.walker.WalkerState; @@ -42,8 +45,11 @@ public class GeLumbridgeTeleportHarnessPlugin extends Plugin { private static final String SCRIPT_NAME = "GE Lumbridge Teleport Harness"; private static final String ITERATIONS_PROPERTY = "microbot.test.geLumbridge.iterations"; private static final String WALK_TIMEOUT_PROPERTY = "microbot.test.geLumbridge.walkTimeoutMs"; + private static final String UPSTREAM_PLANNER_SHADOW_PROPERTY = + "microbot.test.geLumbridge.upstreamPlannerShadow"; private static final int DEFAULT_ITERATIONS = 10; private static final int DEFAULT_WALK_TIMEOUT_MS = 300000; + private static final int SHADOW_SETTLE_TIMEOUT_MS = 120000; private static final WorldPoint LUMBRIDGE_CASTLE = new WorldPoint(3222, 3218, 0); private static final WorldPoint GRAND_EXCHANGE = new WorldPoint(3164, 3486, 0); private static final WorldPoint VARROCK_TELEPORT = new WorldPoint(3213, 3424, 0); @@ -101,6 +107,8 @@ private void runHarness() { GeLumbridgeTeleportResult result = new GeLumbridgeTeleportResult(SCRIPT_NAME); result.iterations = intProperty(ITERATIONS_PROPERTY, DEFAULT_ITERATIONS); result.walkTimeoutMs = intProperty(WALK_TIMEOUT_PROPERTY, DEFAULT_WALK_TIMEOUT_MS); + result.upstreamPlannerShadow = Boolean.parseBoolean( + System.getProperty(UPSTREAM_PLANNER_SHADOW_PROPERTY, "false")); int exitCode = 0; try { @@ -111,7 +119,7 @@ private void runHarness() { return; } - applyTeleportSpellOverride(); + applyShortestPathOverrides(result.upstreamPlannerShadow); log.info("[GeLumbridgeTeleportHarness] Starting {} iteration(s)", result.iterations); LegOutcome setup = runLeg("setup", 0, "setup-to-lumbridge-castle", @@ -144,6 +152,10 @@ private void runHarness() { } } + if (!captureShadowEvidence(result)) { + exitCode = 1; + } + result.complete("completed"); writeAndExit(result, exitCode == 0 ? result.exitCode : exitCode); } catch (Throwable t) { @@ -263,15 +275,44 @@ private String walk(WorldPoint destination, int tolerance, int timeoutMs, LegOut } } - private void applyTeleportSpellOverride() { + private boolean captureShadowEvidence(GeLumbridgeTeleportResult result) { + if (!result.upstreamPlannerShadow) { + return true; + } + + result.shadowSettled = sleepUntil(() -> Rs2PathApi.getShadowStats().getPending() == 0, + SHADOW_SETTLE_TIMEOUT_MS); + Rs2PlannerShadowStats stats = Rs2PathApi.getShadowStats(); + result.shadowEvidence = WalkerShadowHandler.snapshot(); + + boolean passed = result.shadowSettled + && stats.getSubmitted() > 0 + && stats.getCompleted() > 0 + && stats.getPending() == 0 + && stats.getDivergences() == 0 + && stats.getFailures() == 0; + if (!passed) { + result.shadowError = "Planner shadow evidence failed: settled=" + result.shadowSettled + + ", submitted=" + stats.getSubmitted() + + ", completed=" + stats.getCompleted() + + ", pending=" + stats.getPending() + + ", divergences=" + stats.getDivergences() + + ", failures=" + stats.getFailures(); + } + result.addCheck("upstream planner shadow", passed, result.shadowError); + return passed; + } + + private void applyShortestPathOverrides(boolean upstreamPlannerShadow) { Map config = new HashMap<>(); config.put("useTeleportationSpells", true); + config.put("plannerSelectionMode", upstreamPlannerShadow ? "SHADOW" : "LOCAL"); Map data = new HashMap<>(); data.put("config", config); eventBus.post(new PluginMessage("shortestpath", "path", data)); - log.info("[GeLumbridgeTeleportHarness] Applied shortest path override: useTeleportationSpells=true"); + log.info("[GeLumbridgeTeleportHarness] Applied shortest path overrides: {}", config); } private WorldPoint safeLocation() { @@ -328,6 +369,10 @@ private static void writeAndExit(GeLumbridgeTeleportResult result, int exitCode) public static class GeLumbridgeTeleportResult extends TestResult { public int iterations; public int walkTimeoutMs; + public boolean upstreamPlannerShadow; + public boolean shadowSettled; + public String shadowError; + public Map shadowEvidence; public List legs = new ArrayList<>(); public GeLumbridgeTeleportResult(String script) { 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 6666d71245b..9fa193c6448 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 @@ -18,8 +18,6 @@ import net.runelite.client.plugins.loottracker.LootTrackerRecord; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; -import net.runelite.client.plugins.microbot.shortestpath.ShortestPathPlugin; -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.coords.Rs2WorldPoint; @@ -41,6 +39,9 @@ import net.runelite.client.config.ConfigProfile; import net.runelite.client.plugins.microbot.util.settings.Rs2Settings; import net.runelite.client.plugins.microbot.util.tile.Rs2Tile; +import net.runelite.client.plugins.microbot.util.walker.Rs2PathApi; +import net.runelite.client.plugins.microbot.util.walker.Rs2RouteRequest; +import net.runelite.client.plugins.microbot.util.walker.Rs2RouteResult; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; @@ -2327,15 +2328,8 @@ private static AbstractMap.SimpleEntry, BankLocation> getPathAn .map(BankLocation::getWorldPoint) .collect(Collectors.toSet()); - if (ShortestPathPlugin.getPathfinderConfig().getTransports().isEmpty()) { - ShortestPathPlugin.getPathfinderConfig().refresh(); - } - - long originalStart = System.nanoTime(); - Pathfinder pf = new Pathfinder(ShortestPathPlugin.getPathfinderConfig(), worldPoint, targets); - pf.run(); - List path = pf.getPath(); - long originalTime = System.nanoTime() - originalStart; + Rs2RouteResult route = Rs2PathApi.plan(Rs2RouteRequest.toAny(worldPoint, targets)); + List path = route.getPath(); if (path.isEmpty()) { Microbot.log("Unable to find path to nearest bank"); 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 f60a2cb99b1..9b97747d0eb 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 @@ -7,8 +7,6 @@ import net.runelite.api.coords.WorldPoint; import net.runelite.api.widgets.Widget; import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.shortestpath.ShortestPathPlugin; -import net.runelite.client.plugins.microbot.shortestpath.pathfinder.Pathfinder; import net.runelite.client.plugins.microbot.util.gameobject.Rs2BankID; import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; @@ -18,6 +16,9 @@ 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.tile.Rs2Tile; +import net.runelite.client.plugins.microbot.util.walker.Rs2PathApi; +import net.runelite.client.plugins.microbot.util.walker.Rs2RouteRequest; +import net.runelite.client.plugins.microbot.util.walker.Rs2RouteResult; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; @@ -492,14 +493,8 @@ public static DepositBoxLocation getNearestDepositBox(WorldPoint worldPoint, int .map(DepositBoxLocation::getWorldPoint) .collect(Collectors.toSet()); - if (ShortestPathPlugin.getPathfinderConfig().getTransports().isEmpty()) { - ShortestPathPlugin.getPathfinderConfig().refresh(); - } - - Pathfinder pf = new Pathfinder(ShortestPathPlugin.getPathfinderConfig(), worldPoint, targets); - pf.run(); - - List path = pf.getPath(); + Rs2RouteResult route = Rs2PathApi.plan(Rs2RouteRequest.toAny(worldPoint, targets)); + List path = route.getPath(); if (path.isEmpty()) { Microbot.log("Unable to find path to any deposit box"); return null; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/leaguetransport/LeaguesTransportInjection.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/leaguetransport/LeaguesTransportInjection.java index b259fccdade..874f0e01bf6 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/leaguetransport/LeaguesTransportInjection.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/leaguetransport/LeaguesTransportInjection.java @@ -4,7 +4,6 @@ import net.runelite.client.plugins.microbot.shortestpath.Transport; import net.runelite.client.plugins.microbot.shortestpath.TransportType; import net.runelite.client.plugins.microbot.shortestpath.WorldPointUtil; -import net.runelite.client.plugins.microbot.shortestpath.pathfinder.PathfinderConfig; import net.runelite.client.plugins.microbot.util.walker.WebWalkLog; import net.runelite.client.plugins.microbot.shortestpath.PrimitiveIntHashMap; @@ -13,6 +12,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; /** * Pathfinder injection for Leagues Area and catalog transports. @@ -26,14 +26,14 @@ private LeaguesTransportInjection() private static volatile EnumSet lastInjectedUnlockedForBlacklistPrune = null; static void injectLeaguesTransports( - PathfinderConfig pathfinderConfig, + Predicate transportUsable, Rs2LeaguesTransport.LeaguesContext ctx, Set usableTeleports, Map> transports, PrimitiveIntHashMap> transportsPacked, Map typeStats) { - if (pathfinderConfig == null || ctx == null || !ctx.isActive() || ctx.getUnlockedRegions().isEmpty() + if (transportUsable == null || ctx == null || !ctx.isActive() || ctx.getUnlockedRegions().isEmpty() || usableTeleports == null || transports == null || transportsPacked == null || typeStats == null) { return; @@ -57,8 +57,8 @@ static void injectLeaguesTransports( // Uses same unlock snapshot as inject below (tickLeaguesCalibration still rate-limits standalone probes). LeaguesTransportTeleport.calibrateMissingLandingsAsync(unlockedNow); - injectLeaguesAreaTeleports(pathfinderConfig, ctx, ctx.getUnlockedRegions(), usableTeleports, typeStats); - injectLeaguesCatalogTransports(pathfinderConfig, ctx, ctx.getUnlockedRegions(), usableTeleports, transports, transportsPacked, typeStats); + injectLeaguesAreaTeleports(transportUsable, ctx.getUnlockedRegions(), usableTeleports, typeStats); + injectLeaguesCatalogTransports(transportUsable, ctx.getUnlockedRegions(), usableTeleports, transports, transportsPacked, typeStats); } private static boolean mergeOriginlessTeleportByBestDuration(Set usableTeleports, Transport candidate) @@ -90,8 +90,7 @@ private static boolean mergeOriginlessTeleportByBestDuration(Set usab } private static void injectLeaguesAreaTeleports( - PathfinderConfig pathfinderConfig, - Rs2LeaguesTransport.LeaguesContext ctx, + Predicate transportUsable, EnumSet unlockedLeaguesRegions, Set usableTeleports, Map typeStats) @@ -115,7 +114,7 @@ private static void injectLeaguesAreaTeleports( true, 31, java.util.Collections.emptySet()); - if (!pathfinderConfig.isTransportUsableWithLeaguesContext(t, ctx)) + if (!transportUsable.test(t)) { continue; } @@ -136,8 +135,7 @@ private static void injectLeaguesAreaTeleports( } private static void injectLeaguesCatalogTransports( - PathfinderConfig pathfinderConfig, - Rs2LeaguesTransport.LeaguesContext ctx, + Predicate transportUsable, EnumSet unlockedLeaguesRegions, Set usableTeleports, Map> transports, @@ -156,7 +154,7 @@ private static void injectLeaguesCatalogTransports( continue; } - if (!pathfinderConfig.isTransportUsableWithLeaguesContext(t, ctx)) + if (!transportUsable.test(t)) { continue; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/leaguetransport/Rs2LeaguesTransport.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/leaguetransport/Rs2LeaguesTransport.java index 09fdec97b98..00b935cfd7d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/leaguetransport/Rs2LeaguesTransport.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/leaguetransport/Rs2LeaguesTransport.java @@ -2,18 +2,18 @@ import lombok.extern.slf4j.Slf4j; import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.shortestpath.ShortestPathPlugin; import net.runelite.client.plugins.microbot.shortestpath.Transport; import net.runelite.client.plugins.microbot.shortestpath.TransportType; -import net.runelite.client.plugins.microbot.shortestpath.pathfinder.PathfinderConfig; import net.runelite.client.plugins.microbot.shortestpath.PrimitiveIntHashMap; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.text.Rs2TextSanitizer; +import net.runelite.client.plugins.microbot.util.walker.Rs2PathApi; import java.util.EnumSet; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.regex.Matcher; /** @@ -198,11 +198,7 @@ public static boolean isTransportAllowed(LeaguesContext ctx, Transport transport public static void invalidateContext() { - PathfinderConfig cfg = ShortestPathPlugin.pathfinderConfig; - if (cfg != null) - { - cfg.invalidateTransportRefreshCache(); - } + Rs2PathApi.invalidateTransportRefreshCache(); } public static boolean isDestinationBlacklisted(int packedWorldPoint) @@ -250,7 +246,7 @@ public static java.util.List loadCatalogTransports(EnumSet transportUsable, LeaguesContext ctx, Set usableTeleports, Map> transports, @@ -258,7 +254,7 @@ public static void injectLeaguesTransports( Map typeStats) { LeaguesTransportInjection.injectLeaguesTransports( - pathfinderConfig, ctx, usableTeleports, transports, transportsPacked, typeStats); + transportUsable, ctx, usableTeleports, transports, transportsPacked, typeStats); } public static LeaguesRegion parseRegionName(String regionNameRaw) 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 c03ee07d937..5c3b5bb813a 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 @@ -190,6 +190,25 @@ public static boolean quickCast(Spell spell) { return quickCast(spell.getMagicAction()); } + /** + * Click a zero-rune spellbook action by its exact displayed name. + * + *

Home teleports exist on every spellbook but only the standard-book variant is represented by + * {@link MagicAction}. The active spellbook and cooldown are planner requirements; at execution time + * the exact visible widget is the authoritative capability check.

+ */ + public static boolean quickCast(String spellName) { + if (spellName == null || spellName.trim().isEmpty()) return false; + + Microbot.status = "Casting " + spellName; + if (Rs2Tab.getCurrentTab() != InterfaceTab.MAGIC) { + Rs2Tab.switchToMagicTab(); + if (!sleepUntil(() -> Rs2Tab.getCurrentTab() == InterfaceTab.MAGIC)) return false; + } + + return Rs2Widget.clickWidget(spellName, Optional.of(218), 3, true); + } + public static boolean quickCast(MagicAction magicSpell) { Microbot.status = "Casting " + magicSpell.getName(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/magic/Rs2Staff.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/magic/Rs2Staff.java index 64e84005308..338f836f1f6 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/magic/Rs2Staff.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/magic/Rs2Staff.java @@ -6,8 +6,10 @@ import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -40,7 +42,10 @@ public enum Rs2Staff { MYSTIC_MUD_STAFF(ItemID.MYSTIC_MUD_STAFF, List.of(Runes.WATER, Runes.EARTH)), MYSTIC_SMOKE_STAFF(ItemID.MYSTIC_SMOKE_BATTLESTAFF, List.of(Runes.AIR, Runes.FIRE)), MYSTIC_STEAM_STAFF(ItemID.MYSTIC_STEAM_BATTLESTAFF, List.of(Runes.WATER, Runes.FIRE)), - TWINFLAME_STAFF(ItemID.TWINFLAME_STAFF, List.of(Runes.FIRE, Runes.WATER)); + TWINFLAME_STAFF(ItemID.TWINFLAME_STAFF, List.of(Runes.FIRE, Runes.WATER)), + BRYOPHYTAS_STAFF(ItemID.NATURE_STAFF_CHARGED, List.of(Runes.NATURE)), + SHADOWFLAME_QUADRANT(ItemID.SHADOWFLAME_QUADRANT, + List.of(Runes.AIR, Runes.WATER, Runes.EARTH, Runes.FIRE)); private final int itemID; private final List runes; @@ -49,7 +54,22 @@ public enum Rs2Staff { .filter(s -> s != NONE) .collect(Collectors.toMap(Rs2Staff::getItemID, Function.identity())); - static Rs2Staff byItemId(int itemID) { + public boolean provides(Runes rune) { + if (rune == null) return false; + if (runes.contains(rune)) return true; + Runes[] baseRunes = rune.getBaseRunes(); + return baseRunes.length > 0 && runes.containsAll(Arrays.asList(baseRunes)); + } + + public static Set itemIdsProviding(Runes rune) { + LinkedHashSet itemIds = Arrays.stream(values()) + .filter(staff -> staff != NONE && staff.provides(rune)) + .map(Rs2Staff::getItemID) + .collect(Collectors.toCollection(LinkedHashSet::new)); + return Collections.unmodifiableSet(itemIds); + } + + public static Rs2Staff byItemId(int itemID) { return BY_ITEM_ID.getOrDefault(itemID, NONE); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/magic/Rs2Tome.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/magic/Rs2Tome.java index 7a6fdadc740..51da5e6f34f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/magic/Rs2Tome.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/magic/Rs2Tome.java @@ -6,10 +6,13 @@ import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; + @Getter @RequiredArgsConstructor public enum Rs2Tome { @@ -27,7 +30,22 @@ public enum Rs2Tome { .filter(t -> t != NONE) .collect(Collectors.toMap(Rs2Tome::getItemID, Function.identity())); - static Rs2Tome byItemId(int itemID) { + public boolean provides(Runes rune) { + if (rune == null) return false; + if (runes.contains(rune)) return true; + Runes[] baseRunes = rune.getBaseRunes(); + return baseRunes.length > 0 && runes.containsAll(Arrays.asList(baseRunes)); + } + + public static Set itemIdsProviding(Runes rune) { + LinkedHashSet itemIds = Arrays.stream(values()) + .filter(tome -> tome != NONE && tome.provides(rune)) + .map(Rs2Tome::getItemID) + .collect(Collectors.toCollection(LinkedHashSet::new)); + return Collections.unmodifiableSet(itemIds); + } + + public static Rs2Tome byItemId(int itemID) { return BY_ITEM_ID.getOrDefault(itemID, NONE); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcManager.java index a63d7687a8b..00be6896b20 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcManager.java @@ -5,7 +5,6 @@ import com.google.gson.reflect.TypeToken; import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.shortestpath.ShortestPathPlugin; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import org.slf4j.event.Level; @@ -285,8 +284,6 @@ public static List getNpcLocations(String npcName) * @param avoidWilderness Whether to avoid locations in the Wilderness. */ public static MonsterLocation getClosestLocation(String npcName, int minClustering, boolean avoidWilderness) { - boolean originalUseBankItems = ShortestPathPlugin.getPathfinderConfig().isUseBankItems(); - ShortestPathPlugin.getPathfinderConfig().setUseBankItems(true); Microbot.log(Level.INFO,"Finding closest location for: " + npcName); List allLocations = getNpcLocations(npcName); @@ -309,9 +306,11 @@ public static MonsterLocation getClosestLocation(String npcName, int minClusteri return null; } - List centers = validLocations.stream() + List centeredLocations = validLocations.stream() + .filter(location -> location.getBestClusterCenter() != null) + .collect(Collectors.toList()); + List centers = centeredLocations.stream() .map(MonsterLocation::getBestClusterCenter) - .filter(Objects::nonNull) .collect(Collectors.toList()); if (centers.isEmpty()) { Microbot.log(Level.INFO,"Could not compute any centers for " + npcName); @@ -320,14 +319,17 @@ public static MonsterLocation getClosestLocation(String npcName, int minClusteri // 6) Find nearest and return int idx = Rs2Walker.findNearestAccessibleTarget(centers, true); - MonsterLocation closest = validLocations.get(idx); + if (idx < 0 || idx >= centeredLocations.size()) { + Microbot.log(Level.INFO,"No accessible location found for " + npcName); + return null; + } + MonsterLocation closest = centeredLocations.get(idx); if (closest.getCoords().isEmpty()) { Microbot.log(Level.INFO,"Closest location had no coords for " + npcName); return null; } Microbot.log(Level.INFO,"Closest location for " + npcName + ": " + closest.getLocationName()); - ShortestPathPlugin.getPathfinderConfig().setUseBankItems(originalUseBankItems); return closest; } @@ -342,4 +344,4 @@ public static MonsterLocation getClosestLocation(String npcName) return getClosestLocation(npcName, 1, false); } -} \ No newline at end of file +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/poh/PohTeleports.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/poh/PohTeleports.java index 4b929f5715c..d0237f71391 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/poh/PohTeleports.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/poh/PohTeleports.java @@ -9,8 +9,6 @@ import net.runelite.api.gameval.ObjectID; import net.runelite.api.widgets.Widget; import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.shortestpath.Transport; -import net.runelite.client.plugins.microbot.shortestpath.TransportType; import net.runelite.client.plugins.microbot.util.equipment.JewelleryLocationEnum; import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/skills/slayer/Rs2Slayer.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/skills/slayer/Rs2Slayer.java index 3574882938c..eacd3a83927 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/skills/slayer/Rs2Slayer.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/skills/slayer/Rs2Slayer.java @@ -5,7 +5,6 @@ import net.runelite.api.gameval.ItemID; import net.runelite.api.gameval.VarPlayerID; import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.shortestpath.ShortestPathPlugin; import net.runelite.client.plugins.microbot.shortestpath.Transport; import net.runelite.client.plugins.microbot.shortestpath.TransportType; import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; @@ -15,6 +14,8 @@ import net.runelite.client.plugins.microbot.util.npc.Rs2NpcManager; import net.runelite.client.plugins.microbot.util.skills.slayer.enums.ProtectiveEquipment; import net.runelite.client.plugins.microbot.util.skills.slayer.enums.SlayerMaster; +import net.runelite.client.plugins.microbot.util.walker.Rs2TransportEdge; +import net.runelite.client.plugins.microbot.util.walker.Rs2TransportType; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import net.runelite.client.plugins.slayer.Task; import org.jetbrains.annotations.NotNull; @@ -268,8 +269,7 @@ public static boolean walkToSlayerMaster(SlayerMaster master) { * @return the list */ public static List prepareItemTransports(WorldPoint cachedMonsterLocation) { - ShortestPathPlugin.getPathfinderConfig().setUseBankItems(true); - List transports = Rs2Walker.getTransportsForPath(Rs2Walker.getWalkPath(cachedMonsterLocation), 0) + List transports = Rs2Walker.getTransportsForDestination(cachedMonsterLocation, true) .stream() .filter(t -> t.getType() == TransportType.TELEPORTATION_ITEM || t.getType() == TransportType.FAIRY_RING) .peek(t -> { @@ -278,7 +278,6 @@ public static List prepareItemTransports(WorldPoint cachedMonsterLoca } }) .collect(Collectors.toList()); - ShortestPathPlugin.getPathfinderConfig().setUseBankItems(false); transports .forEach(t -> Microbot.log(Level.DEBUG,"Item required: " + t)); @@ -286,6 +285,23 @@ public static List prepareItemTransports(WorldPoint cachedMonsterLoca return getMissingItemTransports(transports); } + /** + * Planner-independent replacement for {@link #prepareItemTransports(WorldPoint)}. + * + *

The bank-item policy belongs to this one route request and the returned edges are the exact + * selections made by the planner. No shared planner configuration or mutable catalog rematch is + * exposed to the caller.

+ */ + public static List prepareItemTransportEdges(WorldPoint cachedMonsterLocation) { + List selected = Rs2Walker + .getTransportEdgesForDestination(cachedMonsterLocation, true) + .stream() + .filter(edge -> edge.getType() == Rs2TransportType.TELEPORTATION_ITEM + || edge.getType() == Rs2TransportType.FAIRY_RING) + .collect(Collectors.toUnmodifiableList()); + return Rs2Walker.getMissingTransportEdges(selected); + } + private static boolean hasRequiredTeleportItem(Transport transport) { if (transport.getType() == TransportType.FAIRY_RING) { return Rs2Inventory.hasItem(ItemID.DRAMEN_STAFF) || @@ -293,10 +309,8 @@ private static boolean hasRequiredTeleportItem(Transport transport) { Rs2Inventory.hasItem(ItemID.LUNAR_MOONCLAN_LIMINAL_STAFF) || Rs2Equipment.isWearing(ItemID.LUNAR_MOONCLAN_LIMINAL_STAFF); } else if (transport.getType() == TransportType.TELEPORTATION_ITEM) { - return transport.getItemIdRequirements() - .stream() - .flatMap(Collection::stream) - .anyMatch(itemId -> Rs2Equipment.isWearing(itemId) || Rs2Inventory.hasItem(itemId)); + return transport.getItemRequirements().stream() + .allMatch(requirement -> requirement.isSatisfiedBy(Rs2Slayer::carriedItemQuantity)); } return false; } @@ -316,12 +330,21 @@ private static List getMissingItemTransports(@NotNull List */ public static List getMissingItemIds(@NotNull List transports) { return transports.stream() - .flatMap(transport -> transport.getItemIdRequirements() - .stream() - .flatMap(Collection::stream) - .filter(Rs2Bank::hasItem) + .flatMap(transport -> transport.getItemRequirements().stream()) + .flatMap(requirement -> requirement.getItemIds().stream() + .filter(itemId -> Rs2Bank.count(itemId) + >= requirement.getRequiredQuantity(itemId)) .findFirst().stream()) .collect(Collectors.toList()); } + private static int carriedItemQuantity(int itemId) { + int quantity = Rs2Inventory.itemQuantity(itemId); + net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel equipped = Rs2Equipment.get(itemId); + if (equipped != null) { + quantity += Math.max(1, equipped.getQuantity()); + } + return quantity; + } + } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2ActiveRouteStatus.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2ActiveRouteStatus.java new file mode 100644 index 00000000000..354a6f22224 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2ActiveRouteStatus.java @@ -0,0 +1,152 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import net.runelite.api.coords.WorldPoint; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * Immutable, planner-independent snapshot of the route currently owned by the walker. + * + *

The generation identifies one published calculation without exposing the concrete planner. + * Callers that wait asynchronously can therefore reject a result when a newer route replaced the + * one they observed.

+ */ +public final class Rs2ActiveRouteStatus +{ + public enum Phase + { + ABSENT, + CALCULATING, + READY + } + + private final long generation; + private final Phase phase; + private final WorldPoint start; + private final Set targets; + private final List rawPath; + private final List walkablePath; + private final Rs2RouteTermination terminationReason; + private final Rs2RouteMetrics metrics; + + Rs2ActiveRouteStatus( + long generation, + Phase phase, + WorldPoint start, + Set targets, + List rawPath, + List walkablePath, + Rs2RouteTermination terminationReason, + Rs2RouteMetrics metrics) + { + if (generation < 0) + { + throw new IllegalArgumentException("generation must be non-negative"); + } + this.generation = generation; + this.phase = Objects.requireNonNull(phase, "phase"); + this.start = start; + this.targets = targets == null + ? Collections.emptySet() + : Collections.unmodifiableSet(new LinkedHashSet<>(targets)); + this.rawPath = rawPath == null ? Collections.emptyList() : List.copyOf(rawPath); + this.walkablePath = walkablePath == null + ? Collections.emptyList() + : List.copyOf(walkablePath); + this.terminationReason = terminationReason; + this.metrics = metrics; + validatePhase(); + } + + private void validatePhase() + { + if (phase == Phase.ABSENT + && (start != null || !targets.isEmpty() || !rawPath.isEmpty() || !walkablePath.isEmpty() + || terminationReason != null || metrics != null)) + { + throw new IllegalArgumentException("an absent route cannot carry planner state"); + } + if (phase == Phase.READY && (terminationReason == null || metrics == null)) + { + throw new IllegalArgumentException("a ready route must include termination and metrics"); + } + if (phase == Phase.CALCULATING && (terminationReason != null || metrics != null)) + { + throw new IllegalArgumentException("a calculating route cannot include completed search state"); + } + } + + static Rs2ActiveRouteStatus absent(long generation) + { + return new Rs2ActiveRouteStatus( + generation, Phase.ABSENT, null, Collections.emptySet(), + Collections.emptyList(), Collections.emptyList(), null, null); + } + + public long getGeneration() + { + return generation; + } + + public Phase getPhase() + { + return phase; + } + + public boolean isPresent() + { + return phase != Phase.ABSENT; + } + + public boolean isCalculating() + { + return phase == Phase.CALCULATING; + } + + public boolean isReady() + { + return phase == Phase.READY; + } + + public Optional getStart() + { + return Optional.ofNullable(start); + } + + public Set getTargets() + { + return targets; + } + + public List getRawPath() + { + return rawPath; + } + + public List getWalkablePath() + { + return walkablePath; + } + + public Optional getEndpoint() + { + return rawPath.isEmpty() + ? Optional.empty() + : Optional.of(rawPath.get(rawPath.size() - 1)); + } + + public Optional getTerminationReason() + { + return Optional.ofNullable(terminationReason); + } + + public Optional getMetrics() + { + return Optional.ofNullable(metrics); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2HotAirBalloon.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2HotAirBalloon.java new file mode 100644 index 00000000000..b1daed5d3e9 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2HotAirBalloon.java @@ -0,0 +1,114 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import net.runelite.api.GameObject; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.gameval.InterfaceID; +import net.runelite.api.gameval.ObjectID; +import net.runelite.client.plugins.microbot.shortestpath.TransportExecutionRegistry; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; +import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; + +import java.util.Optional; +import java.util.List; + +import static net.runelite.client.plugins.microbot.util.Global.sleepUntilTrue; + +/** Executes an already-unlocked hot-air-balloon network edge. */ +final class Rs2HotAirBalloon +{ + private static final int MAP_WAIT_POLL_MS = 100; + private static final int MAP_WAIT_TIMEOUT_MS = 5_000; + private static final int BASKET_SEARCH_RADIUS = 8; + private static final List BASKET_OBJECT_IDS = List.of( + ObjectID.ZEP_BASKET_ENTRANA, + ObjectID.ZEP_BASKET, + ObjectID.ZEP_MULTI_BASKET_ENTRANA, + ObjectID.ZEP_MULTI_BASKET_TAV, + ObjectID.ZEP_MULTI_BASKET_CAST, + ObjectID.ZEP_MULTI_BASKET_GNO, + ObjectID.ZEP_MULTI_BASKET_CRAFT, + ObjectID.ZEP_MULTI_BASKET_VARR); + + private Rs2HotAirBalloon() + { + } + + static boolean handle(Rs2TransportEdge transport) + { + if (transport == null || transport.getOrigin() == null) + { + return false; + } + Optional destination = + TransportExecutionRegistry.balloonDestinationFor(transport.getDisplayInfo()); + if (destination.isEmpty()) + { + return false; + } + + if (!isMapVisible()) + { + GameObject basket = findBasket(transport.getOrigin()); + if (basket == null || !Rs2GameObject.interact(basket, transport.getAction())) + { + return false; + } + if (!sleepUntilTrue(Rs2HotAirBalloon::isMapVisible, + MAP_WAIT_POLL_MS, MAP_WAIT_TIMEOUT_MS)) + { + return false; + } + } + + return Rs2Widget.clickWidget(destinationButton(destination.get())); + } + + static boolean isBasketObjectId(int objectId) + { + return BASKET_OBJECT_IDS.contains(objectId); + } + + private static GameObject findBasket(WorldPoint origin) + { + for (int objectId : BASKET_OBJECT_IDS) + { + GameObject basket = Rs2GameObject.getGameObject( + objectId, origin, BASKET_SEARCH_RADIUS); + if (basket != null) + { + return basket; + } + } + return null; + } + + static int destinationButton(TransportExecutionRegistry.BalloonDestination destination) + { + if (destination == null) + { + return -1; + } + switch (destination) + { + case CASTLE_WARS: + return InterfaceID.ZepBalloonMap.BTN_CAST; + case GRAND_TREE: + return InterfaceID.ZepBalloonMap.BTN_GNO; + case CRAFTING_GUILD: + return InterfaceID.ZepBalloonMap.BTN_CRAFT; + case ENTRANA: + return InterfaceID.ZepBalloonMap.BTN_ENT; + case TAVERLEY: + return InterfaceID.ZepBalloonMap.BTN_TAV; + case VARROCK: + return InterfaceID.ZepBalloonMap.BTN_VARR; + default: + return -1; + } + } + + private static boolean isMapVisible() + { + return Rs2Widget.isWidgetVisible(InterfaceID.ZepBalloonMap.ROOT_RECT0); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PathApi.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PathApi.java index c22ee17ba07..d480160f3f3 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PathApi.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PathApi.java @@ -1,18 +1,44 @@ package net.runelite.client.plugins.microbot.util.walker; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.shortestpath.ShortestPathPlugin; +import net.runelite.client.plugins.microbot.shortestpath.PlannerSelectionMode; import net.runelite.client.plugins.microbot.shortestpath.TeleportationItem; import net.runelite.client.plugins.microbot.shortestpath.Transport; +import net.runelite.client.plugins.microbot.shortestpath.TransportExecutionRegistry; +import net.runelite.client.plugins.microbot.shortestpath.TransportItemRequirement; +import net.runelite.client.plugins.microbot.shortestpath.TransportType; +import net.runelite.client.plugins.microbot.shortestpath.WorldPointUtil; +import net.runelite.client.plugins.microbot.shortestpath.pathfinder.CollisionMap; +import net.runelite.client.plugins.microbot.shortestpath.pathfinder.PathEdge; import net.runelite.client.plugins.microbot.shortestpath.pathfinder.Pathfinder; import net.runelite.client.plugins.microbot.shortestpath.pathfinder.PathfinderConfig; +import net.runelite.client.plugins.microbot.shortestpath.pathfinder.PathTerminationReason; +import net.runelite.client.plugins.microbot.shortestpath.pathfinder.live.LiveCollisionView; import net.runelite.client.ui.overlay.worldmap.WorldMapPoint; import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; /** * Microbot-owned facade over the shortest-path plugin's mutable static state. @@ -22,28 +48,145 @@ * Automation code ({@code Rs2Walker} and ~25 other consumers) currently reaches directly into * {@link ShortestPathPlugin}'s public static fields and accessors. Every time an upstream * (Skretzo/shortest-path) fix touches that internal wiring, the walker is at risk. Routing all - * plugin-state access through this single class freezes the surface the walker sees, so future - * upstream backports can change the plugin internals while only this facade (and not every - * consumer) has to move with them.

+ * plugin-state access through this single class confines direct static coupling, so future upstream + * backports have one compatibility seam instead of many consumers to update.

* - *

Contract. This is a thin, 1:1 delegation. Every method here forwards verbatim to - * the corresponding {@link ShortestPathPlugin} static member catalogued in the Stage 1 sweep. It - * intentionally introduces no behaviour change and holds no state of its own. The - * value types it returns ({@link Pathfinder}, {@link PathfinderConfig}, {@link Transport}, - * {@code TransportType}, {@code WorldPointUtil}) are treated as the stable Microbot-facing path API - * and are deliberately not re-wrapped — they are pure data / pure functions.

+ *

Contract. Legacy methods here retain thin delegation to the corresponding + * {@link ShortestPathPlugin} static member. New planning operations accept Microbot-owned immutable + * request/result values and keep refresh, construction, cancellation, executor ownership and temporary + * policy changes behind this seam. The + * legacy concrete accessors for {@link Pathfinder}, {@link PathfinderConfig} and {@link Transport} + * remain for binary compatibility, but new operations expose immutable route values or narrow named + * queries. That makes this a compatibility seam, not yet the final stable planning contract: active route + * state, lifecycle, synchronous queries and migrated catalog consumers no longer require concrete planner + * types outside this class.

* *

Migration status. Stage 3 is complete: every consumer under {@code microbot/util/} now * routes through this facade, so the only remaining references to {@link ShortestPathPlugin}'s - * static members outside the {@code shortestpath} package are the delegations below. That invariant - * is greppable, and is what keeps the blast radius of an upstream backport confined to this class: - *

grep -rn "ShortestPathPlugin\." microbot/util/   # expect hits in Rs2PathApi only
- * {@link ShortestPathPlugin}'s members remain public and binary-compatible for out-of-tree callers. - * Do not add logic here — if a call needs new behaviour, put it behind the plugin and expose it - * through a matching delegate.

+ * static members outside the {@code shortestpath} package are the delegations below, plus the plugin + * class literal used by {@code MicrobotPluginChoice}. That invariant is enforced by + * {@code scripts/check-shortest-path-boundary.py}. + * {@link ShortestPathPlugin}'s members remain public and binary-compatible for out-of-tree callers.

*/ public final class Rs2PathApi { + private static volatile Pathfinder activeRouteSnapshotSource; + private static volatile Rs2RouteResult activeRouteSnapshot; + private static final AtomicLong activeRouteGeneration = new AtomicLong(); + private static volatile boolean activeRouteComparisonEligible; + private static final Object shadowEvidenceMutex = new Object(); + private static final long shadowEvidenceStartedAtEpochMillis = System.currentTimeMillis(); + private static final long[][] shadowCoverageOutcomes = new long + [Rs2PlannerShadowContext.Coverage.values().length] + [Rs2PlannerShadowComparison.Status.values().length]; + private static final long[][] shadowTransportExecutorOutcomes = new long + [Rs2TransportExecutor.values().length] + [Rs2PlannerShadowComparison.Status.values().length]; + private static final long[][] shadowTransportTypeOutcomes = new long + [Rs2TransportType.values().length] + [Rs2PlannerShadowComparison.Status.values().length]; + private static long shadowGeneration; + private static long shadowSubmitted; + private static long shadowCompleted; + private static long shadowMatches; + private static long shadowDivergences; + private static long shadowFailures; + private static long shadowStaleResults; + private static long shadowDiscarded; + private static long shadowRouteShapeDifferences; + private static long upstreamCanarySelections; + private static long localFallbackDivergences; + private static long localFallbackFailures; + private static long shadowWalkerArrivals; + private static long shadowWalkerUnreachable; + private static long shadowWalkerExits; + private static long shadowRecoveryArrivals; + private static long shadowRecoveryUnreachable; + private static long shadowRecoveryExits; + private static volatile Rs2PlannerShadowComparison lastShadowComparison; + private static volatile Rs2PlannerShadowComparison lastRouteShapeDifference; + private static volatile Rs2PlannerShadowComparison lastDivergence; + private static volatile Rs2PlannerShadowComparison lastPlannerFailure; + private static final ThreadPoolExecutor SHADOW_EXECUTOR = new ThreadPoolExecutor( + 1, 1, 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(1), + new ThreadFactoryBuilder().setDaemon(true) + .setNameFormat("microbot-upstream-planner-shadow-%d").build(), + (command, executor) -> + { + if (executor.isShutdown()) + { + recordShadowDiscarded(); + return; + } + Runnable discarded = executor.getQueue().poll(); + if (discarded != null) + { + recordShadowDiscarded(); + } + if (!executor.getQueue().offer(command)) + { + recordShadowDiscarded(); + } + }); + private static final Map catalogEdgeSnapshots = + new ConcurrentHashMap<>(); + + private static final class CatalogEdgeSnapshot + { + private final Set source; + private final List edges; + + private CatalogEdgeSnapshot(Set source, List edges) + { + this.source = source; + this.edges = edges; + } + } + + private static final class PlannerComparisonTicket + { + private final long generation; + private final Rs2PlannerShadowContext context; + + private PlannerComparisonTicket(long generation, Rs2PlannerShadowContext context) + { + this.generation = generation; + this.context = context; + } + } + + private static final class PlannerEvaluation + { + private final PlannerComparisonTicket ticket; + private final Rs2RouteResult local; + private final Rs2RouteResult candidate; + private final Rs2PlannerShadowComparison comparison; + + private PlannerEvaluation( + PlannerComparisonTicket ticket, + Rs2RouteResult local, + Rs2RouteResult candidate, + Rs2PlannerShadowComparison comparison) + { + this.ticket = ticket; + this.local = local; + this.candidate = candidate; + this.comparison = comparison; + } + + private PlannerEvaluation materializationFailed(RuntimeException failure) + { + return new PlannerEvaluation( + ticket, + local, + null, + Rs2PlannerShadowComparison.failed( + comparison.getShadowEngineId(), ticket.context, + local, failure)); + } + } + private Rs2PathApi() { } @@ -66,9 +209,41 @@ public static Pathfinder getPathfinder() public static void setPathfinder(Pathfinder pathfinder) { + if (ShortestPathPlugin.getPathfinder() != pathfinder) + { + clearActiveRouteSnapshot(); + activeRouteComparisonEligible = false; + activeRouteGeneration.incrementAndGet(); + // A route replacement makes any in-flight comparison evidence stale, even when + // the replacement has shadow mode disabled and therefore submits no newer task. + synchronized (shadowEvidenceMutex) + { + shadowGeneration++; + lastShadowComparison = null; + } + } ShortestPathPlugin.setPathfinder(pathfinder); } + /** Replace the concrete compatibility view without starting a new logical route generation. */ + private static boolean replaceActivePathfinderLocked( + Pathfinder expected, Pathfinder replacement) + { + if (getPathfinder() != expected) + { + return false; + } + clearActiveRouteSnapshot(); + ShortestPathPlugin.setPathfinder(replacement); + return true; + } + + private static void clearActiveRouteSnapshot() + { + activeRouteSnapshotSource = null; + activeRouteSnapshot = null; + } + /** @return the {@link Future} tracking the in-flight pathfinding task, or {@code null}. */ public static Future getPathfinderFuture() { @@ -97,10 +272,1483 @@ public static Object getPathfinderMutex() return ShortestPathPlugin.getPathfinderMutex(); } + /** Immutable start point of the currently published route, if one exists. */ + public static Optional getActiveRouteStart() + { + return getActiveRouteStatus().getStart(); + } + + /** Immutable target snapshot of the currently published route. */ + public static Set getActiveRouteTargets() + { + return getActiveRouteStatus().getTargets(); + } + + /** + * Capture a coherent immutable view of the currently published route. + * + *

The lifecycle mutex prevents a route replacement halfway through the copy. The pathfinder may + * continue improving its partial path while calculating, but the returned lists cannot change under + * the caller.

+ */ + public static Rs2ActiveRouteStatus getActiveRouteStatus() + { + synchronized (getPathfinderMutex()) + { + long generation = activeRouteGeneration.get(); + Pathfinder source = getPathfinder(); + if (source == null) + { + return Rs2ActiveRouteStatus.absent(generation); + } + + Future activeFuture = getPathfinderFuture(); + boolean selectionComplete = activeFuture == null || activeFuture.isDone(); + boolean ready = source.isDone() && selectionComplete; + List rawPath = immutablePath(source.getPath()); + List walkablePath = ready + ? immutablePath(source.getWalkablePath()) + : rawPath; + if (!ready) + { + return new Rs2ActiveRouteStatus( + generation, + Rs2ActiveRouteStatus.Phase.CALCULATING, + source.getStart(), + source.getTargets(), + rawPath, + walkablePath, + null, + null); + } + + Pathfinder.PathfinderStats stats = source.getStats(); + Rs2RouteMetrics metrics = new Rs2RouteMetrics( + stats == null ? Rs2RouteMetrics.UNAVAILABLE : stats.getElapsedTimeNanos(), + source.getSelectedPathCost(), + stats == null ? Rs2RouteMetrics.UNAVAILABLE : stats.getNodesChecked(), + stats == null ? Rs2RouteMetrics.UNAVAILABLE : stats.getTransportsChecked()); + return new Rs2ActiveRouteStatus( + generation, + Rs2ActiveRouteStatus.Phase.READY, + source.getStart(), + source.getTargets(), + rawPath, + walkablePath, + mapTermination(source.getTerminationReason()), + metrics); + } + } + + private static List immutablePath(List path) + { + return path == null || path.isEmpty() ? Collections.emptyList() : List.copyOf(path); + } + + /** + * Cancel and unpublish the active local planner without exposing its concrete lifecycle to callers. + */ + public static void cancelAndClearActiveRoute() + { + synchronized (getPathfinderMutex()) + { + cancelAndClearActiveRouteLocked(); + } + } + + private static void cancelAndClearActiveRouteLocked() + { + Pathfinder active = getPathfinder(); + if (active != null) + { + active.cancel(); + } + Future activeFuture = getPathfinderFuture(); + if (activeFuture != null && !activeFuture.isDone()) + { + activeFuture.cancel(true); + } + setPathfinderFuture(null); + setPathfinder(null); + } + + /** + * Refresh, create and publish the active route using only Microbot-owned request policy. + * + *

The cave preference preserves the existing walker policy: calculate both the normal route and + * a walking-only route, then prefer walking when it reaches any requested target and is not longer. + * Non-cave planning remains asynchronous on the owned single-thread executor.

+ */ + public static boolean restartActiveRoute( + Rs2RouteRequest request, boolean preferWalkingOnly, int reachedDistance) + { + return restartActiveRoute( + request, preferWalkingOnly, reachedDistance, + Rs2PlannerShadowContext.Invocation.ACTIVE_ROUTE); + } + + /** + * Restart an active route with coordinate-free invocation metadata for shadow evidence. + */ + public static boolean restartActiveRoute( + Rs2RouteRequest request, + boolean preferWalkingOnly, + int reachedDistance, + Rs2PlannerShadowContext.Invocation invocation) + { + Objects.requireNonNull(request, "request"); + Objects.requireNonNull(invocation, "invocation"); + if (invocation == Rs2PlannerShadowContext.Invocation.SYNCHRONOUS_QUERY) + { + throw new IllegalArgumentException( + "an active route cannot use the synchronous-query shadow invocation"); + } + if (reachedDistance < 0) + { + throw new IllegalArgumentException("reachedDistance must be non-negative"); + } + if (request.getUseBankItems() != null) + { + throw new IllegalArgumentException( + "active asynchronous routes cannot use a temporary bank-item policy"); + } + PathfinderConfig config = getPathfinderConfig(); + if (config == null) + { + return false; + } + + synchronized (getPathfinderMutex()) + { + cancelAndClearActiveRouteLocked(); + if (shouldRefresh(request, config)) + { + config.refresh(request.getRefreshTarget()); + } + + if (preferWalkingOnly) + { + PlannerSelectionMode plannerMode = config.getPlannerSelectionMode(); + boolean comparisonEnabled = plannerMode.comparisonEnabled(); + Rs2RouteRequest normalRequest = comparisonEnabled + ? resolvePolicy(request, config) : null; + Rs2PlanningSnapshot normalSnapshot = comparisonEnabled + ? resolvePlanningSnapshot(normalRequest, config) : null; + Pathfinder normal = runLocalPlanner(config, request); + Pathfinder walkingOnly; + Rs2RouteRequest walkingRequest = null; + Rs2PlanningSnapshot walkingSnapshot = null; + try + { + config.setIgnoreTeleportAndItems(true); + if (comparisonEnabled) + { + walkingRequest = resolvePolicy(request, config); + walkingSnapshot = resolvePlanningSnapshot(walkingRequest, config); + } + walkingOnly = runLocalPlanner(config, request); + } + finally + { + config.setIgnoreTeleportAndItems(false); + } + Pathfinder selected = selectCaveRoute( + normal, walkingOnly, request.getTargets(), reachedDistance); + setPathfinder(selected); + boolean walkingSelected = selected == walkingOnly; + Rs2RouteRequest selectedRequest = walkingSelected ? walkingRequest : normalRequest; + Rs2PlanningSnapshot selectedSnapshot = walkingSelected ? walkingSnapshot : normalSnapshot; + activeRouteComparisonEligible = plannerMode == PlannerSelectionMode.SHADOW + || isF2pCanary(plannerMode, selectedRequest); + if (isF2pCanary(plannerMode, selectedRequest)) + { + Rs2RouteResult local = snapshot(selected, activeSearchNanos(selected)); + PlannerEvaluation evaluation = evaluateUpstream( + selectedRequest, selectedSnapshot, local, invocation, walkingSelected); + Pathfinder materialized = null; + if (shouldSelectUpstream(evaluation.comparison)) + { + try + { + materialized = materializeUpstreamRoute( + evaluation.candidate, config); + } + catch (RuntimeException failure) + { + evaluation = evaluation.materializationFailed(failure); + } + } + if (materialized != null) + { + replaceActivePathfinderLocked(selected, materialized); + } + recordPlannerComparison(evaluation); + recordCanaryOutcome(evaluation.comparison); + } + else if (plannerMode == PlannerSelectionMode.SHADOW) + { + submitUpstreamShadow( + selectedRequest, + selectedSnapshot, + snapshot(selected, activeSearchNanos(selected)), + invocation, + walkingSelected); + } + return true; + } + + ExecutorService executor = getPathfindingExecutor(); + if (executor == null || executor.isShutdown()) + { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat("shortest-path-%d") + .build(); + executor = Executors.newSingleThreadExecutor(threadFactory); + setPathfindingExecutor(executor); + } + PlannerSelectionMode plannerMode = config.getPlannerSelectionMode(); + boolean comparisonEnabled = plannerMode.comparisonEnabled(); + Rs2RouteRequest comparisonRequest = comparisonEnabled + ? resolvePolicy(request, config) : null; + Rs2PlanningSnapshot comparisonSnapshot = comparisonEnabled + ? resolvePlanningSnapshot(comparisonRequest, config) : null; + Pathfinder active = new Pathfinder(config, request.getStart(), request.getTargets()); + setPathfinder(active); + activeRouteComparisonEligible = plannerMode == PlannerSelectionMode.SHADOW + || isF2pCanary(plannerMode, comparisonRequest); + long routeGeneration = activeRouteGeneration.get(); + setPathfinderFuture(executor.submit(() -> + { + active.run(); + if (!comparisonEnabled) + { + return; + } + if (isF2pCanary(plannerMode, comparisonRequest)) + { + runActiveCanarySelection( + active, routeGeneration, comparisonRequest, comparisonSnapshot, + config, invocation); + return; + } + if (plannerMode != PlannerSelectionMode.SHADOW) + { + return; + } + synchronized (getPathfinderMutex()) + { + if (getPathfinder() != active + || activeRouteGeneration.get() != routeGeneration) + { + return; + } + submitUpstreamShadow( + comparisonRequest, + comparisonSnapshot, + snapshot(active, activeSearchNanos(active)), + invocation, + false); + } + })); + return true; + } + } + + private static Pathfinder runLocalPlanner(PathfinderConfig config, Rs2RouteRequest request) + { + Pathfinder pathfinder = new Pathfinder(config, request.getStart(), request.getTargets()); + pathfinder.run(); + return pathfinder; + } + + static boolean isF2pCanary( + PlannerSelectionMode plannerMode, Rs2RouteRequest request) + { + return plannerMode != null + && plannerMode.f2pCanaryEnabled() + && request != null + && request.getPolicy().map(policy -> !policy.isMembersWorld()).orElse(false); + } + + private static void runActiveCanarySelection( + Pathfinder active, + long routeGeneration, + Rs2RouteRequest request, + Rs2PlanningSnapshot planningSnapshot, + PathfinderConfig config, + Rs2PlannerShadowContext.Invocation invocation) + { + synchronized (getPathfinderMutex()) + { + if (getPathfinder() != active || activeRouteGeneration.get() != routeGeneration) + { + return; + } + } + + Rs2RouteResult local = snapshot(active, activeSearchNanos(active)); + PlannerEvaluation evaluation = evaluateUpstream( + request, planningSnapshot, local, invocation, false); + Pathfinder materialized = null; + if (shouldSelectUpstream(evaluation.comparison)) + { + try + { + materialized = materializeUpstreamRoute(evaluation.candidate, config); + } + catch (RuntimeException failure) + { + evaluation = evaluation.materializationFailed(failure); + } + } + + synchronized (getPathfinderMutex()) + { + if (getPathfinder() != active || activeRouteGeneration.get() != routeGeneration) + { + recordPlannerComparison(evaluation); + return; + } + if (materialized != null) + { + replaceActivePathfinderLocked(active, materialized); + } + recordPlannerComparison(evaluation); + recordCanaryOutcome(evaluation.comparison); + } + } + + /** Package-private selection seam for cave lifecycle policy regressions. */ + static Pathfinder selectCaveRoute( + Pathfinder normal, + Pathfinder walkingOnly, + Set targets, + int reachedDistance) + { + boolean normalAvailable = normal != null && !normal.getPath().isEmpty(); + boolean walkingAvailable = walkingOnly != null && !walkingOnly.getPath().isEmpty(); + if (!walkingAvailable) + { + return normalAvailable ? normal : walkingOnly; + } + WorldPoint endpoint = walkingOnly.getPath().get(walkingOnly.getPath().size() - 1); + boolean walkingReachesTarget = targets.stream() + .anyMatch(target -> target.getPlane() == endpoint.getPlane() + && target.distanceTo2D(endpoint) <= reachedDistance); + if (walkingReachesTarget + && normalAvailable + && normal.getPath().size() >= walkingOnly.getPath().size()) + { + return walkingOnly; + } + return normalAvailable ? normal : walkingOnly; + } + + // ------------------------------------------------------------------ + // Microbot-owned synchronous planning contract + // ------------------------------------------------------------------ + + /** + * Calculate a route without publishing it as the active walker pathfinder. + * + *

The shared configuration and its transport snapshots are mutable, so synchronous searches are + * serialized with walker start/cancel transitions. A request-level bank-item policy is temporary and + * always restored, including when refresh or pathfinding fails. This operation must run on a script or + * worker thread because refresh and search are blocking.

+ */ + public static Rs2RouteResult plan(Rs2RouteRequest request) + { + return calculate(request); + } + + private static Rs2RouteResult calculate(Rs2RouteRequest request) + { + Objects.requireNonNull(request, "request"); + if (Microbot.getClientThread() != null && Microbot.getClientThread().isClientThread()) + { + throw new IllegalStateException("synchronous route planning must not run on the client thread"); + } + PathfinderConfig config = getPathfinderConfig(); + if (config == null) + { + throw new IllegalStateException("shortest-path configuration is not initialized"); + } + + synchronized (getPathfinderMutex()) + { + Boolean requestedBankItems = request.getUseBankItems(); + boolean originalUseBankItems = config.isUseBankItems(); + boolean bankPolicyChanged = requestedBankItems != null + && requestedBankItems != originalUseBankItems; + try + { + if (bankPolicyChanged) + { + config.setUseBankItems(requestedBankItems); + } + if (shouldRefresh(request, config)) + { + config.refresh(request.getRefreshTarget()); + } + Rs2RouteRequest resolved = resolvePolicy(request, config); + Rs2PlanningSnapshot snapshot = resolvePlanningSnapshot(resolved, config); + Rs2RouteResult local = localPlanner(config).plan(resolved, snapshot); + PlannerSelectionMode plannerMode = config.getPlannerSelectionMode(); + if (isF2pCanary(plannerMode, resolved)) + { + PlannerEvaluation evaluation = evaluateUpstream( + resolved, + snapshot, + local, + Rs2PlannerShadowContext.Invocation.SYNCHRONOUS_QUERY, + false); + recordPlannerComparison(evaluation); + recordCanaryOutcome(evaluation.comparison); + return shouldSelectUpstream(evaluation.comparison) + ? evaluation.candidate : local; + } + if (plannerMode == PlannerSelectionMode.SHADOW) + { + submitUpstreamShadow( + resolved, + snapshot, + local, + Rs2PlannerShadowContext.Invocation.SYNCHRONOUS_QUERY, + false); + } + return local; + } + finally + { + if (bankPolicyChanged) + { + config.setUseBankItems(originalUseBankItems); + config.refresh(request.getRefreshTarget()); + } + } + } + } + + private static boolean shouldRefresh(Rs2RouteRequest request, PathfinderConfig config) + { + if (request.getUseBankItems() != null + || request.getRefreshPolicy() == Rs2RouteRequest.RefreshPolicy.ALWAYS) + { + return true; + } + return request.getRefreshPolicy() == Rs2RouteRequest.RefreshPolicy.IF_TRANSPORTS_EMPTY + && config.getTransports().isEmpty(); + } + + /** Package-private calculation seam for headless tests with an isolated configuration. */ + static Rs2RouteResult planWithConfig(Rs2RouteRequest request, PathfinderConfig config) + { + Rs2RouteRequest resolved = resolvePolicy(request, config); + return localPlanner(config).plan(resolved, resolvePlanningSnapshot(resolved, config)); + } + + static Rs2RoutePlanner localPlanner(PathfinderConfig config) + { + return new LocalRoutePlanner(config); + } + + static Rs2RoutePlanner upstreamPlanner() + { + UpstreamRoutePlanner delegate = new UpstreamRoutePlanner(); + if (Boolean.getBoolean("microbot.test.mode") + && Boolean.getBoolean("microbot.test.walker.forceUpstreamPlannerFailure")) + { + return new Rs2RoutePlanner() + { + @Override + public String getEngineId() + { + return delegate.getEngineId(); + } + + @Override + public Rs2RouteResult plan( + Rs2RouteRequest request, Rs2PlanningSnapshot snapshot) + { + throw new IllegalStateException("synthetic upstream rollout failure"); + } + }; + } + return delegate; + } + + public static Optional getLastShadowComparison() + { + return Optional.ofNullable(lastShadowComparison); + } + + /** + * Most recently completed semantic match whose exact walking shape differed. + * + *

Unlike {@link #getLastShadowComparison()}, this process-lifetime diagnostic survives active-route + * teardown so a settled harness snapshot can still classify a non-zero aggregate shape-difference count.

+ */ + public static Optional getLastRouteShapeDifference() + { + return Optional.ofNullable(lastRouteShapeDifference); + } + + /** + * Most recently completed semantic divergence. + * + *

This process-lifetime diagnostic deliberately survives active-route teardown. It contains only the + * coordinate-free comparison summary exposed by the shadow endpoint, so a harness can explain a non-zero + * divergence counter without retaining the player's route.

+ */ + public static Optional getLastDivergence() + { + return Optional.ofNullable(lastDivergence); + } + + /** + * Most recently completed upstream planner failure, retained for the process lifetime. + * + *

The serialized comparison exposes only the exception class name, never its message or route data.

+ */ + public static Optional getLastPlannerFailure() + { + return Optional.ofNullable(lastPlannerFailure); + } + + public static boolean isUpstreamPlannerShadowEnabled() + { + PathfinderConfig config = getPathfinderConfig(); + return config != null && config.getPlannerSelectionMode().comparisonEnabled(); + } + + public static PlannerSelectionMode getPlannerSelectionMode() + { + PathfinderConfig config = getPathfinderConfig(); + return config == null ? PlannerSelectionMode.LOCAL : config.getPlannerSelectionMode(); + } + + public static String getUpstreamPlannerEngineId() + { + return upstreamPlanner().getEngineId(); + } + + /** Process-lifetime counters for bounded, non-authoritative shadow evidence. */ + public static Rs2PlannerShadowStats getShadowStats() + { + synchronized (shadowEvidenceMutex) + { + EnumMap + coverage = new EnumMap<>(Rs2PlannerShadowContext.Coverage.class); + for (Rs2PlannerShadowContext.Coverage value + : Rs2PlannerShadowContext.Coverage.values()) + { + long[] outcomes = shadowCoverageOutcomes[value.ordinal()]; + coverage.put(value, new Rs2PlannerShadowCoverageStats( + outcomes[Rs2PlannerShadowComparison.Status.MATCH.ordinal()], + outcomes[Rs2PlannerShadowComparison.Status.DIVERGENCE.ordinal()], + outcomes[Rs2PlannerShadowComparison.Status.FAILED.ordinal()])); + } + EnumMap + transportExecutors = new EnumMap<>(Rs2TransportExecutor.class); + for (Rs2TransportExecutor value : Rs2TransportExecutor.values()) + { + long[] outcomes = shadowTransportExecutorOutcomes[value.ordinal()]; + transportExecutors.put(value, new Rs2PlannerShadowCoverageStats( + outcomes[Rs2PlannerShadowComparison.Status.MATCH.ordinal()], + outcomes[Rs2PlannerShadowComparison.Status.DIVERGENCE.ordinal()], + outcomes[Rs2PlannerShadowComparison.Status.FAILED.ordinal()])); + } + EnumMap + transportTypes = new EnumMap<>(Rs2TransportType.class); + for (Rs2TransportType value : Rs2TransportType.values()) + { + long[] outcomes = shadowTransportTypeOutcomes[value.ordinal()]; + transportTypes.put(value, new Rs2PlannerShadowCoverageStats( + outcomes[Rs2PlannerShadowComparison.Status.MATCH.ordinal()], + outcomes[Rs2PlannerShadowComparison.Status.DIVERGENCE.ordinal()], + outcomes[Rs2PlannerShadowComparison.Status.FAILED.ordinal()])); + } + return new Rs2PlannerShadowStats( + shadowSubmitted, + shadowCompleted, + shadowMatches, + shadowDivergences, + shadowFailures, + shadowStaleResults, + shadowDiscarded, + shadowRouteShapeDifferences, + upstreamCanarySelections, + localFallbackDivergences, + localFallbackFailures, + shadowEvidenceStartedAtEpochMillis, + coverage, + transportExecutors, + transportTypes, + new Rs2WalkerShadowExecutionStats( + shadowWalkerArrivals, + shadowWalkerUnreachable, + shadowWalkerExits, + shadowRecoveryArrivals, + shadowRecoveryUnreachable, + shadowRecoveryExits)); + } + } + + /** + * Whether the current logical active route was admitted to planner comparison. + * + *

This is captured when a blocking walk first consumes a generation-matched ready route because arrival + * normally clears the active route before the terminal outcome is recorded. In F2P-canary mode it is false + * for members-policy routes, even though the process-wide planner mode still has comparison capability + * enabled.

+ */ + static boolean isActiveRouteComparisonEligible() + { + return activeRouteComparisonEligible; + } + + /** Generation-matched form used when a walker consumes a previously captured active-route snapshot. */ + static boolean isActiveRouteComparisonEligible(long routeGeneration) + { + synchronized (getPathfinderMutex()) + { + return activeRouteGeneration.get() == routeGeneration + && activeRouteComparisonEligible; + } + } + + /** Record one blocking walk's terminal result for a route that actually entered planner comparison. */ + static void recordShadowWalkerOutcome( + WalkerState state, boolean recoveryTriggered, boolean comparisonEligible) + { + Objects.requireNonNull(state, "state"); + if (!comparisonEligible || state == WalkerState.MOVING) + { + return; + } + synchronized (shadowEvidenceMutex) + { + switch (state) + { + case ARRIVED: shadowWalkerArrivals++; break; + case UNREACHABLE: shadowWalkerUnreachable++; break; + case EXIT: shadowWalkerExits++; break; + default: throw new IllegalStateException("unhandled walker state " + state); + } + if (recoveryTriggered) + { + switch (state) + { + case ARRIVED: shadowRecoveryArrivals++; break; + case UNREACHABLE: shadowRecoveryUnreachable++; break; + case EXIT: shadowRecoveryExits++; break; + default: throw new IllegalStateException("unhandled walker state " + state); + } + } + } + } + + private static void recordShadowDiscarded() + { + synchronized (shadowEvidenceMutex) + { + shadowDiscarded++; + } + } + + private static void submitUpstreamShadow( + Rs2RouteRequest request, + Rs2PlanningSnapshot snapshot, + Rs2RouteResult local, + Rs2PlannerShadowContext.Invocation invocation, + boolean walkingOnlySelected) + { + Rs2PlannerShadowContext context = Rs2PlannerShadowContext.from( + invocation, walkingOnlySelected, request, local); + PlannerComparisonTicket ticket = beginPlannerComparison(context); + SHADOW_EXECUTOR.execute(() -> recordPlannerComparison( + evaluateUpstream(ticket, request, snapshot, local))); + } + + private static PlannerEvaluation evaluateUpstream( + Rs2RouteRequest request, + Rs2PlanningSnapshot snapshot, + Rs2RouteResult local, + Rs2PlannerShadowContext.Invocation invocation, + boolean walkingOnlySelected) + { + Rs2PlannerShadowContext context = Rs2PlannerShadowContext.from( + invocation, walkingOnlySelected, request, local); + return evaluateUpstream(beginPlannerComparison(context), request, snapshot, local); + } + + private static PlannerComparisonTicket beginPlannerComparison( + Rs2PlannerShadowContext context) + { + synchronized (shadowEvidenceMutex) + { + long generation = ++shadowGeneration; + shadowSubmitted++; + lastShadowComparison = null; + return new PlannerComparisonTicket(generation, context); + } + } + + private static PlannerEvaluation evaluateUpstream( + PlannerComparisonTicket ticket, + Rs2RouteRequest request, + Rs2PlanningSnapshot snapshot, + Rs2RouteResult local) + { + Rs2RoutePlanner planner = upstreamPlanner(); + try + { + Rs2RouteResult candidate = planner.plan(request, snapshot); + return new PlannerEvaluation( + ticket, + local, + candidate, + Rs2PlannerShadowComparison.compare( + planner.getEngineId(), ticket.context, local, candidate)); + } + catch (RuntimeException failure) + { + return new PlannerEvaluation( + ticket, + local, + null, + Rs2PlannerShadowComparison.failed( + planner.getEngineId(), ticket.context, local, failure)); + } + } + + private static void recordPlannerComparison(PlannerEvaluation evaluation) + { + Rs2PlannerShadowComparison comparison = evaluation.comparison; + synchronized (shadowEvidenceMutex) + { + switch (comparison.getStatus()) + { + case MATCH: shadowMatches++; break; + case DIVERGENCE: + shadowDivergences++; + lastDivergence = comparison; + break; + case FAILED: + shadowFailures++; + lastPlannerFailure = comparison; + break; + default: throw new IllegalStateException( + "unhandled shadow comparison status " + comparison.getStatus()); + } + for (Rs2PlannerShadowContext.Coverage value + : comparison.getContext().getCoverage()) + { + shadowCoverageOutcomes[value.ordinal()][comparison.getStatus().ordinal()]++; + } + for (Rs2TransportExecutor value + : comparison.getContext().getTransportExecutors()) + { + shadowTransportExecutorOutcomes[value.ordinal()] + [comparison.getStatus().ordinal()]++; + } + for (Rs2TransportType value : comparison.getContext().getTransportTypes()) + { + shadowTransportTypeOutcomes[value.ordinal()] + [comparison.getStatus().ordinal()]++; + } + if (comparison.getStatus() != Rs2PlannerShadowComparison.Status.FAILED + && !comparison.isPathMatches()) + { + shadowRouteShapeDifferences++; + lastRouteShapeDifference = comparison; + } + shadowCompleted++; + if (shadowGeneration == evaluation.ticket.generation) + { + lastShadowComparison = comparison; + } + else + { + shadowStaleResults++; + } + } + } + + private static void recordCanaryOutcome(Rs2PlannerShadowComparison comparison) + { + synchronized (shadowEvidenceMutex) + { + switch (comparison.getStatus()) + { + case MATCH: upstreamCanarySelections++; break; + case DIVERGENCE: localFallbackDivergences++; break; + case FAILED: localFallbackFailures++; break; + default: throw new IllegalStateException( + "unhandled canary comparison status " + comparison.getStatus()); + } + } + } + + /** A route-shape-only difference is a semantic match and remains eligible for the canary. */ + static boolean shouldSelectUpstream(Rs2PlannerShadowComparison comparison) + { + return comparison != null + && comparison.getStatus() == Rs2PlannerShadowComparison.Status.MATCH; + } + + static Rs2RouteRequest resolvePolicy( + Rs2RouteRequest request, PathfinderConfig config) + { + Objects.requireNonNull(request, "request"); + Objects.requireNonNull(config, "config"); + EnumSet enabledTypes = EnumSet.noneOf(Rs2TransportType.class); + for (TransportType type : config.getEnabledTransportTypes()) + { + enabledTypes.add(mapTransportType(type)); + } + Set restrictedPoints = new LinkedHashSet<>(); + for (int packed : config.getRestrictedPointsPacked()) + { + restrictedPoints.add(WorldPointUtil.unpackWorldPoint(packed)); + } + Rs2RoutePolicy policy = new Rs2RoutePolicy( + config.isUseBankItems(), + config.isAvoidWilderness(), + config.isAvoidDangerousNpcs(), + config.isIgnoreTeleportAndItems(), + Rs2Walker.disableTeleports, + config.isMembersWorld(), + config.getLiveCollisionOverlay().isEnabled(), + config.getCalculationCutoffMillis(), + config.getDistanceBeforeUsingTeleport(), + Rs2RoutePolicy.TeleportationItemMode.valueOf( + config.getTeleportationItemPolicy().name()), + enabledTypes, + restrictedPoints); + return request.withPolicy(policy); + } + + static Rs2PlanningSnapshot resolvePlanningSnapshot( + Rs2RouteRequest request, PathfinderConfig config) + { + Objects.requireNonNull(request, "request"); + Objects.requireNonNull(config, "config"); + Rs2RoutePolicy policy = request.getPolicy().orElseThrow( + () -> new IllegalArgumentException("planning snapshot requires a resolved policy")); + Set admitted = Collections.newSetFromMap(new java.util.IdentityHashMap<>()); + Map> activeTransports = config.getTransports(); + if (activeTransports != null) + { + for (Set values : activeTransports.values()) + { + admitted.addAll(values); + } + } + Set usableTeleports = config.getUsableTeleportsSnapshot(); + if (usableTeleports != null) + { + admitted.addAll(usableTeleports); + } + List edges = new ArrayList<>(admitted.size()); + for (Transport transport : admitted) + { + edges.add(toTransportEdge(transport)); + } + net.runelite.client.plugins.microbot.shortestpath.pathfinder.live.LiveCollisionOverlay overlay = + config.getLiveCollisionOverlay(); + LiveCollisionView live = overlay == null ? null : overlay.current(); + Rs2PlanningSnapshot.CollisionOverride collision = live == null ? null : live::edge; + Set blocked = config.getBlockedTransportEdgesPacked(); + return new Rs2PlanningSnapshot( + policy, + edges, + collision, + blocked == null ? Collections.emptySet() : new LinkedHashSet<>(blocked), + config::isDangerousAdjacentTile); + } + + private static final class LocalRoutePlanner implements Rs2RoutePlanner + { + private final PathfinderConfig config; + + private LocalRoutePlanner(PathfinderConfig config) + { + this.config = Objects.requireNonNull(config, "config"); + } + + @Override + public String getEngineId() + { + return "microbot-local"; + } + + @Override + public Rs2RouteResult plan(Rs2RouteRequest request, Rs2PlanningSnapshot snapshot) + { + Objects.requireNonNull(request, "request"); + Objects.requireNonNull(snapshot, "snapshot"); + Rs2RoutePolicy policy = request.getPolicy().orElseThrow( + () -> new IllegalArgumentException("route planner requires a resolved policy")); + if (snapshot.getPolicy() != policy) + { + throw new IllegalArgumentException("route request and planning snapshot policy differ"); + } + long started = System.nanoTime(); + Pathfinder pathfinder = new Pathfinder( + config, request.getStart(), request.getTargets()); + pathfinder.run(); + long elapsed = System.nanoTime() - started; + return snapshot(pathfinder, elapsed); + } + } + + /** Transitional concrete view for legacy overlays; the immutable route remains authoritative. */ + static Pathfinder materializeUpstreamRoute( + Rs2RouteResult result, PathfinderConfig config) + { + Objects.requireNonNull(result, "result"); + Objects.requireNonNull(config, "config"); + List path = result.getPath(); + List steps = result.getSteps(); + if (steps.size() != Math.max(0, path.size() - 1)) + { + throw new IllegalArgumentException( + "upstream route steps must align with the materialized path"); + } + List transportsByStep = new ArrayList<>(steps.size()); + for (int i = 0; i < steps.size(); i++) + { + Rs2RouteStep step = steps.get(i); + if (!path.get(i).equals(step.getFrom()) + || !path.get(i + 1).equals(step.getTo())) + { + throw new IllegalArgumentException( + "upstream route step endpoints must align with the materialized path"); + } + if (!step.isTransport()) + { + transportsByStep.add(null); + continue; + } + Object sourceIdentity = step.getTransport().orElseThrow( + IllegalStateException::new).getSourceIdentity(); + if (!(sourceIdentity instanceof Transport)) + { + throw new IllegalArgumentException( + "upstream transport step is missing exact local execution identity"); + } + transportsByStep.add((Transport) sourceIdentity); + } + + Rs2RouteMetrics metrics = result.getMetrics(); + return Pathfinder.completedRoute( + config, + result.getStart(), + result.getTargets(), + path, + transportsByStep, + mapTermination(result.getTerminationReason()), + metrics.getPathCost(), + metrics.getSearchNanos(), + metrics.getNodesChecked(), + metrics.getTransportsChecked(), + metrics.getLiveCollisionEdgesChecked()); + } + + /** + * Immutable view of the completed path currently published to the walker. + * + *

The source pathfinder identity is checked on every call. Replanning therefore invalidates the + * cached value even if the new route happens to contain the same points. In-flight pathfinders do not + * publish partial executor contracts.

+ */ + public static Optional getActiveRoute() + { + Pathfinder source = getPathfinder(); + Future activeFuture = getPathfinderFuture(); + if (source == null || !source.isDone() + || (activeFuture != null && !activeFuture.isDone())) + { + return Optional.empty(); + } + Rs2RouteResult snapshot = activeRouteSnapshot; + if (activeRouteSnapshotSource == source && snapshot != null) + { + return Optional.of(snapshot); + } + synchronized (getPathfinderMutex()) + { + activeFuture = getPathfinderFuture(); + if (getPathfinder() != source || !source.isDone() + || (activeFuture != null && !activeFuture.isDone())) + { + return Optional.empty(); + } + snapshot = snapshot(source, Rs2RouteMetrics.UNAVAILABLE); + activeRouteSnapshotSource = source; + activeRouteSnapshot = snapshot; + return Optional.of(snapshot); + } + } + + private static Rs2RouteResult snapshot(Pathfinder pathfinder, long elapsed) + { + Pathfinder.PathfinderStats stats = pathfinder.getStats(); + List path = pathfinder.getPath() == null + ? Collections.emptyList() + : pathfinder.getPath(); + List steps = new ArrayList<>(); + for (PathEdge edge : pathfinder.getPathEdges()) + { + if (edge.isTransport()) + { + steps.add(Rs2RouteStep.transport( + edge.getFrom(), edge.getTo(), toTransportEdge(edge.getTransport()))); + } + else + { + steps.add(Rs2RouteStep.walk(edge.getFrom(), edge.getTo())); + } + } + return new Rs2RouteResult( + pathfinder.getStart(), + pathfinder.getTargets(), + path, + steps, + mapTermination(pathfinder.getTerminationReason()), + new Rs2RouteMetrics( + elapsed, + pathfinder.getSelectedPathCost(), + stats == null ? Rs2RouteMetrics.UNAVAILABLE : stats.getNodesChecked(), + stats == null ? Rs2RouteMetrics.UNAVAILABLE : stats.getTransportsChecked(), + stats == null ? Rs2RouteMetrics.UNAVAILABLE + : stats.getLiveCollisionEdgesChecked())); + } + + private static long activeSearchNanos(Pathfinder pathfinder) + { + Pathfinder.PathfinderStats stats = pathfinder.getStats(); + return stats == null + ? Rs2RouteMetrics.UNAVAILABLE : stats.getElapsedTimeNanos(); + } + + /** + * Exact local execution selection for the runtime walker. + * + *

The immutable edge and its executor are the engine-independent contract. The local transport is + * an opaque implementation payload for the existing handlers (not a public planning value); POH in + * particular carries executable subtype behavior. It is always the exact object selected by search, + * never a catalog lookup or origin/destination rematch.

+ */ + static final class ActiveTransportSelection + { + private final int pathIndex; + private final Rs2TransportEdge edge; + private final Transport localExecutionTransport; + + private ActiveTransportSelection(int pathIndex, Rs2TransportEdge edge, Transport localExecutionTransport) + { + this.pathIndex = pathIndex; + this.edge = edge; + this.localExecutionTransport = localExecutionTransport; + } + + int getPathIndex() { return pathIndex; } + Rs2TransportEdge getEdge() { return edge; } + Rs2TransportExecutor getExecutor() { return edge.getExecutor(); } + Transport getLocalExecutionTransport() { return localExecutionTransport; } + boolean isExecutable() { return getExecutor() != Rs2TransportExecutor.UNSUPPORTED; } + } + + static Optional getActiveTransportSelection( + List expectedPath, int pathIndex) + { + List selections = getActiveTransportSelections(expectedPath); + return selections.stream() + .filter(selection -> selection.getPathIndex() == pathIndex) + .findFirst(); + } + + static List getActiveTransportSelections(List expectedPath) + { + Pathfinder source = getPathfinder(); + List selections = getTransportSelections(source, expectedPath); + return getPathfinder() == source ? selections : Collections.emptyList(); + } + + static Optional getActiveTransportEdge(WorldPoint from, WorldPoint to) + { + if (from == null || to == null) + { + return Optional.empty(); + } + return getActiveRoute().flatMap(route -> route.getTransportEdge(from, to)); + } + + /** Package-private pure seam for route-selection regressions. */ + static List getTransportSelections( + Pathfinder source, List expectedPath) + { + if (source == null || !source.isDone() || expectedPath == null) + { + return Collections.emptyList(); + } + List actualPath = source.getPath(); + if (actualPath == null || !actualPath.equals(expectedPath)) + { + return Collections.emptyList(); + } + List pathEdges = source.getPathEdges(); + if (pathEdges.size() != Math.max(0, actualPath.size() - 1)) + { + return Collections.emptyList(); + } + List selections = new ArrayList<>(); + for (int i = 0; i < pathEdges.size(); i++) + { + PathEdge pathEdge = pathEdges.get(i); + Transport selected = pathEdge.getTransport(); + if (selected == null) + { + continue; + } + if (!actualPath.get(i).equals(pathEdge.getFrom()) + || !actualPath.get(i + 1).equals(pathEdge.getTo())) + { + return Collections.emptyList(); + } + selections.add(new ActiveTransportSelection(i, toTransportEdge(selected), selected)); + } + return List.copyOf(selections); + } + + private static Rs2TransportEdge toTransportEdge(Transport transport) + { + List itemRequirements = new ArrayList<>(); + for (TransportItemRequirement requirement : transport.getItemRequirements()) + { + itemRequirements.add(new Rs2TransportItemRequirement( + requirement.getAlternatives(), + requirement.getStaffAlternatives(), + requirement.getOffhandAlternatives(), + requirement.isRuneOnly())); + } + return new Rs2TransportEdge( + transport.getOrigin(), + transport.getDestination(), + mapTransportType(transport.getType()), + mapExecutor(TransportExecutionRegistry.executorFor(transport).orElse(null)), + mapTerminalTravelMode(transport), + transport.getDisplayInfo(), + transport.getAction(), + transport.getName(), + transport.getObjectId(), + transport.getDuration(), + TransportType.isTeleport(transport.getType(), transport.getOrigin()), + transport.isConsumable(), + transport.isMembers(), + transport.getMaxWildernessLevel(), + transport.getCurrencyName(), + transport.getCurrencyAmount(), + itemRequirements, + transport); + } + + private static Rs2TerminalTravelMode mapTerminalTravelMode(Transport transport) + { + return TransportExecutionRegistry.terminalTravelModeFor(transport) + .map(mode -> Rs2TerminalTravelMode.valueOf(mode.name())) + .orElse(Rs2TerminalTravelMode.UNSUPPORTED); + } + + private static Rs2TransportExecutor mapExecutor(TransportExecutionRegistry.Executor executor) + { + if (executor == null) + { + return Rs2TransportExecutor.UNSUPPORTED; + } + try + { + return Rs2TransportExecutor.valueOf(executor.name()); + } + catch (IllegalArgumentException ignored) + { + return Rs2TransportExecutor.UNSUPPORTED; + } + } + + private static Rs2TransportType mapTransportType(TransportType type) + { + if (type == null) + { + return Rs2TransportType.UNKNOWN; + } + try + { + return Rs2TransportType.valueOf(type.name()); + } + catch (IllegalArgumentException ignored) + { + return Rs2TransportType.UNKNOWN; + } + } + + private static Rs2RouteTermination mapTermination(PathTerminationReason reason) + { + if (reason == null) + { + return Rs2RouteTermination.FAILED; + } + switch (reason) + { + case TARGET_REACHED: + return Rs2RouteTermination.TARGET_REACHED; + case SEARCH_EXHAUSTED: + return Rs2RouteTermination.SEARCH_EXHAUSTED; + case CUTOFF_REACHED: + return Rs2RouteTermination.CUTOFF_REACHED; + case CANCELLED: + return Rs2RouteTermination.CANCELLED; + case FAILED: + default: + return Rs2RouteTermination.FAILED; + } + } + + private static PathTerminationReason mapTermination(Rs2RouteTermination reason) + { + if (reason == null) + { + return PathTerminationReason.FAILED; + } + switch (reason) + { + case TARGET_REACHED: + return PathTerminationReason.TARGET_REACHED; + case SEARCH_EXHAUSTED: + return PathTerminationReason.SEARCH_EXHAUSTED; + case CUTOFF_REACHED: + return PathTerminationReason.CUTOFF_REACHED; + case CANCELLED: + return PathTerminationReason.CANCELLED; + case FAILED: + default: + return PathTerminationReason.FAILED; + } + } + // ------------------------------------------------------------------ // Config // ------------------------------------------------------------------ + /** + * Whether at least one tile within {@code distance} of {@code target} is standable in the + * configured static collision map. + * + *

This check deliberately abstains when the configuration, target or map region is absent. + * Instances and newly added regions must be left to the planner rather than rejected as blocked.

+ */ + public static boolean hasWalkableTileWithin(WorldPoint target, int distance) + { + PathfinderConfig config = getPathfinderConfig(); + return hasWalkableTileWithin(config == null ? null : config.getMap(), target, distance); + } + + /** Package-private collision seam for deterministic headless tests. */ + static boolean hasWalkableTileWithin(CollisionMap map, WorldPoint target, int distance) + { + if (map == null || target == null) + { + return true; + } + if (!map.hasRegion(target.getX(), target.getY())) + { + return true; + } + int radius = Math.max(0, distance); + for (int dx = -radius; dx <= radius; dx++) + { + for (int dy = -radius; dy <= radius; dy++) + { + int x = target.getX() + dx; + int y = target.getY() + dy; + if (!map.hasRegion(x, y) || !map.isBlocked(x, y, target.getPlane())) + { + return true; + } + } + } + return false; + } + + /** Nearest mapped standable tile to {@code target} within {@code maxRadius}, or {@code null}. */ + public static WorldPoint nearestWalkableTile(WorldPoint target, int maxRadius) + { + PathfinderConfig config = getPathfinderConfig(); + return nearestWalkableTile(config == null ? null : config.getMap(), target, maxRadius); + } + + /** Package-private collision seam for deterministic headless tests. */ + static WorldPoint nearestWalkableTile(CollisionMap map, WorldPoint target, int maxRadius) + { + if (map == null || target == null) + { + return null; + } + for (int radius = 1; radius <= Math.max(0, maxRadius); radius++) + { + for (int dx = -radius; dx <= radius; dx++) + { + for (int dy = -radius; dy <= radius; dy++) + { + if (Math.max(Math.abs(dx), Math.abs(dy)) != radius) + { + continue; + } + int x = target.getX() + dx; + int y = target.getY() + dy; + if (map.hasRegion(x, y) && !map.isBlocked(x, y, target.getPlane())) + { + return new WorldPoint(x, y, target.getPlane()); + } + } + } + } + return null; + } + + /** Refresh transport and restriction policy without exposing the mutable configuration. */ + public static boolean refreshPlanningConfiguration() + { + return refreshPlanningConfiguration(null); + } + + /** Refresh transport and restriction policy for an optional route target. */ + public static boolean refreshPlanningConfiguration(WorldPoint target) + { + PathfinderConfig config = getPathfinderConfig(); + if (config == null) + { + return false; + } + synchronized (getPathfinderMutex()) + { + config.refresh(target); + } + return true; + } + + /** Invalidate cached transport-policy snapshots so the next refresh observes external state. */ + public static boolean invalidateTransportRefreshCache() + { + PathfinderConfig config = getPathfinderConfig(); + if (config == null) + { + return false; + } + synchronized (getPathfinderMutex()) + { + config.invalidateTransportRefreshCache(); + } + return true; + } + + /** Record a stable walking edge failure in the planner's learned-block store. */ + public static boolean learnBlockedEdge(WorldPoint origin, WorldPoint destination, String reason) + { + PathfinderConfig config = getPathfinderConfig(); + if (config == null) + { + return false; + } + synchronized (getPathfinderMutex()) + { + return config.learnBlockedEdge(origin, destination, reason); + } + } + + /** Whether runtime recovery policy should avoid this dangerous-NPC adjacency tile. */ + public static boolean shouldAvoidDangerousTile(WorldPoint tile) + { + PathfinderConfig config = getPathfinderConfig(); + return config != null + && config.isAvoidDangerousNpcs() + && tile != null + && config.isDangerousAdjacentTile(WorldPointUtil.packWorldPoint(tile)); + } + + /** Whether the currently refreshed planning policy permits spirit-tree travel. */ + public static boolean isSpiritTreeTravelEnabled() + { + PathfinderConfig config = getPathfinderConfig(); + return config != null && config.isUseSpiritTrees(); + } + + /** Planner-consistent Wilderness classification without exposing its implementation class. */ + public static boolean isInWilderness(WorldPoint point) + { + return point != null && PathfinderConfig.isInWilderness(point); + } + + /** + * Whether {@code itemId} belongs to a currently known item-teleport requirement. + * + *

An empty catalog is refreshed under the lifecycle mutex before it is inspected. Additional + * compatibility IDs cover items such as fairy-ring staves that are not ordinary teleport rows.

+ */ + public static boolean isTeleportItem(int itemId, int... additionalItemIds) + { + if (additionalItemIds != null) + { + for (int additionalItemId : additionalItemIds) + { + if (itemId == additionalItemId) + { + return true; + } + } + } + PathfinderConfig config = getPathfinderConfig(); + if (config == null) + { + return false; + } + synchronized (getPathfinderMutex()) + { + if (config.getAllTransports().isEmpty()) + { + config.refresh(); + } + return config.getAllTransports().values().stream() + .flatMap(Set::stream) + .filter(transport -> TransportType.isTeleport( + transport.getType(), transport.getOrigin())) + .flatMap(transport -> transport.getItemIdRequirements().stream()) + .flatMap(Set::stream) + .anyMatch(requiredItemId -> requiredItemId == itemId); + } + } + + /** + * Switch the shared active-route policy from bank visibility to inventory/equipment visibility + * after required transport items have been withdrawn, then rebuild the target-specific catalog. + */ + public static boolean prepareInventoryOnlyRoute(WorldPoint target) + { + PathfinderConfig config = getPathfinderConfig(); + if (config == null) + { + return false; + } + synchronized (getPathfinderMutex()) + { + config.setUseBankItems(false); + config.refresh(target); + } + return true; + } + /** @return the shared pathfinder configuration (transports, restrictions, toggles). */ public static PathfinderConfig getPathfinderConfig() { @@ -172,7 +1820,115 @@ public static void setMarker(WorldMapPoint marker) // Transport data // ------------------------------------------------------------------ - /** @return the transport graph keyed by origin tile. */ + /** + * Whether the static catalog contains at least one transport keyed by {@code origin}. + * + *

This is the planner-independent query used by recovery and obstacle classification. Callers + * that only need transport presence must not consume the mutable concrete catalog.

+ */ + public static boolean hasCatalogTransportOrigin(WorldPoint origin) + { + return hasCatalogTransportOrigin(ShortestPathPlugin.getTransports(), origin); + } + + static boolean hasCatalogTransportOrigin( + Map> transports, WorldPoint origin) + { + if (transports == null || origin == null) + { + return false; + } + Set atOrigin = transports.get(origin); + return atOrigin != null && !atOrigin.isEmpty(); + } + + /** Whether the static catalog contains the exact directed {@code origin -> destination} edge. */ + public static boolean hasCatalogTransportEdge(WorldPoint origin, WorldPoint destination) + { + return hasCatalogTransportEdge(ShortestPathPlugin.getTransports(), origin, destination); + } + + static boolean hasCatalogTransportEdge( + Map> transports, + WorldPoint origin, + WorldPoint destination) + { + if (transports == null || origin == null || destination == null) + { + return false; + } + Set atOrigin = transports.get(origin); + if (atOrigin == null || atOrigin.isEmpty()) + { + return false; + } + return atOrigin.stream() + .filter(Objects::nonNull) + .anyMatch(transport -> destination.equals(transport.getDestination())); + } + + /** + * Immutable planner-independent descriptions of every catalog entry keyed by {@code origin}. + * + *

This is intended for catalog classification (for example distinguishing a door row from a + * ladder), not route execution. Runtime execution must use the exact edge selected in the active + * route so ambiguous same-endpoint transports cannot be rematched incorrectly.

+ */ + public static List getCatalogTransportEdges(WorldPoint origin) + { + if (origin == null) + { + return Collections.emptyList(); + } + Map> transports = ShortestPathPlugin.getTransports(); + Set source = transports == null ? null : transports.get(origin); + if (source == null || source.isEmpty()) + { + catalogEdgeSnapshots.remove(origin); + return Collections.emptyList(); + } + CatalogEdgeSnapshot cached = catalogEdgeSnapshots.get(origin); + if (cached != null && cached.source == source) + { + return cached.edges; + } + List edges = toCatalogTransportEdges(source); + catalogEdgeSnapshots.put(origin, new CatalogEdgeSnapshot(source, edges)); + return edges; + } + + static List getCatalogTransportEdges( + Map> transports, WorldPoint origin) + { + if (transports == null || origin == null) + { + return Collections.emptyList(); + } + return toCatalogTransportEdges(transports.get(origin)); + } + + private static List toCatalogTransportEdges(Set atOrigin) + { + if (atOrigin == null || atOrigin.isEmpty()) + { + return Collections.emptyList(); + } + List result = new ArrayList<>(atOrigin.size()); + for (Transport transport : atOrigin) + { + if (transport != null) + { + result.add(toTransportEdge(transport)); + } + } + return List.copyOf(result); + } + + /** + * Compatibility access to the concrete mutable transport graph. + * New code should use the named catalog queries above or exact immutable route steps. + */ + @Deprecated public static Map> getTransports() { return ShortestPathPlugin.getTransports(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowComparison.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowComparison.java new file mode 100644 index 00000000000..7b98e1d0531 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowComparison.java @@ -0,0 +1,118 @@ +package net.runelite.client.plugins.microbot.util.walker; + +/** Immutable summary of the latest completed production shadow comparison. */ +public final class Rs2PlannerShadowComparison +{ + public enum Status + { + MATCH, + DIVERGENCE, + FAILED + } + + private final Status status; + private final String shadowEngineId; + private final Rs2PlannerShadowContext context; + private final boolean terminationMatches; + private final boolean endpointMatches; + private final boolean costComparable; + private final boolean costMatches; + private final boolean selectedTransportsMatch; + private final boolean pathMatches; + private final long shadowSearchNanos; + private final long localSearchNanos; + private final String failureType; + + private Rs2PlannerShadowComparison( + Status status, + String shadowEngineId, + Rs2PlannerShadowContext context, + boolean terminationMatches, + boolean endpointMatches, + boolean costComparable, + boolean costMatches, + boolean selectedTransportsMatch, + boolean pathMatches, + long shadowSearchNanos, + long localSearchNanos, + String failureType) + { + this.status = status; + this.shadowEngineId = shadowEngineId; + this.context = context; + this.terminationMatches = terminationMatches; + this.endpointMatches = endpointMatches; + this.costComparable = costComparable; + this.costMatches = costMatches; + this.selectedTransportsMatch = selectedTransportsMatch; + this.pathMatches = pathMatches; + this.shadowSearchNanos = shadowSearchNanos; + this.localSearchNanos = localSearchNanos; + this.failureType = failureType; + } + + static Rs2PlannerShadowComparison compare( + String engineId, + Rs2PlannerShadowContext context, + Rs2RouteResult local, + Rs2RouteResult shadow) + { + boolean termination = local.getTerminationReason() == shadow.getTerminationReason(); + boolean endpoint = local.getEndpoint().equals(shadow.getEndpoint()); + boolean comparable = local.getMetrics().hasPathCost() && shadow.getMetrics().hasPathCost(); + boolean cost = comparable + && local.getMetrics().getPathCost() == shadow.getMetrics().getPathCost(); + boolean transports = sameSelectedTransports(local, shadow); + boolean path = local.getPath().equals(shadow.getPath()); + Status status = termination && endpoint && comparable && cost && transports + ? Status.MATCH : Status.DIVERGENCE; + return new Rs2PlannerShadowComparison( + status, engineId, context, termination, endpoint, comparable, cost, transports, path, + shadow.getMetrics().getSearchNanos(), local.getMetrics().getSearchNanos(), null); + } + + static Rs2PlannerShadowComparison failed( + String engineId, Rs2PlannerShadowContext context, Rs2RouteResult local, + RuntimeException failure) + { + return new Rs2PlannerShadowComparison( + Status.FAILED, engineId, context, false, false, false, false, false, false, + Rs2RouteMetrics.UNAVAILABLE, local.getMetrics().getSearchNanos(), + failure.getClass().getSimpleName()); + } + + private static boolean sameSelectedTransports(Rs2RouteResult local, Rs2RouteResult shadow) + { + java.util.List localSteps = local.getTransportSteps(); + java.util.List shadowSteps = shadow.getTransportSteps(); + if (localSteps.size() != shadowSteps.size()) + { + return false; + } + for (int i = 0; i < localSteps.size(); i++) + { + Rs2TransportEdge localEdge = localSteps.get(i).getTransport().orElseThrow( + IllegalStateException::new); + Rs2TransportEdge shadowEdge = shadowSteps.get(i).getTransport().orElseThrow( + IllegalStateException::new); + if (localEdge.getSourceIdentity() != shadowEdge.getSourceIdentity()) + { + return false; + } + } + return true; + } + + public Status getStatus() { return status; } + public String getShadowEngineId() { return shadowEngineId; } + public Rs2PlannerShadowContext getContext() { return context; } + public boolean isTerminationMatches() { return terminationMatches; } + public boolean isEndpointMatches() { return endpointMatches; } + public boolean isCostComparable() { return costComparable; } + public boolean isCostMatches() { return costMatches; } + public boolean isSelectedTransportsMatch() { return selectedTransportsMatch; } + public boolean isPathMatches() { return pathMatches; } + public long getShadowSearchNanos() { return shadowSearchNanos; } + public long getLocalSearchNanos() { return localSearchNanos; } + public String getFailureType() { return failureType; } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowContext.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowContext.java new file mode 100644 index 00000000000..32247ae16d8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowContext.java @@ -0,0 +1,165 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import net.runelite.api.coords.WorldPoint; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Objects; +import java.util.Set; + +/** Coordinate-free route classification attached to one production shadow comparison. */ +public final class Rs2PlannerShadowContext +{ + public enum Invocation + { + SYNCHRONOUS_QUERY, + ACTIVE_ROUTE, + ACTIVE_REPLAN, + RECOVERY_REPLAN + } + + public enum Coverage + { + SYNCHRONOUS_QUERY, + ACTIVE_ROUTE, + ACTIVE_REPLAN, + RECOVERY_REPLAN, + SURFACE_COORDINATES_ONLY, + UNDERGROUND_COORDINATES, + WALKING_ONLY_SELECTED, + USES_TRANSPORT, + SELECTS_ITEM_GATED_TRANSPORT, + BANK_ITEMS_ENABLED, + BANK_ROUTE_DIRECT, + BANK_ROUTE_TO_BANK, + BANK_ROUTE_FROM_BANK, + BANK_ROUTE_FROM_BANK_SELECTS_ITEM_GATED_TRANSPORT, + LIVE_COLLISION_ENABLED, + LIVE_COLLISION_CONSULTED + } + + private static final int UNDERGROUND_Y = 6400; + + private final Invocation invocation; + private final Set coverage; + private final Set transportExecutors; + private final Set transportTypes; + + private Rs2PlannerShadowContext( + Invocation invocation, + Set coverage, + Set transportExecutors, + Set transportTypes) + { + this.invocation = Objects.requireNonNull(invocation, "invocation"); + this.coverage = Collections.unmodifiableSet(EnumSet.copyOf(coverage)); + this.transportExecutors = Collections.unmodifiableSet( + EnumSet.copyOf(transportExecutors)); + this.transportTypes = Collections.unmodifiableSet(EnumSet.copyOf(transportTypes)); + } + + static Rs2PlannerShadowContext from( + Invocation invocation, + boolean walkingOnlySelected, + Rs2RouteRequest request, + Rs2RouteResult local) + { + Objects.requireNonNull(request, "request"); + Objects.requireNonNull(local, "local"); + Rs2RoutePolicy policy = request.getPolicy().orElseThrow( + () -> new IllegalArgumentException("shadow context requires resolved policy")); + EnumSet coverage = EnumSet.noneOf(Coverage.class); + switch (invocation) + { + case SYNCHRONOUS_QUERY: coverage.add(Coverage.SYNCHRONOUS_QUERY); break; + case ACTIVE_ROUTE: coverage.add(Coverage.ACTIVE_ROUTE); break; + case ACTIVE_REPLAN: coverage.add(Coverage.ACTIVE_REPLAN); break; + case RECOVERY_REPLAN: coverage.add(Coverage.RECOVERY_REPLAN); break; + default: throw new IllegalStateException("unhandled invocation " + invocation); + } + boolean underground = isUnderground(request.getStart()) + || request.getTargets().stream().anyMatch(Rs2PlannerShadowContext::isUnderground) + || local.getPath().stream().anyMatch(Rs2PlannerShadowContext::isUnderground); + coverage.add(underground + ? Coverage.UNDERGROUND_COORDINATES : Coverage.SURFACE_COORDINATES_ONLY); + if (walkingOnlySelected) + { + coverage.add(Coverage.WALKING_ONLY_SELECTED); + } + EnumSet transportExecutors = + EnumSet.noneOf(Rs2TransportExecutor.class); + EnumSet transportTypes = EnumSet.noneOf(Rs2TransportType.class); + boolean itemGatedTransport = false; + for (Rs2RouteStep step : local.getTransportSteps()) + { + Rs2TransportEdge edge = step.getTransport().orElseThrow(IllegalStateException::new); + transportExecutors.add(edge.getExecutor()); + transportTypes.add(edge.getType()); + itemGatedTransport |= !edge.getItemRequirements().isEmpty() + || edge.getCurrencyAmount() > 0; + } + if (!transportExecutors.isEmpty()) + { + coverage.add(Coverage.USES_TRANSPORT); + } + if (itemGatedTransport) + { + coverage.add(Coverage.SELECTS_ITEM_GATED_TRANSPORT); + } + if (policy.isUseBankItems()) + { + coverage.add(Coverage.BANK_ITEMS_ENABLED); + } + switch (request.getPurpose()) + { + case GENERAL: break; + case BANK_ROUTE_DIRECT: coverage.add(Coverage.BANK_ROUTE_DIRECT); break; + case BANK_ROUTE_TO_BANK: coverage.add(Coverage.BANK_ROUTE_TO_BANK); break; + case BANK_ROUTE_FROM_BANK: + coverage.add(Coverage.BANK_ROUTE_FROM_BANK); + if (itemGatedTransport) + { + coverage.add(Coverage.BANK_ROUTE_FROM_BANK_SELECTS_ITEM_GATED_TRANSPORT); + } + break; + default: throw new IllegalStateException( + "unhandled route request purpose " + request.getPurpose()); + } + if (policy.isLiveCollisionEnabled()) + { + coverage.add(Coverage.LIVE_COLLISION_ENABLED); + } + if (local.getMetrics().hasLiveCollisionEdgesChecked() + && local.getMetrics().getLiveCollisionEdgesChecked() > 0L) + { + coverage.add(Coverage.LIVE_COLLISION_CONSULTED); + } + return new Rs2PlannerShadowContext( + invocation, coverage, transportExecutors, transportTypes); + } + + private static boolean isUnderground(WorldPoint point) + { + return point != null && point.getY() >= UNDERGROUND_Y; + } + + public Invocation getInvocation() + { + return invocation; + } + + public Set getCoverage() + { + return coverage; + } + + public Set getTransportExecutors() + { + return transportExecutors; + } + + public Set getTransportTypes() + { + return transportTypes; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowCoverageStats.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowCoverageStats.java new file mode 100644 index 00000000000..766e25d179c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowCoverageStats.java @@ -0,0 +1,32 @@ +package net.runelite.client.plugins.microbot.util.walker; + +/** Immutable outcome counters for one overlapping shadow-evidence coverage tag. */ +public final class Rs2PlannerShadowCoverageStats +{ + private final long completed; + private final long matches; + private final long divergences; + private final long failures; + + Rs2PlannerShadowCoverageStats(long matches, long divergences, long failures) + { + this.matches = requireNonNegative(matches, "matches"); + this.divergences = requireNonNegative(divergences, "divergences"); + this.failures = requireNonNegative(failures, "failures"); + this.completed = Math.addExact(Math.addExact(matches, divergences), failures); + } + + private static long requireNonNegative(long value, String name) + { + if (value < 0) + { + throw new IllegalArgumentException(name + " must be non-negative"); + } + return value; + } + + public long getCompleted() { return completed; } + public long getMatches() { return matches; } + public long getDivergences() { return divergences; } + public long getFailures() { return failures; } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowStats.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowStats.java new file mode 100644 index 00000000000..7b28515aca3 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlannerShadowStats.java @@ -0,0 +1,120 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; + +/** Immutable process-lifetime aggregate for production planner shadow evidence. */ +public final class Rs2PlannerShadowStats +{ + private final long submitted; + private final long completed; + private final long matches; + private final long divergences; + private final long failures; + private final long staleResults; + private final long discarded; + private final long routeShapeDifferences; + private final long upstreamCanarySelections; + private final long localFallbackDivergences; + private final long localFallbackFailures; + private final long startedAtEpochMillis; + private final Map coverage; + private final Map transportExecutors; + private final Map transportTypes; + private final Rs2WalkerShadowExecutionStats execution; + + Rs2PlannerShadowStats( + long submitted, + long completed, + long matches, + long divergences, + long failures, + long staleResults, + long discarded, + long routeShapeDifferences, + long upstreamCanarySelections, + long localFallbackDivergences, + long localFallbackFailures, + long startedAtEpochMillis, + Map coverage, + Map transportExecutors, + Map transportTypes, + Rs2WalkerShadowExecutionStats execution) + { + this.submitted = requireNonNegative(submitted, "submitted"); + this.completed = requireNonNegative(completed, "completed"); + this.matches = requireNonNegative(matches, "matches"); + this.divergences = requireNonNegative(divergences, "divergences"); + this.failures = requireNonNegative(failures, "failures"); + this.staleResults = requireNonNegative(staleResults, "staleResults"); + this.discarded = requireNonNegative(discarded, "discarded"); + this.routeShapeDifferences = requireNonNegative( + routeShapeDifferences, "routeShapeDifferences"); + this.upstreamCanarySelections = requireNonNegative( + upstreamCanarySelections, "upstreamCanarySelections"); + this.localFallbackDivergences = requireNonNegative( + localFallbackDivergences, "localFallbackDivergences"); + this.localFallbackFailures = requireNonNegative( + localFallbackFailures, "localFallbackFailures"); + this.startedAtEpochMillis = requireNonNegative(startedAtEpochMillis, "startedAtEpochMillis"); + EnumMap copy = + new EnumMap<>(Rs2PlannerShadowContext.Coverage.class); + copy.putAll(coverage); + this.coverage = Collections.unmodifiableMap(copy); + EnumMap executorCopy = + new EnumMap<>(Rs2TransportExecutor.class); + executorCopy.putAll(transportExecutors); + this.transportExecutors = Collections.unmodifiableMap(executorCopy); + EnumMap typeCopy = + new EnumMap<>(Rs2TransportType.class); + typeCopy.putAll(transportTypes); + this.transportTypes = Collections.unmodifiableMap(typeCopy); + this.execution = java.util.Objects.requireNonNull(execution, "execution"); + if (matches + divergences + failures != completed) + { + throw new IllegalArgumentException( + "completed comparisons must equal matches, divergences and failures"); + } + } + + private static long requireNonNegative(long value, String name) + { + if (value < 0) + { + throw new IllegalArgumentException(name + " must be non-negative"); + } + return value; + } + + public long getSubmitted() { return submitted; } + public long getCompleted() { return completed; } + public long getMatches() { return matches; } + public long getDivergences() { return divergences; } + public long getFailures() { return failures; } + public long getStaleResults() { return staleResults; } + public long getDiscarded() { return discarded; } + public long getRouteShapeDifferences() { return routeShapeDifferences; } + public long getUpstreamCanarySelections() { return upstreamCanarySelections; } + public long getLocalFallbackDivergences() { return localFallbackDivergences; } + public long getLocalFallbackFailures() { return localFallbackFailures; } + public long getStartedAtEpochMillis() { return startedAtEpochMillis; } + public Map getCoverage() + { + return coverage; + } + public Map getTransportExecutors() + { + return transportExecutors; + } + public Map getTransportTypes() + { + return transportTypes; + } + public Rs2WalkerShadowExecutionStats getExecution() { return execution; } + + public long getPending() + { + return Math.max(0L, submitted - completed - discarded); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlanningSnapshot.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlanningSnapshot.java new file mode 100644 index 00000000000..baca095994e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2PlanningSnapshot.java @@ -0,0 +1,141 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import net.runelite.api.coords.WorldPoint; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.IntPredicate; + +/** + * Immutable engine-neutral graph inputs captured after Microbot has resolved runtime admission. + * + *

The static collision archive is pinned separately and shared by both engines. This value owns the + * per-search overlays and already-filtered exact transport catalog, so an engine never reads mutable + * Microbot plugin state while searching.

+ */ +public final class Rs2PlanningSnapshot +{ + @FunctionalInterface + public interface CollisionOverride + { + /** Known north/east edge value, or {@code null} to fall back to pinned static collision. */ + Boolean edge(int x, int y, int plane, int flag); + } + + private static final int DANGEROUS_TILE_PENALTY = 100; + private static final CollisionOverride NO_COLLISION_OVERRIDE = (x, y, plane, flag) -> null; + + private final Rs2RoutePolicy policy; + private final List admittedTransports; + private final CollisionOverride collisionOverride; + private final Set blockedWalkingEdges; + private final IntPredicate dangerousTilePredicate; + + Rs2PlanningSnapshot( + Rs2RoutePolicy policy, + List admittedTransports, + CollisionOverride collisionOverride, + Set blockedWalkingEdges, + IntPredicate dangerousTilePredicate) + { + this.policy = Objects.requireNonNull(policy, "policy"); + this.admittedTransports = List.copyOf(admittedTransports); + this.collisionOverride = collisionOverride == null + ? NO_COLLISION_OVERRIDE : collisionOverride; + this.blockedWalkingEdges = Collections.unmodifiableSet( + new LinkedHashSet<>(blockedWalkingEdges)); + this.dangerousTilePredicate = Objects.requireNonNull( + dangerousTilePredicate, "dangerousTilePredicate"); + } + + public Rs2RoutePolicy getPolicy() + { + return policy; + } + + public List getAdmittedTransports() + { + return admittedTransports; + } + + public Boolean collisionOverride(int x, int y, int plane, int flag) + { + return collisionOverride.edge(x, y, plane, flag); + } + + public boolean isWalkingEdgeBlocked(int originPacked, int destinationPacked) + { + if (blockedWalkingEdges.isEmpty()) + { + return false; + } + if (blockedWalkingEdges.contains(edgeKey(originPacked, destinationPacked))) + { + return true; + } + int ox = unpackX(originPacked); + int oy = unpackY(originPacked); + int oz = unpackPlane(originPacked); + int dx = Integer.signum(unpackX(destinationPacked) - ox); + int dy = Integer.signum(unpackY(destinationPacked) - oy); + if (unpackPlane(destinationPacked) != oz || dx == 0 || dy == 0) + { + return false; + } + int xThenY = pack(ox + dx, oy, oz); + int yThenX = pack(ox, oy + dy, oz); + return blockedWalkingEdges.contains(edgeKey(originPacked, xThenY)) + || blockedWalkingEdges.contains(edgeKey(xThenY, destinationPacked)) + || blockedWalkingEdges.contains(edgeKey(originPacked, yThenX)) + || blockedWalkingEdges.contains(edgeKey(yThenX, destinationPacked)); + } + + public int getAdditionalWalkingCost(int packedDestination, Set targets) + { + if (!policy.isAvoidDangerousNpcs() || containsPacked(targets, packedDestination)) + { + return 0; + } + return dangerousTilePredicate.test(packedDestination) ? DANGEROUS_TILE_PENALTY : 0; + } + + private static boolean containsPacked(Set points, int packed) + { + for (WorldPoint point : points) + { + if (pack(point.getX(), point.getY(), point.getPlane()) == packed) + { + return true; + } + } + return false; + } + + private static long edgeKey(int originPacked, int destinationPacked) + { + return ((long) originPacked << 32) ^ (destinationPacked & 0xffffffffL); + } + + private static int pack(int x, int y, int plane) + { + return (x & 0x7FFF) | ((y & 0x7FFF) << 15) | ((plane & 0x3) << 30); + } + + private static int unpackX(int packed) + { + return packed & 0x7FFF; + } + + private static int unpackY(int packed) + { + return (packed >> 15) & 0x7FFF; + } + + private static int unpackPlane(int packed) + { + return (packed >> 30) & 0x3; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteMetrics.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteMetrics.java new file mode 100644 index 00000000000..ef63e28a844 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteMetrics.java @@ -0,0 +1,105 @@ +package net.runelite.client.plugins.microbot.util.walker; + +/** + * Immutable, planner-independent measurements for one route calculation. + * + *

A value of {@link #UNAVAILABLE} means an engine cannot expose that measurement without + * changing its search contract. Keeping that distinction explicit prevents the comparison harness + * from silently treating missing data as zero.

+ */ +public final class Rs2RouteMetrics +{ + public static final long UNAVAILABLE = -1L; + + private final long searchNanos; + private final long pathCost; + private final long nodesChecked; + private final long transportsChecked; + private final long liveCollisionEdgesChecked; + + Rs2RouteMetrics( + long searchNanos, + long pathCost, + long nodesChecked, + long transportsChecked) + { + this(searchNanos, pathCost, nodesChecked, transportsChecked, UNAVAILABLE); + } + + Rs2RouteMetrics( + long searchNanos, + long pathCost, + long nodesChecked, + long transportsChecked, + long liveCollisionEdgesChecked) + { + validateOptionalMetric("searchNanos", searchNanos); + validateOptionalMetric("pathCost", pathCost); + validateOptionalMetric("nodesChecked", nodesChecked); + validateOptionalMetric("transportsChecked", transportsChecked); + validateOptionalMetric("liveCollisionEdgesChecked", liveCollisionEdgesChecked); + this.searchNanos = searchNanos; + this.pathCost = pathCost; + this.nodesChecked = nodesChecked; + this.transportsChecked = transportsChecked; + this.liveCollisionEdgesChecked = liveCollisionEdgesChecked; + } + + private static void validateOptionalMetric(String name, long value) + { + if (value < 0 && value != UNAVAILABLE) + { + throw new IllegalArgumentException(name + " must be non-negative or UNAVAILABLE"); + } + } + + public long getSearchNanos() + { + return searchNanos; + } + + public boolean hasSearchNanos() + { + return searchNanos != UNAVAILABLE; + } + + public long getPathCost() + { + return pathCost; + } + + public boolean hasPathCost() + { + return pathCost != UNAVAILABLE; + } + + public long getNodesChecked() + { + return nodesChecked; + } + + public boolean hasNodesChecked() + { + return nodesChecked != UNAVAILABLE; + } + + public long getTransportsChecked() + { + return transportsChecked; + } + + public boolean hasTransportsChecked() + { + return transportsChecked != UNAVAILABLE; + } + + public long getLiveCollisionEdgesChecked() + { + return liveCollisionEdgesChecked; + } + + public boolean hasLiveCollisionEdgesChecked() + { + return liveCollisionEdgesChecked != UNAVAILABLE; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RoutePlanner.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RoutePlanner.java new file mode 100644 index 00000000000..1fe39222da6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RoutePlanner.java @@ -0,0 +1,14 @@ +package net.runelite.client.plugins.microbot.util.walker; + +/** Interchangeable route-engine boundary used after Microbot resolves an immutable request policy. */ +public interface Rs2RoutePlanner +{ + /** Stable diagnostic id such as {@code microbot-local} or a pinned upstream revision. */ + String getEngineId(); + + /** + * Calculate one route. Implementations must reject requests without a resolved policy rather than + * consulting mutable Microbot or plugin globals. + */ + Rs2RouteResult plan(Rs2RouteRequest request, Rs2PlanningSnapshot snapshot); +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RoutePolicy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RoutePolicy.java new file mode 100644 index 00000000000..379886f3b0a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RoutePolicy.java @@ -0,0 +1,108 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import net.runelite.api.coords.WorldPoint; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Immutable, engine-neutral policy resolved for one route calculation. + * + *

Graph data and the engine's static collision representation remain engine inputs; every mutable + * Microbot routing choice that can change admission or search behavior is copied here before dispatch. + * An upstream adapter must consume this value rather than reading {@code ShortestPathPlugin} globals.

+ */ +public final class Rs2RoutePolicy +{ + public enum TeleportationItemMode + { + NONE, + INVENTORY, + INVENTORY_NON_CONSUMABLE + } + + private final boolean useBankItems; + private final boolean avoidWilderness; + private final boolean avoidDangerousNpcs; + private final boolean ignoreTeleportAndItems; + private final boolean teleportsDisabled; + private final boolean membersWorld; + private final boolean liveCollisionEnabled; + private final long calculationCutoffMillis; + private final int distanceBeforeUsingTeleport; + private final TeleportationItemMode teleportationItemMode; + private final Set enabledTransportTypes; + private final Set restrictedPoints; + + public Rs2RoutePolicy( + boolean useBankItems, + boolean avoidWilderness, + boolean avoidDangerousNpcs, + boolean ignoreTeleportAndItems, + boolean teleportsDisabled, + boolean membersWorld, + boolean liveCollisionEnabled, + long calculationCutoffMillis, + int distanceBeforeUsingTeleport, + TeleportationItemMode teleportationItemMode, + Set enabledTransportTypes, + Set restrictedPoints) + { + if (calculationCutoffMillis <= 0) + { + throw new IllegalArgumentException("calculationCutoffMillis must be positive"); + } + if (distanceBeforeUsingTeleport < 0) + { + throw new IllegalArgumentException("distanceBeforeUsingTeleport must be non-negative"); + } + this.useBankItems = useBankItems; + this.avoidWilderness = avoidWilderness; + this.avoidDangerousNpcs = avoidDangerousNpcs; + this.ignoreTeleportAndItems = ignoreTeleportAndItems; + this.teleportsDisabled = teleportsDisabled; + this.membersWorld = membersWorld; + this.liveCollisionEnabled = liveCollisionEnabled; + this.calculationCutoffMillis = calculationCutoffMillis; + this.distanceBeforeUsingTeleport = distanceBeforeUsingTeleport; + this.teleportationItemMode = Objects.requireNonNull( + teleportationItemMode, "teleportationItemMode"); + Objects.requireNonNull(enabledTransportTypes, "enabledTransportTypes"); + EnumSet enabledCopy = enabledTransportTypes.isEmpty() + ? EnumSet.noneOf(Rs2TransportType.class) + : EnumSet.copyOf(enabledTransportTypes); + this.enabledTransportTypes = Collections.unmodifiableSet(enabledCopy); + Objects.requireNonNull(restrictedPoints, "restrictedPoints"); + LinkedHashSet restrictionCopy = new LinkedHashSet<>(); + for (WorldPoint point : restrictedPoints) + { + restrictionCopy.add(Objects.requireNonNull(point, "restricted point")); + } + this.restrictedPoints = Collections.unmodifiableSet(restrictionCopy); + } + + public Rs2RoutePolicy withUseBankItems(boolean enabled) + { + return new Rs2RoutePolicy( + enabled, avoidWilderness, avoidDangerousNpcs, ignoreTeleportAndItems, + teleportsDisabled, membersWorld, liveCollisionEnabled, calculationCutoffMillis, + distanceBeforeUsingTeleport, teleportationItemMode, enabledTransportTypes, + restrictedPoints); + } + + public boolean isUseBankItems() { return useBankItems; } + public boolean isAvoidWilderness() { return avoidWilderness; } + public boolean isAvoidDangerousNpcs() { return avoidDangerousNpcs; } + public boolean isIgnoreTeleportAndItems() { return ignoreTeleportAndItems; } + public boolean isTeleportsDisabled() { return teleportsDisabled; } + public boolean isMembersWorld() { return membersWorld; } + public boolean isLiveCollisionEnabled() { return liveCollisionEnabled; } + public long getCalculationCutoffMillis() { return calculationCutoffMillis; } + public int getDistanceBeforeUsingTeleport() { return distanceBeforeUsingTeleport; } + public TeleportationItemMode getTeleportationItemMode() { return teleportationItemMode; } + public Set getEnabledTransportTypes() { return enabledTransportTypes; } + public Set getRestrictedPoints() { return restrictedPoints; } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteRequest.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteRequest.java new file mode 100644 index 00000000000..d50f7d08e5f --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteRequest.java @@ -0,0 +1,159 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import net.runelite.api.coords.WorldPoint; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * Immutable Microbot-owned input for a synchronous route calculation. + * + *

The request deliberately describes policy without exposing {@code PathfinderConfig}. New + * synchronous planning consumers should use this value through {@link Rs2PathApi#plan(Rs2RouteRequest)} + * instead of constructing a shortest-path {@code Pathfinder} directly.

+ */ +public final class Rs2RouteRequest +{ + /** Caller-owned workflow intent used only to classify planner evidence. */ + public enum Purpose + { + GENERAL, + BANK_ROUTE_DIRECT, + BANK_ROUTE_TO_BANK, + BANK_ROUTE_FROM_BANK + } + + public enum RefreshPolicy + { + NEVER, + IF_TRANSPORTS_EMPTY, + ALWAYS + } + + private final WorldPoint start; + private final Set targets; + private final RefreshPolicy refreshPolicy; + private final WorldPoint refreshTarget; + private final Boolean useBankItems; + private final Rs2RoutePolicy policy; + private final Purpose purpose; + + private Rs2RouteRequest( + WorldPoint start, + Set targets, + RefreshPolicy refreshPolicy, + WorldPoint refreshTarget, + Boolean useBankItems, + Rs2RoutePolicy policy, + Purpose purpose) + { + this.start = Objects.requireNonNull(start, "start"); + Objects.requireNonNull(targets, "targets"); + if (targets.isEmpty()) + { + throw new IllegalArgumentException("targets must not be empty"); + } + LinkedHashSet targetCopy = new LinkedHashSet<>(); + for (WorldPoint target : targets) + { + targetCopy.add(Objects.requireNonNull(target, "target")); + } + this.targets = Collections.unmodifiableSet(targetCopy); + this.refreshPolicy = Objects.requireNonNull(refreshPolicy, "refreshPolicy"); + this.refreshTarget = refreshTarget; + this.useBankItems = useBankItems; + this.policy = policy; + this.purpose = Objects.requireNonNull(purpose, "purpose"); + } + + public static Rs2RouteRequest to(WorldPoint start, WorldPoint target) + { + return toAny(start, Collections.singleton(target)); + } + + public static Rs2RouteRequest toAny(WorldPoint start, Set targets) + { + return new Rs2RouteRequest( + start, targets, RefreshPolicy.IF_TRANSPORTS_EMPTY, null, null, null, + Purpose.GENERAL); + } + + public Rs2RouteRequest withRefreshPolicy(RefreshPolicy policy) + { + return new Rs2RouteRequest( + start, targets, policy, refreshTarget, useBankItems, this.policy, purpose); + } + + public Rs2RouteRequest withRefreshTarget(WorldPoint target) + { + return new Rs2RouteRequest( + start, targets, refreshPolicy, target, useBankItems, policy, purpose); + } + + /** + * Include or exclude bank contents while evaluating transport requirements. Planning with this + * temporary policy always refreshes the transport snapshot and restores the previous policy after + * the search. + */ + public Rs2RouteRequest withBankItems(boolean enabled) + { + return new Rs2RouteRequest( + start, targets, RefreshPolicy.ALWAYS, refreshTarget, enabled, + policy == null ? null : policy.withUseBankItems(enabled), purpose); + } + + /** Classify this request without changing planning behavior. */ + public Rs2RouteRequest withPurpose(Purpose purpose) + { + return new Rs2RouteRequest( + start, targets, refreshPolicy, refreshTarget, useBankItems, policy, + Objects.requireNonNull(purpose, "purpose")); + } + + /** Attach the fully resolved policy passed to an engine implementation. */ + public Rs2RouteRequest withPolicy(Rs2RoutePolicy resolvedPolicy) + { + Rs2RoutePolicy nonNullPolicy = Objects.requireNonNull(resolvedPolicy, "resolvedPolicy"); + return new Rs2RouteRequest( + start, targets, refreshPolicy, refreshTarget, + nonNullPolicy.isUseBankItems(), nonNullPolicy, purpose); + } + + public WorldPoint getStart() + { + return start; + } + + public Set getTargets() + { + return targets; + } + + public RefreshPolicy getRefreshPolicy() + { + return refreshPolicy; + } + + public WorldPoint getRefreshTarget() + { + return refreshTarget; + } + + public Boolean getUseBankItems() + { + return useBankItems; + } + + public Optional getPolicy() + { + return Optional.ofNullable(policy); + } + + public Purpose getPurpose() + { + return purpose; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteResult.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteResult.java new file mode 100644 index 00000000000..cfd2f82e29b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteResult.java @@ -0,0 +1,185 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import net.runelite.api.coords.WorldPoint; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** Immutable result of a synchronous Microbot route calculation. */ +public final class Rs2RouteResult +{ + private final WorldPoint start; + private final Set targets; + private final List path; + private final List steps; + private final Map> transportEdgesByEndpoints; + private final Rs2RouteTermination terminationReason; + private final Rs2RouteMetrics metrics; + + Rs2RouteResult( + WorldPoint start, + Set targets, + List path, + List steps, + Rs2RouteTermination terminationReason, + Rs2RouteMetrics metrics) + { + this.start = start; + this.targets = Collections.unmodifiableSet(new LinkedHashSet<>(targets)); + this.path = path == null ? Collections.emptyList() : List.copyOf(path); + this.steps = steps == null ? Collections.emptyList() : List.copyOf(steps); + validateSteps(this.path, this.steps); + this.transportEdgesByEndpoints = indexTransportEdges(this.steps); + this.terminationReason = Objects.requireNonNull(terminationReason, "terminationReason"); + this.metrics = Objects.requireNonNull(metrics, "metrics"); + } + + public WorldPoint getStart() + { + return start; + } + + public Set getTargets() + { + return targets; + } + + public List getPath() + { + return path; + } + + public List getSteps() + { + return steps; + } + + public List getTransportSteps() + { + List transportSteps = new ArrayList<>(); + for (Rs2RouteStep step : steps) + { + if (step.isTransport()) + { + transportSteps.add(step); + } + } + return List.copyOf(transportSteps); + } + + /** Exact selected transport for a directed route edge, if that edge is a transport. */ + public Optional getTransportEdge(WorldPoint from, WorldPoint to) + { + if (from == null || to == null) + { + return Optional.empty(); + } + Map byDestination = transportEdgesByEndpoints.get(from); + return byDestination == null + ? Optional.empty() + : Optional.ofNullable(byDestination.get(to)); + } + + /** Whether the search ended normally rather than through cancellation or an internal failure. */ + public boolean isSearchCompleted() + { + return terminationReason != Rs2RouteTermination.CANCELLED + && terminationReason != Rs2RouteTermination.FAILED; + } + + public Rs2RouteTermination getTerminationReason() + { + return terminationReason; + } + + public long getSearchNanos() + { + return metrics.getSearchNanos(); + } + + public boolean hasSearchNanos() + { + return metrics.hasSearchNanos(); + } + + public Rs2RouteMetrics getMetrics() + { + return metrics; + } + + public Optional getEndpoint() + { + return path.isEmpty() ? Optional.empty() : Optional.of(path.get(path.size() - 1)); + } + + public Optional getReachedTarget(int tolerance) + { + if (tolerance < 0) + { + throw new IllegalArgumentException("tolerance must be non-negative"); + } + Optional endpoint = getEndpoint(); + if (endpoint.isEmpty()) + { + return Optional.empty(); + } + WorldPoint end = endpoint.get(); + return targets.stream() + .filter(target -> target.getPlane() == end.getPlane()) + .filter(target -> target.distanceTo2D(end) <= tolerance) + .findFirst(); + } + + public boolean isTargetReached(int tolerance) + { + return getReachedTarget(tolerance).isPresent(); + } + + private static void validateSteps(List path, List steps) + { + int expectedSteps = Math.max(0, path.size() - 1); + if (steps.size() != expectedSteps) + { + throw new IllegalArgumentException( + "route steps must describe every path edge: expected " + expectedSteps + + ", got " + steps.size()); + } + for (int i = 0; i < steps.size(); i++) + { + Rs2RouteStep step = steps.get(i); + if (!path.get(i).equals(step.getFrom()) || !path.get(i + 1).equals(step.getTo())) + { + throw new IllegalArgumentException("route step " + i + " is not contiguous with path"); + } + } + } + + private static Map> indexTransportEdges( + List steps) + { + Map> mutable = new LinkedHashMap<>(); + for (Rs2RouteStep step : steps) + { + if (!step.isTransport()) + { + continue; + } + Rs2TransportEdge edge = step.getTransport().orElseThrow(IllegalStateException::new); + mutable.computeIfAbsent(step.getFrom(), ignored -> new LinkedHashMap<>()) + .putIfAbsent(step.getTo(), edge); + } + Map> immutable = new LinkedHashMap<>(); + for (Map.Entry> entry : mutable.entrySet()) + { + immutable.put(entry.getKey(), Collections.unmodifiableMap(entry.getValue())); + } + return Collections.unmodifiableMap(immutable); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteStep.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteStep.java new file mode 100644 index 00000000000..55a166caef8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteStep.java @@ -0,0 +1,53 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import net.runelite.api.coords.WorldPoint; + +import java.util.Objects; +import java.util.Optional; + +/** One immutable, planner-independent edge in a route. */ +public final class Rs2RouteStep +{ + public enum Kind + { + WALK, + TRANSPORT + } + + private final WorldPoint from; + private final WorldPoint to; + private final Kind kind; + private final Rs2TransportEdge transport; + + private Rs2RouteStep(WorldPoint from, WorldPoint to, Kind kind, Rs2TransportEdge transport) + { + this.from = Objects.requireNonNull(from, "from"); + this.to = Objects.requireNonNull(to, "to"); + this.kind = Objects.requireNonNull(kind, "kind"); + this.transport = transport; + if ((kind == Kind.TRANSPORT) != (transport != null)) + { + throw new IllegalArgumentException("transport metadata must match route-step kind"); + } + if (transport != null && !to.equals(transport.getDestination())) + { + throw new IllegalArgumentException("transport destination must match route-step destination"); + } + } + + public static Rs2RouteStep walk(WorldPoint from, WorldPoint to) + { + return new Rs2RouteStep(from, to, Kind.WALK, null); + } + + public static Rs2RouteStep transport(WorldPoint from, WorldPoint to, Rs2TransportEdge transport) + { + return new Rs2RouteStep(from, to, Kind.TRANSPORT, transport); + } + + public WorldPoint getFrom() { return from; } + public WorldPoint getTo() { return to; } + public Kind getKind() { return kind; } + public Optional getTransport() { return Optional.ofNullable(transport); } + public boolean isTransport() { return kind == Kind.TRANSPORT; } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteTermination.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteTermination.java new file mode 100644 index 00000000000..dd982ae5329 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2RouteTermination.java @@ -0,0 +1,11 @@ +package net.runelite.client.plugins.microbot.util.walker; + +/** Stable Microbot-owned reason why route planning stopped. */ +public enum Rs2RouteTermination +{ + TARGET_REACHED, + SEARCH_EXHAUSTED, + CUTOFF_REACHED, + CANCELLED, + FAILED +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TerminalTravelMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TerminalTravelMode.java new file mode 100644 index 00000000000..5b6a70b0249 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TerminalTravelMode.java @@ -0,0 +1,9 @@ +package net.runelite.client.plugins.microbot.util.walker; + +/** Planner-independent interaction sequence for a terminal SHIP, NPC or BOAT edge. */ +public enum Rs2TerminalTravelMode +{ + DIRECT, + DIALOGUE_DESTINATION, + UNSUPPORTED +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportEdge.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportEdge.java new file mode 100644 index 00000000000..e974a855fd0 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportEdge.java @@ -0,0 +1,121 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import net.runelite.api.coords.WorldPoint; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** Immutable execution-facing description of the exact transport selected for a route edge. */ +public final class Rs2TransportEdge +{ + private final WorldPoint origin; + private final WorldPoint destination; + private final Rs2TransportType type; + private final Rs2TransportExecutor executor; + private final Rs2TerminalTravelMode terminalTravelMode; + private final String displayInfo; + private final String action; + private final String target; + private final int objectId; + private final int duration; + private final boolean teleport; + private final boolean consumable; + private final boolean members; + private final int maxWildernessLevel; + private final String currencyName; + private final int currencyAmount; + private final List itemRequirements; + /** Opaque local-engine identity; never exposed by the public planner-independent contract. */ + private final Object sourceIdentity; + + public Rs2TransportEdge( + WorldPoint origin, + WorldPoint destination, + Rs2TransportType type, + Rs2TransportExecutor executor, + Rs2TerminalTravelMode terminalTravelMode, + String displayInfo, + String action, + String target, + int objectId, + int duration, + boolean teleport, + boolean consumable, + boolean members, + int maxWildernessLevel, + String currencyName, + int currencyAmount, + List itemRequirements) + { + this(origin, destination, type, executor, terminalTravelMode, displayInfo, action, target, + objectId, duration, teleport, consumable, members, maxWildernessLevel, currencyName, + currencyAmount, itemRequirements, null); + } + + Rs2TransportEdge( + WorldPoint origin, + WorldPoint destination, + Rs2TransportType type, + Rs2TransportExecutor executor, + Rs2TerminalTravelMode terminalTravelMode, + String displayInfo, + String action, + String target, + int objectId, + int duration, + boolean teleport, + boolean consumable, + boolean members, + int maxWildernessLevel, + String currencyName, + int currencyAmount, + List itemRequirements, + Object sourceIdentity) + { + this.origin = origin; + this.destination = Objects.requireNonNull(destination, "destination"); + this.type = Objects.requireNonNull(type, "type"); + this.executor = Objects.requireNonNull(executor, "executor"); + this.terminalTravelMode = Objects.requireNonNull(terminalTravelMode, "terminalTravelMode"); + if ((executor == Rs2TransportExecutor.TERMINAL_TRAVEL) + != (terminalTravelMode != Rs2TerminalTravelMode.UNSUPPORTED)) + { + throw new IllegalArgumentException("terminal travel executor and mode must agree"); + } + this.displayInfo = displayInfo; + this.action = action; + this.target = target; + this.objectId = objectId; + this.duration = duration; + this.teleport = teleport; + this.consumable = consumable; + this.members = members; + this.maxWildernessLevel = maxWildernessLevel; + this.currencyName = currencyName == null ? "" : currencyName; + this.currencyAmount = currencyAmount; + this.itemRequirements = itemRequirements == null + ? Collections.emptyList() + : List.copyOf(itemRequirements); + this.sourceIdentity = sourceIdentity; + } + + public WorldPoint getOrigin() { return origin; } + public WorldPoint getDestination() { return destination; } + public Rs2TransportType getType() { return type; } + public Rs2TransportExecutor getExecutor() { return executor; } + public Rs2TerminalTravelMode getTerminalTravelMode() { return terminalTravelMode; } + public String getDisplayInfo() { return displayInfo; } + public String getAction() { return action; } + public String getTarget() { return target; } + public int getObjectId() { return objectId; } + public int getDuration() { return duration; } + public boolean isTeleport() { return teleport; } + public boolean isConsumable() { return consumable; } + public boolean isMembers() { return members; } + public int getMaxWildernessLevel() { return maxWildernessLevel; } + public String getCurrencyName() { return currencyName; } + public int getCurrencyAmount() { return currencyAmount; } + public List getItemRequirements() { return itemRequirements; } + Object getSourceIdentity() { return sourceIdentity; } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportExecutor.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportExecutor.java new file mode 100644 index 00000000000..71f9b6d0f5a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportExecutor.java @@ -0,0 +1,25 @@ +package net.runelite.client.plugins.microbot.util.walker; + +/** Planner-independent Microbot runtime capability selected for a transport edge. */ +public enum Rs2TransportExecutor +{ + BARROWS_DIG, + CANOE, + CHARTER_SHIP, + FAIRY_RING, + GNOME_GLIDER, + HOT_AIR_BALLOON, + ITEM_TELEPORT, + MAGIC_CARPET, + MAGIC_MUSHTREE, + MINIGAME_TELEPORT, + OBJECT, + POH, + QUETZAL, + SEASONAL, + SPELL_TELEPORT, + SPIRIT_TREE, + TERMINAL_TRAVEL, + WILDERNESS_OBELISK, + UNSUPPORTED +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportItemRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportItemRequirement.java new file mode 100644 index 00000000000..b9844c73e71 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportItemRequirement.java @@ -0,0 +1,224 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.IntPredicate; +import java.util.function.IntUnaryOperator; + +/** One immutable AND-clause whose item/quantity alternatives are OR-ed. */ +public final class Rs2TransportItemRequirement +{ + private final Map alternatives; + private final Set staffAlternatives; + private final Set offhandAlternatives; + private final boolean runeOnly; + + public Rs2TransportItemRequirement(Map alternatives) + { + this(alternatives, Collections.emptySet(), Collections.emptySet(), false); + } + + public Rs2TransportItemRequirement( + Map alternatives, + Set staffAlternatives, + Set offhandAlternatives, + boolean runeOnly) + { + if (alternatives == null || alternatives.isEmpty()) + { + throw new IllegalArgumentException("item requirement must contain an alternative"); + } + Map copy = new LinkedHashMap<>(); + for (Map.Entry alternative : alternatives.entrySet()) + { + Integer itemId = Objects.requireNonNull(alternative.getKey(), "itemId"); + Integer quantity = Objects.requireNonNull(alternative.getValue(), "quantity"); + if (itemId <= 0 || quantity < 0) + { + throw new IllegalArgumentException("invalid item requirement: " + alternative); + } + copy.put(itemId, quantity); + } + this.alternatives = Collections.unmodifiableMap(copy); + this.staffAlternatives = immutablePositiveIds(staffAlternatives, "staff"); + this.offhandAlternatives = immutablePositiveIds(offhandAlternatives, "offhand"); + this.runeOnly = runeOnly; + } + + private static Set immutablePositiveIds(Set itemIds, String label) + { + if (itemIds == null || itemIds.isEmpty()) + { + return Collections.emptySet(); + } + LinkedHashSet copy = new LinkedHashSet<>(); + for (Integer itemId : itemIds) + { + if (itemId == null || itemId <= 0) + { + throw new IllegalArgumentException(label + " item id must be positive: " + itemId); + } + copy.add(itemId); + } + return Collections.unmodifiableSet(copy); + } + + public Map getAlternatives() + { + return alternatives; + } + + public Set getStaffAlternatives() { return staffAlternatives; } + public Set getOffhandAlternatives() { return offhandAlternatives; } + public boolean isRuneOnly() { return runeOnly; } + + public boolean isSatisfiedBy(IntUnaryOperator availableQuantity) + { + Objects.requireNonNull(availableQuantity, "availableQuantity"); + return alternatives.entrySet().stream().anyMatch(alternative -> + { + int required = alternative.getValue(); + int available = Math.max(0, availableQuantity.applyAsInt(alternative.getKey())); + return (required == 0 && available == 0) || (required > 0 && available >= required); + }); + } + + private boolean isSatisfiedBy(IntUnaryOperator availableQuantity, int staffItemId, int offhandItemId) + { + return isSatisfiedBy(availableQuantity) + || staffAlternatives.contains(staffItemId) + || offhandAlternatives.contains(offhandItemId); + } + + public static Optional selectProviders( + List requirements, + IntUnaryOperator availableQuantity, + IntPredicate staffAvailable, + IntPredicate offhandAvailable) + { + return selectProviders( + requirements, availableQuantity, staffAvailable, offhandAvailable, false); + } + + /** + * Select equipment for provider-bearing clauses while deferring ordinary missing items to the + * withdrawal/purchase planner. This prevents an unbanked purchasable pass from rejecting the + * equipment phase before its fare fallback can be evaluated. + */ + public static Optional selectEquipmentProviders( + List requirements, + IntUnaryOperator availableQuantity, + IntPredicate staffAvailable, + IntPredicate offhandAvailable) + { + return selectProviders( + requirements, availableQuantity, staffAvailable, offhandAvailable, true); + } + + private static Optional selectProviders( + List requirements, + IntUnaryOperator availableQuantity, + IntPredicate staffAvailable, + IntPredicate offhandAvailable, + boolean deferOrdinaryRequirements) + { + if (requirements == null || requirements.isEmpty()) + { + return Optional.of(ProviderSelection.NONE); + } + TreeSet staffs = new TreeSet<>(); + TreeSet offhands = new TreeSet<>(); + for (Rs2TransportItemRequirement requirement : requirements) + { + requirement.staffAlternatives.stream().filter(staffAvailable::test).forEach(staffs::add); + requirement.offhandAlternatives.stream().filter(offhandAvailable::test).forEach(offhands::add); + } + List staffCandidates = new ArrayList<>(); + staffCandidates.add(ProviderSelection.NO_ITEM); + staffCandidates.addAll(staffs); + List offhandCandidates = new ArrayList<>(); + offhandCandidates.add(ProviderSelection.NO_ITEM); + offhandCandidates.addAll(offhands); + for (Integer staff : staffCandidates) + { + for (Integer offhand : offhandCandidates) + { + boolean satisfied = true; + for (Rs2TransportItemRequirement requirement : requirements) + { + if (deferOrdinaryRequirements + && requirement.staffAlternatives.isEmpty() + && requirement.offhandAlternatives.isEmpty()) + { + continue; + } + if (!requirement.isSatisfiedBy(availableQuantity, staff, offhand)) + { + satisfied = false; + break; + } + } + if (satisfied) + { + return Optional.of(new ProviderSelection(staff, offhand)); + } + } + } + return Optional.empty(); + } + + public static final class ProviderSelection + { + private static final int NO_ITEM = -1; + private static final ProviderSelection NONE = new ProviderSelection(NO_ITEM, NO_ITEM); + + private final int staffItemId; + private final int offhandItemId; + + private ProviderSelection(int staffItemId, int offhandItemId) + { + this.staffItemId = staffItemId; + this.offhandItemId = offhandItemId; + } + + public int getStaffItemId() { return staffItemId; } + public int getOffhandItemId() { return offhandItemId; } + public boolean hasStaff() { return staffItemId > 0; } + public boolean hasOffhand() { return offhandItemId > 0; } + } + + @Override + public boolean equals(Object other) + { + if (this == other) + { + return true; + } + if (!(other instanceof Rs2TransportItemRequirement)) + { + return false; + } + Rs2TransportItemRequirement that = (Rs2TransportItemRequirement) other; + return alternatives.equals(that.alternatives) + && staffAlternatives.equals(that.staffAlternatives) + && offhandAlternatives.equals(that.offhandAlternatives) + && runeOnly == that.runeOnly; + } + + @Override + public int hashCode() + { + int result = alternatives.hashCode(); + result = 31 * result + staffAlternatives.hashCode(); + result = 31 * result + offhandAlternatives.hashCode(); + return 31 * result + Boolean.hashCode(runeOnly); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportLoadout.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportLoadout.java new file mode 100644 index 00000000000..bfe494468ce --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportLoadout.java @@ -0,0 +1,78 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Immutable bank preparation required by the exact transport edges selected for a route. + * + *

Withdrawals and equipment changes are one contract: a route that depends on a banked staff or + * tome is not prepared until the item is both withdrawn and equipped. An unsatisfiable loadout is + * distinct from an empty loadout so callers cannot mistake a missing bank item for "nothing to do".

+ */ +public final class Rs2TransportLoadout +{ + private static final Rs2TransportLoadout EMPTY = + new Rs2TransportLoadout(Collections.emptyMap(), Collections.emptyList(), true); + private static final Rs2TransportLoadout UNAVAILABLE = + new Rs2TransportLoadout(Collections.emptyMap(), Collections.emptyList(), false); + + private final Map withdrawals; + private final List equipmentItemIds; + private final boolean satisfiable; + + public Rs2TransportLoadout( + Map withdrawals, + List equipmentItemIds, + boolean satisfiable) + { + LinkedHashMap withdrawalCopy = new LinkedHashMap<>(); + if (withdrawals != null) + { + for (Map.Entry withdrawal : withdrawals.entrySet()) + { + Integer itemId = withdrawal.getKey(); + Integer quantity = withdrawal.getValue(); + if (itemId == null || itemId <= 0 || quantity == null || quantity <= 0) + { + throw new IllegalArgumentException("invalid transport withdrawal: " + withdrawal); + } + withdrawalCopy.merge(itemId, quantity, Integer::sum); + } + } + this.withdrawals = Collections.unmodifiableMap(withdrawalCopy); + if (equipmentItemIds == null) + { + this.equipmentItemIds = Collections.emptyList(); + } + else + { + for (Integer itemId : equipmentItemIds) + { + if (itemId == null || itemId <= 0) + { + throw new IllegalArgumentException("invalid equipment item id: " + itemId); + } + } + this.equipmentItemIds = List.copyOf(equipmentItemIds); + } + this.satisfiable = satisfiable; + } + + public static Rs2TransportLoadout empty() + { + return EMPTY; + } + + public static Rs2TransportLoadout unavailable() + { + return UNAVAILABLE; + } + + public Map getWithdrawals() { return withdrawals; } + public List getEquipmentItemIds() { return equipmentItemIds; } + public boolean isSatisfiable() { return satisfiable; } + public boolean isEmpty() { return withdrawals.isEmpty() && equipmentItemIds.isEmpty(); } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportPlanningPolicy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportPlanningPolicy.java new file mode 100644 index 00000000000..792b3c778fd --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportPlanningPolicy.java @@ -0,0 +1,28 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import net.runelite.client.plugins.microbot.shortestpath.Transport; +import net.runelite.client.plugins.microbot.shortestpath.TransportExecutionRegistry; +import net.runelite.client.plugins.microbot.shortestpath.pathfinder.TransportPlanningPolicy; + +/** Microbot executor capabilities projected into the local planner's catalog-admission seam. */ +public final class Rs2TransportPlanningPolicy implements TransportPlanningPolicy +{ + public static final Rs2TransportPlanningPolicy INSTANCE = new Rs2TransportPlanningPolicy(); + + private Rs2TransportPlanningPolicy() + { + } + + @Override + public boolean isAdmitted(Transport transport) + { + return TransportExecutionRegistry.canExecute(transport); + } + + @Override + public boolean isZeroRuneSpell(Transport transport) + { + return transport != null + && TransportExecutionRegistry.homeTeleportFor(transport.getDisplayInfo()).isPresent(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportType.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportType.java new file mode 100644 index 00000000000..2b363812c0c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2TransportType.java @@ -0,0 +1,40 @@ +package net.runelite.client.plugins.microbot.util.walker; + +/** + * Planner-independent transport categories understood by the Microbot walker boundary. + * + *

The superset includes names used by both the local planner and the tracked upstream planner so + * an adapter can normalize either engine without exposing its enum.

+ */ +public enum Rs2TransportType +{ + TRANSPORT, + AGILITY_SHORTCUT, + GRAPPLE_SHORTCUT, + BOAT, + CANOE, + CHARTER_SHIP, + SHIP, + FAIRY_RING, + QUETZAL, + QUETZAL_WHISTLE, + GNOME_GLIDER, + MINECART, + POH, + SPIRIT_TREE, + TELEPORTATION_BOX, + TELEPORTATION_LEVER, + TELEPORTATION_PORTAL, + TELEPORTATION_PORTAL_POH, + TELEPORTATION_MINIGAME, + TELEPORTATION_ITEM, + TELEPORTATION_SPELL, + TELEPORTATION_SPELL_HOME, + WILDERNESS_OBELISK, + MAGIC_CARPET, + HOT_AIR_BALLOON, + MAGIC_MUSHTREE, + SEASONAL_TRANSPORT, + NPC, + UNKNOWN +} 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 0871e8a71a6..5a8efd7896d 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 @@ -19,9 +19,6 @@ import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.globval.enums.InterfaceTab; import net.runelite.client.plugins.microbot.shortestpath.*; -import net.runelite.client.plugins.microbot.shortestpath.pathfinder.CollisionMap; -import net.runelite.client.plugins.microbot.shortestpath.pathfinder.Pathfinder; -import net.runelite.client.plugins.microbot.shortestpath.pathfinder.PathfinderConfig; 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.camera.Rs2Camera; @@ -50,6 +47,7 @@ import net.runelite.client.plugins.microbot.util.logging.Rs2LogRateLimit; import java.util.function.BooleanSupplier; import java.util.function.Predicate; +import java.util.function.Supplier; import org.slf4j.event.Level; import net.runelite.client.plugins.microbot.util.poh.PohTeleports; import net.runelite.client.plugins.microbot.util.poh.PohTransport; @@ -88,7 +86,7 @@ import java.util.*; import java.util.List; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Future; +import java.util.concurrent.TimeoutException; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.concurrent.TimeUnit; @@ -133,6 +131,8 @@ public static WorldPoint getCurrentTarget() { // interim-target state migrated to WalkerRouteState (see routeState) private static final long PARTIAL_TRANS_RECAL_COOLDOWN_MS = 3500L; + /** A single delayed client tick must not turn an otherwise healthy blocking walk into EXIT. */ + private static final int CLIENT_THREAD_TIMEOUT_RETRIES = 2; private static final int INTERIM_CLOSE_TILES = 5; @@ -234,7 +234,7 @@ public static WorldPoint getCurrentTarget() { private static final int ROUTE_PROGRESS_FORWARD_SEARCH_TILES = 40; /** - * How long to wait for {@code Rs2PathApi.getPathfinder()} to become non-null at route start. + * How long to wait for an active route to be published at route start. *

* The pathfinder is only published after {@code PathfinderConfig.refresh()} completes * (see {@code Rs2WalkerLifecycleRuntime.restartPathfinding}), and a cache-missing refresh has been @@ -308,6 +308,7 @@ public static WorldPoint getCurrentTarget() { /** After scene-object transport {@link #handleObject} — landing poll timeout + matching warn (cf. {@link #SHIP_NPC_BOAT_LANDING_WAIT_MS}). */ private static final int POST_HANDLE_OBJECT_LANDING_WAIT_MS = 5_000; private static final int POST_HANDLE_OBJECT_FAILED_SETTLE_MS = 800; + private static final int AL_KHARID_TOLL_INTERACTION_START_WAIT_MS = 2_500; /** Teleport “already near destination” skip in path loop — same semantics as prior {@code distanceTo2D < 3}. */ private static final int TELEPORT_NEAR_SKIP_CHEBYSHEV = 3; @@ -345,9 +346,16 @@ private static String compactWorldPoint(WorldPoint wp) { } private static void markWalkSessionStart(WorldPoint target) { + testRecoveryReplanRequests.set(0); + WalkEvidenceContext evidence = walkEvidenceContext.get(); + if (evidence != null) + { + evidence.started = true; + } routeState.walkSessionStartedAtMs = System.currentTimeMillis(); routeState.firstMovementClickMarked = false; startupPhasesLogged.clear(); + TERMINAL_TRAVEL_ATTEMPTED_EDGES.clear(); routeState.lastTransportHandledAtLocation = null; routeState.lastTransportOriginLocation = null; routeState.lastTransportDestinationLocation = null; @@ -698,8 +706,8 @@ private static void waitUntilIdleAfterSceneWalk(WorldPoint cancelGoal, int timeo * the nearest reachable tile and then reported {@code partial-retries-exhausted}, which reads * as a walker fault rather than a bad coordinate. * - *

Permissive by design. An unmapped region reads as fully blocked (see - * {@link CollisionMap#hasRegion}), so every ambiguous case returns {@code true} and lets the + *

Permissive by design. An unmapped collision region reads as fully blocked, so every + * ambiguous case returns {@code true} and lets the * pathfinder decide. Only a target sitting in mapped, wholly blocked terrain is rejected — * otherwise this would refuse instances and any region missing from the collision map. */ @@ -713,53 +721,7 @@ private static void waitUntilIdleAfterSceneWalk(WorldPoint cancelGoal, int timeo * Cove staircase approach at (2531,2834) reads walkable in the scene and blocked here. */ public static boolean isWalkableInCollisionMap(WorldPoint tile) { - PathfinderConfig config = Rs2PathApi.getPathfinderConfig(); - return hasWalkableTileWithin(config != null ? config.getMap() : null, tile, 0); - } - - static boolean hasWalkableTileWithin(CollisionMap map, WorldPoint target, int distance) { - if (map == null || target == null) { - return true; - } - if (!map.hasRegion(target.getX(), target.getY())) { - return true; - } - int radius = Math.max(0, distance); - for (int dx = -radius; dx <= radius; dx++) { - for (int dy = -radius; dy <= radius; dy++) { - int x = target.getX() + dx; - int y = target.getY() + dy; - if (!map.hasRegion(x, y)) { - return true; - } - if (!map.isBlocked(x, y, target.getPlane())) { - return true; - } - } - } - return false; - } - - /** Nearest walkable tile to {@code target} within {@code maxRadius}, or null. Diagnostics only. */ - static WorldPoint nearestWalkableTile(CollisionMap map, WorldPoint target, int maxRadius) { - if (map == null || target == null) { - return null; - } - for (int r = 1; r <= maxRadius; r++) { - for (int dx = -r; dx <= r; dx++) { - for (int dy = -r; dy <= r; dy++) { - if (Math.max(Math.abs(dx), Math.abs(dy)) != r) { - continue; // ring only; inner rings already scanned - } - int x = target.getX() + dx; - int y = target.getY() + dy; - if (map.hasRegion(x, y) && !map.isBlocked(x, y, target.getPlane())) { - return new WorldPoint(x, y, target.getPlane()); - } - } - } - } - return null; + return Rs2PathApi.hasWalkableTileWithin(tile, 0); } /** Door / gate from main path loop vs {@link #handleNearbyRawPathSceneObjects} raw-path scan (same nudge UX). */ @@ -957,6 +919,51 @@ private static void logRouteClear(String reason) { * context and therefore retain their exact behaviour.

*/ private static final ThreadLocal walkCompletionContext = new ThreadLocal<>(); + private static final ThreadLocal walkEvidenceContext = new ThreadLocal<>(); + private static final AtomicInteger testRecoveryReplanRequests = new AtomicInteger(); + + private static final class WalkEvidenceContext + { + private boolean started; + private boolean comparisonEligible; + private boolean recoveryTriggered; + } + + private static void captureActiveRouteComparisonEligibility(long routeGeneration) + { + WalkEvidenceContext evidence = walkEvidenceContext.get(); + if (evidence != null && !evidence.comparisonEligible + && Rs2PathApi.isActiveRouteComparisonEligible(routeGeneration)) + { + evidence.comparisonEligible = true; + } + } + + private static WalkerState withShadowExecutionEvidence(Supplier action) + { + WalkEvidenceContext existing = walkEvidenceContext.get(); + if (existing != null) + { + return action.get(); + } + WalkEvidenceContext evidence = new WalkEvidenceContext(); + walkEvidenceContext.set(evidence); + try + { + WalkerState result = Objects.requireNonNull(action.get(), "walker result"); + if (evidence.started) + { + Rs2PathApi.recordShadowWalkerOutcome( + result, evidence.recoveryTriggered, evidence.comparisonEligible); + } + return result; + } + finally + { + testRecoveryReplanRequests.set(0); + walkEvidenceContext.remove(); + } + } private static final class WalkCompletionContext { private final WorldPoint target; @@ -978,6 +985,8 @@ private WalkCompletionContext(WorldPoint target, BooleanSupplier condition) { private static final Set SEASONAL_HANDLER_MISS_LOGGED = ConcurrentHashMap.newKeySet(); private static final AtomicInteger SEASONAL_HANDLER_MISS_LOGGED_COUNT = new AtomicInteger(0); private static final int SEASONAL_HANDLER_MISS_LOG_CAP = 128; + /** Terminal NPC edges already clicked during the current top-level walk invocation. */ + private static final Set TERMINAL_TRAVEL_ATTEMPTED_EDGES = ConcurrentHashMap.newKeySet(); /** * One-shot DEBUG when {@link WorldMapPointManager} is null during route clear (shutdown race). * Later races same JVM stay silent — intentional noise cap. @@ -990,7 +999,9 @@ static void clearWalkerDedupeForTesting() SEASONAL_HANDLER_MISS_LOGGED.clear(); SEASONAL_HANDLER_MISS_LOGGED_COUNT.set(0); WORLD_MAP_REMOVE_NULL_LOGGED.set(false); + testRecoveryReplanRequests.set(0); recentCurrentTileTransportByEdge.clear(); + TERMINAL_TRAVEL_ATTEMPTED_EDGES.clear(); clearRecentTransportContext(); resetRouteProgress(); } @@ -1121,14 +1132,18 @@ public static void recordPartialRetry(int attempt, int finalDist) { public static void recordUnreachable(String cause, WorldPoint player, WorldPoint target, WorldPoint pathEndpoint, int pathSize, int distanceThreshold, - Pathfinder pathfinder) { + Rs2RouteMetrics routeMetrics) { unreachableCount.incrementAndGet(); lastReason = "unreachable:" + cause; lastEventAtMs.set(System.currentTimeMillis()); int distToTarget = (pathEndpoint != null && target != null) ? pathEndpoint.distanceTo(target) : -1; - Pathfinder.PathfinderStats pfStats = (pathfinder != null) ? pathfinder.getStats() : null; - String stats = (pfStats != null) ? pfStats.toString() : "null"; - log.warn("[WalkerTelemetry] UNREACHABLE cause={} player={} target={} pathEndpoint={} pathSize={} endpointToTarget={} threshold={} pathfinderStats={} totalUnreachable={}", + String stats = routeMetrics == null ? "null" : String.format( + "RouteMetrics(nodes=%d,transports=%d,time=%dms,cost=%d)", + routeMetrics.getNodesChecked(), + routeMetrics.getTransportsChecked(), + routeMetrics.hasSearchNanos() ? routeMetrics.getSearchNanos() / 1_000_000L : -1L, + routeMetrics.getPathCost()); + log.warn("[WalkerTelemetry] UNREACHABLE cause={} player={} target={} pathEndpoint={} pathSize={} endpointToTarget={} threshold={} routeMetrics={} totalUnreachable={}", cause, player, target, pathEndpoint, pathSize, distToTarget, distanceThreshold, stats, unreachableCount.get()); } @@ -1300,11 +1315,9 @@ public static WalkerState walkWithState(WorldPoint target, int distance) { } } try { - if (config.walkWithBankedTransports()) { - return walkWithBankedTransportsAndState(target, distance, false); - } else { - return walkWithStateInternal(target, distance); - } + return withShadowExecutionEvidence(() -> config.walkWithBankedTransports() + ? walkWithBankedTransportsAndStateLocked(target, distance, false) + : walkWithStateInternal(target, distance)); } finally { walkerLock.unlock(); } @@ -1363,11 +1376,9 @@ public static WalkerState walkWithStateTry(WorldPoint target, int distance, long } try { - if (config.walkWithBankedTransports()) - { - return walkWithBankedTransportsAndStateLocked(target, distance, false); - } - return walkWithStateInternal(target, distance); + return withShadowExecutionEvidence(() -> config.walkWithBankedTransports() + ? walkWithBankedTransportsAndStateLocked(target, distance, false) + : walkWithStateInternal(target, distance)); } finally { @@ -1400,13 +1411,12 @@ private static WalkerState walkWithStateInternal(WorldPoint target, int distance return WalkerState.ARRIVED; } - final Pathfinder pathfinder = Rs2PathApi.getPathfinder(); - if (pathfinder != null && !pathfinder.isDone()) { + final Rs2ActiveRouteStatus routeStatus = Rs2PathApi.getActiveRouteStatus(); + if (routeStatus.isCalculating()) { return WalkerState.MOVING; } - boolean hasCurrentPath = pathfinder != null - && pathfinder.isDone() - && pathfinder.getTargets().contains(target); + boolean hasCurrentPath = routeStatus.isReady() + && routeStatus.getTargets().contains(target); if (!hasCurrentPath) { setTarget(target); } else { @@ -1495,8 +1505,8 @@ public static WalkerState walkStep(WorldPoint target, int distance) { setTarget(target); return WalkerState.MOVING; } - Pathfinder pathfinder = Rs2PathApi.getPathfinder(); - if (pathfinder == null || !pathfinder.isDone()) { + Rs2ActiveRouteStatus routeStatus = Rs2PathApi.getActiveRouteStatus(); + if (!routeStatus.isReady()) { return WalkerState.MOVING; // path still computing — wait, don't reset it } @@ -1505,8 +1515,8 @@ public static WalkerState walkStep(WorldPoint target, int distance) { return WalkerState.MOVING; } - final List rawPath = pathfinder.getPath(); - final List path = pathfinder.getWalkablePath(); + final List rawPath = routeStatus.getRawPath(); + final List path = routeStatus.getWalkablePath(); if (!walkStepPathReachesTarget(path, target, distance)) { setTarget(null, "rs2walker:walkStep:no-walkable-path"); return WalkerState.UNREACHABLE; @@ -1606,10 +1616,8 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part } // Pre-flight: a destination with no walkable tile within the arrival distance can never be // reached, so reject it here rather than after a full route ending at the nearest wall. - PathfinderConfig preflightConfig = Rs2PathApi.getPathfinderConfig(); - CollisionMap preflightMap = preflightConfig != null ? preflightConfig.getMap() : null; - if (!hasWalkableTileWithin(preflightMap, target, distance)) { - WorldPoint nearestWalkable = nearestWalkableTile(preflightMap, target, 48); + if (!Rs2PathApi.hasWalkableTileWithin(target, distance)) { + WorldPoint nearestWalkable = Rs2PathApi.nearestWalkableTile(target, 48); log.warn("[Walker] walk rejected: target {} has no walkable tile within {} in the collision map" + " (nearest walkable {}); check the destination coordinate", target, distance, @@ -1620,6 +1628,7 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part return WalkerState.UNREACHABLE; } int partialRetriesWorking = partialRetries; + int clientThreadTimeoutRetries = 0; // When the last partial retry was spent, so route progress made after it can refill the // budget. Without this the counter is monotonic for the entire walk. long lastPartialRetryAtMs = 0L; @@ -1650,15 +1659,19 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part return WalkerState.EXIT; } - Pathfinder pathfinder = Rs2PathApi.getPathfinder(); - if (pathfinder == null) { + Rs2ActiveRouteStatus routeStatus = Rs2PathApi.getActiveRouteStatus(); + if (!routeStatus.isPresent()) { markStartupPhase("pf_wait_enter", target, "reason=pathfinder_null"); walkerDiag("pathfinder null; waiting up to %dms", PATHFINDER_NULL_WAIT_MS); - pathfinder = sleepUntilNotNull(Rs2PathApi::getPathfinder, PATHFINDER_NULL_WAIT_MS); + Rs2WalkerRuntimeAwaits.awaitCondition( + () -> Rs2PathApi.getActiveRouteStatus().isPresent(), + 100, + PATHFINDER_NULL_WAIT_MS); + routeStatus = Rs2PathApi.getActiveRouteStatus(); if (walkCancelledDiag(target, "processWalk:after-wait-pathfinder", processWalkTail)) { return WalkerState.EXIT; } - if (pathfinder == null) { + if (!routeStatus.isPresent()) { if (currentTarget != null && currentTarget.equals(target)) { walkerDiag("pathfinder null but target still set; recalculating"); recalculatePath(); @@ -1671,17 +1684,24 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part markStartupPhase("pf_ready", target, "source=pathfinder_not_null"); } - if (!pathfinder.isDone()) { + if (routeStatus.isCalculating()) { + long observedGeneration = routeStatus.getGeneration(); markStartupPhase("pf_wait_retry", target, "slice=" + PATHFINDER_DONE_POLL_WAIT_MS); if (pathfinderPendingSinceMs == 0L) { pathfinderPendingSinceMs = System.currentTimeMillis(); } walkerDiag("pathfinder not done; short-poll max %dms", PATHFINDER_DONE_POLL_WAIT_MS); - boolean isDone = Rs2WalkerRuntimeAwaits.awaitPathfinderDone(pathfinder, PATHFINDER_DONE_POLL_WAIT_MS); + Rs2WalkerRuntimeAwaits.awaitCondition(() -> { + Rs2ActiveRouteStatus current = Rs2PathApi.getActiveRouteStatus(); + return !current.isPresent() + || current.getGeneration() != observedGeneration + || current.isReady(); + }, 100, PATHFINDER_DONE_POLL_WAIT_MS); + routeStatus = Rs2PathApi.getActiveRouteStatus(); if (walkCancelledDiag(target, "processWalk:after-wait-done", processWalkTail)) { return WalkerState.EXIT; } - if (!isDone) { + if (!routeStatus.isReady()) { if (System.currentTimeMillis() - pathfinderPendingSinceMs > 10_000L) { traceProcessWalkExit("pathfinder-timeout-not-done", target, processWalkTail); setTarget(null, "rs2walker:processWalk:pathfinder-timeout-not-done"); @@ -1696,13 +1716,21 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part markStartupPhase("pf_ready", target, "source=pathfinder_done"); } pathfinderPendingSinceMs = 0L; + captureActiveRouteComparisonEligibility(routeStatus.getGeneration()); + + if (consumeRecoveryReplanForTest()) + { + WebWalkLog.spDebug("test_recovery_replan | target={}", target); + recalculatePathForRecovery(); + continue; + } if (Rs2PathApi.getMarker() == null) { restoreTargetMarker(target); } - final List rawPath = pathfinder.getPath(); - final List path = pathfinder.getWalkablePath(); + final List rawPath = routeStatus.getRawPath(); + final List path = routeStatus.getWalkablePath(); final int[] smoothedToRaw = mapSmoothedToRaw(path, rawPath); int rawSize = rawPath == null ? -1 : rawPath.size(); int walkSize = path == null ? -1 : path.size(); @@ -1722,7 +1750,8 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part partialPath = true; } else { Telemetry.recordUnreachable("no-walkable-path", walkLoop.playerLoc, - target, dst, path == null ? 0 : path.size(), distance, pathfinder); + target, dst, path == null ? 0 : path.size(), distance, + routeStatus.getMetrics().orElse(null)); setTarget(null, "rs2walker:processWalk:no-walkable-path"); return WalkerState.UNREACHABLE; } @@ -1811,11 +1840,11 @@ private static WalkerState processWalk(WorldPoint target, int distance, int part if (immediateRouteTransportPending) { WebWalkLog.spDebug("stall_recovery_suppressed | reason=immediate-route-transport idx={}", earlyRouteStartIdx); } else if (!Rs2Player.isMoving() && !Rs2Player.isAnimating() && !Rs2Player.isInteracting()) { - setTarget(target); + recalculatePathForRecovery(); tryIssueRouteRecoveryClick(rawPath, path, target, distance, "stall recovery click"); continue; } else { - setTarget(target); + recalculatePathForRecovery(); continue; } } @@ -2102,7 +2131,7 @@ && shouldYieldForActiveRouteInterim(activeInterimPlayer, path, activeInterimNowM if (config.cancelInstead()) { setTarget(null, "rs2walker:processWalk:off-path-cancel-instead"); } else { - recalculatePath(); + recalculatePathForRecovery(); } exitReason = "not-near-path"; break; @@ -2521,11 +2550,8 @@ && handleTransportsInRawSegment(rawPath, rawEdgeStart, rawEdgeEnd, true)) { int recoverIdx = findForwardReachableRecoveryIndex(path, i, playerLoc, recoveryMinimapReach); if (recoverIdx < 0) { - recoverIdx = RouteRecovery.findFurthestClickableIndex(path, i, playerLoc, - wp -> { - Set ts = Rs2PathApi.getTransports().get(wp); - return ts != null && !ts.isEmpty(); - }, + recoverIdx = RouteRecovery.findFurthestClickableIndex(path, i, playerLoc, + Rs2PathApi::hasCatalogTransportOrigin, recoveryMinimapReach); } int minRecoveryIdx = Math.max(indexOfStartPoint, i); @@ -2544,12 +2570,10 @@ && handleTransportsInRawSegment(rawPath, rawEdgeStart, rawEdgeEnd, true)) { // (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 = Rs2PathApi.getPathfinderConfig(); - if (dangerCfg != null && dangerCfg.isAvoidDangerousNpcs() && recoverTarget != null - && dangerCfg.isDangerousAdjacentTile(WorldPointUtil.packWorldPoint(recoverTarget))) { + if (Rs2PathApi.shouldAvoidDangerousTile(recoverTarget)) { int safeIdx = recoverIdx; while (safeIdx > minRecoveryIdx - && dangerCfg.isDangerousAdjacentTile(WorldPointUtil.packWorldPoint(path.get(safeIdx)))) { + && Rs2PathApi.shouldAvoidDangerousTile(path.get(safeIdx))) { safeIdx--; } recoverIdx = safeIdx; @@ -2564,9 +2588,7 @@ && handleTransportsInRawSegment(rawPath, rawEdgeStart, rawEdgeEnd, true)) { Rs2Walker::isKnownWalkableOrUnloaded); if (rawRecoveryTarget != null && !rawRecoveryTarget.equals(playerLoc) - && (dangerCfg == null - || !dangerCfg.isAvoidDangerousNpcs() - || !dangerCfg.isDangerousAdjacentTile(WorldPointUtil.packWorldPoint(rawRecoveryTarget)))) { + && !Rs2PathApi.shouldAvoidDangerousTile(rawRecoveryTarget)) { recoverTarget = rawRecoveryTarget; } // Prefer walking onto the reachable transport / agility-shortcut origin the unified @@ -2605,7 +2627,7 @@ && isMovementWalkerOwned(System.currentTimeMillis(), lastAttemptedMinimapClickAt routeState.lastWalledRecoveryReplanAtMs = System.currentTimeMillis(); WebWalkLog.spInfo("recovery_target_walled | to={} player={} replanning", compactWorldPoint(recoverTarget), compactWorldPoint(playerLoc)); - recalculatePath(); + recalculatePathForRecovery(); exitReason = "recovery-target-walled-replan"; break; } @@ -2767,11 +2789,8 @@ && walkFastCanvas(recoverTarget)) { routeState.interimLastRetargetAtMs = 0L; } - int targetIdx = RouteRecovery.findFurthestForwardClickableIndex(path, i, playerLoc, - wp -> { - Set ts = Rs2PathApi.getTransports().get(wp); - return ts != null && !ts.isEmpty(); - }, + int targetIdx = RouteRecovery.findFurthestForwardClickableIndex(path, i, playerLoc, + Rs2PathApi::hasCatalogTransportOrigin, MINIMAP_REACH_EUCLIDEAN); WorldPoint targetWp = path.get(targetIdx); // If the forward waypoint is outside minimap reach, interpolate a @@ -3141,7 +3160,8 @@ && walkFastCanvas(recoverTarget)) { // than the actual shortfall. WorldPoint unreachableEndpoint = path.isEmpty() ? null : path.get(path.size() - 1); Telemetry.recordUnreachable("partial-retries-exhausted", Rs2Player.getWorldLocation(), - target, unreachableEndpoint, path.size(), distance, Rs2PathApi.getPathfinder()); + target, unreachableEndpoint, path.size(), distance, + Rs2PathApi.getActiveRouteStatus().getMetrics().orElse(null)); setTarget(null, "rs2walker:processWalk:partial-retries-exhausted"); return WalkerState.UNREACHABLE; } else { @@ -3209,6 +3229,16 @@ && walkFastCanvas(recoverTarget)) { setTarget(null, "rs2walker:processWalk:interrupted-exception"); return WalkerState.EXIT; } + if (isClientThreadReadTimeout(ex) + && clientThreadTimeoutRetries < CLIENT_THREAD_TIMEOUT_RETRIES + && Objects.equals(currentTarget, target) + && !Thread.currentThread().isInterrupted()) { + int nextRetry = ++clientThreadTimeoutRetries; + WebWalkLog.spInfo("client_thread_timeout_retry | attempt={}/{} target={}", + nextRetry, CLIENT_THREAD_TIMEOUT_RETRIES, target); + processWalkTail--; + continue; + } log.error("Exception in Rs2Walker:", ex); WebWalkLog.interruptedExit("walker exception exit (403)"); traceProcessWalkExit("exception-" + ex.getClass().getSimpleName(), target, MAX_PROCESS_WALK_TAIL_ITERATIONS - 1); @@ -3992,11 +4022,8 @@ private static boolean tryIssueRouteMovementClick(List rawPath, // pathfinder makes the idle nudge issue the first click instead of the main loop). WorldPoint clickTarget = selectRouteClickTarget(rawPath, playerLoc, maxEuclidean, rawAnchorIndex); if (clickTarget == null) { - int clickableIdx = RouteRecovery.findFurthestForwardClickableIndex(path, startIdx, playerLoc, - wp -> { - Set ts = Rs2PathApi.getTransports().get(wp); - return ts != null && !ts.isEmpty(); - }, + int clickableIdx = RouteRecovery.findFurthestForwardClickableIndex(path, startIdx, playerLoc, + Rs2PathApi::hasCatalogTransportOrigin, maxEuclidean); clickableIdx = Math.max(startIdx, Math.min(clickableIdx, path.size() - 1)); clickTarget = path.get(clickableIdx); @@ -4227,12 +4254,8 @@ public static WorldPoint walkCanvas(WorldPoint worldPoint) { * @return total amount of tiles */ public static int getTotalTiles(WorldPoint start, WorldPoint destination) { - if (Rs2PathApi.getPathfinderConfig().getTransports().isEmpty()) { - Rs2PathApi.getPathfinderConfig().refresh(); - } - Pathfinder pathfinder = new Pathfinder(Rs2PathApi.getPathfinderConfig(), start, destination); - pathfinder.run(); - List path = pathfinder.getPath(); + Rs2RouteResult route = Rs2PathApi.plan(Rs2RouteRequest.to(start, destination)); + List path = route.getPath(); 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( @@ -4302,7 +4325,6 @@ public static int getTotalTiles(WorldPoint destination) { // takes an avg 200-300 ms // Used mainly for agility, might have to tweak this for other stuff public static boolean canReach(WorldPoint worldPoint, int sizeX, int sizeY, int pathSizeX, int pathSizeY,boolean useBankedItems) { - boolean originalUseBankItems = Rs2PathApi.getPathfinderConfig().isUseBankItems(); WorldArea pathArea = null; // Create centered WorldArea for the object instead of corner-based @@ -4314,16 +4336,14 @@ public static boolean canReach(WorldPoint worldPoint, int sizeX, int sizeY, int WorldArea objectArea = new WorldArea(objectSouthWest, sizeX + 2, sizeY + 2); try { - Rs2PathApi.getPathfinderConfig().setUseBankItems(useBankedItems); - Rs2PathApi.getPathfinderConfig().refresh(worldPoint); - if (Rs2PathApi.getPathfinderConfig().getTransports().isEmpty()) { - Rs2PathApi.getPathfinderConfig().refresh(worldPoint); - } - Pathfinder pathfinder = new Pathfinder(Rs2PathApi.getPathfinderConfig(), Rs2Player.getWorldLocation(), worldPoint); - pathfinder.run(); + Rs2RouteResult route = Rs2PathApi.plan( + Rs2RouteRequest.to(Rs2Player.getWorldLocation(), worldPoint) + .withRefreshTarget(worldPoint) + .withBankItems(useBankedItems)); // Create centered WorldArea for the path endpoint instead of corner-based - WorldPoint pathEndpoint = pathfinder.getPath().get(pathfinder.getPath().size() - 1); + WorldPoint pathEndpoint = route.getEndpoint().orElseThrow( + () -> new IllegalStateException("planner returned no endpoint")); WorldPoint pathSouthWest = new WorldPoint( pathEndpoint.getX() - pathSizeX / 2, pathEndpoint.getY() - pathSizeY / 2, @@ -4333,9 +4353,6 @@ public static boolean canReach(WorldPoint worldPoint, int sizeX, int sizeY, int } catch (Exception e) { log.trace("Exception in canReach: {} - ", e.getMessage(), e); return false; - } finally { - Rs2PathApi.getPathfinderConfig().setUseBankItems(originalUseBankItems); - Rs2PathApi.getPathfinderConfig().refresh(worldPoint); } return pathArea != null ? pathArea.intersectsWith2D(objectArea) : false; } @@ -4377,16 +4394,17 @@ public static boolean canReach(WorldPoint worldPoint, int sizeX, int sizeY, bool */ public static List getWalkPath(WorldPoint start, WorldPoint target) { long startTime = System.nanoTime(); - Rs2PathApi.getPathfinderConfig().refresh(target); - long pathfinderStartTime = System.nanoTime(); - Pathfinder pathfinderLocal = new Pathfinder(Rs2PathApi.getPathfinderConfig(), start, target); - pathfinderLocal.run(); - List path = pathfinderLocal.getPath(); - long pathfinderEndTime = System.nanoTime(); + Rs2RouteResult route = Rs2PathApi.plan( + Rs2RouteRequest.to(start, target) + .withRefreshTarget(target) + .withRefreshPolicy(Rs2RouteRequest.RefreshPolicy.ALWAYS)); + List path = route.getPath(); long totalEndTime = System.nanoTime(); - double configTimeMs = (pathfinderStartTime - startTime) / 1_000_000.0; - double pathfinderTimeMs = (pathfinderEndTime - pathfinderStartTime) / 1_000_000.0; + double pathfinderTimeMs = route.hasSearchNanos() + ? route.getSearchNanos() / 1_000_000.0 + : 0.0; double totalTimeMs = (totalEndTime - startTime) / 1_000_000.0; + double configTimeMs = Math.max(0.0, totalTimeMs - pathfinderTimeMs); StringBuilder performanceLog = new StringBuilder(); performanceLog.append("getWalkPath Performance: ") @@ -4586,40 +4604,7 @@ private static Map buildPathFirstIndex(List pat * @return The filtered and processed list of transports */ 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.SPIRIT_TREE || - (t.getType() == TransportType.TRANSPORT && t.getCurrencyAmount() > 0) || - (t.getType() == TransportType.SEASONAL_TRANSPORT - && Rs2LeaguesTransport.isLeaguesActive() - && t.getDisplayInfo() != null - && t.getDisplayInfo().toLowerCase().startsWith("leagues area:"))) - .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.setItemIdRequirements(Set.of(Set.of( - 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) { - 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); - } - } - }) - .collect(Collectors.toList()); + return Rs2WalkerBankingPlanner.applyTransportFiltering(transports); } @@ -4752,9 +4737,9 @@ && isRawTransportOriginNearPlayer(rawPath, ri, playerLoc, RAW_TRANSPORT_DISPATCH } // (3) Reachable transport / agility-shortcut origin ahead: wide forward-window scan. - WorldPoint shortcutOrigin = RouteRecovery.findReachableTransportOriginAhead( - rawPath, playerRawIdx, playerLoc, - reachableTilesCache.keySet(), Rs2PathApi.getTransports(), + WorldPoint shortcutOrigin = RouteRecovery.findReachableTransportOriginAhead( + rawPath, playerRawIdx, playerLoc, + reachableTilesCache.keySet(), Rs2PathApi::hasCatalogTransportOrigin, recoveryMinimapReach - 1, ROUTE_PROGRESS_FORWARD_SEARCH_TILES); if (shortcutOrigin != null && !shortcutOrigin.equals(playerLoc)) { return ObstacleResolution.walkToOrigin(shortcutOrigin); @@ -5030,7 +5015,7 @@ private static boolean hasPendingDoorLikeSceneObjectBeforeDirectClick(List radius && to.distanceTo2D(playerLoc) > radius) { break; } - if (isCatalogBackedTransportSegment(route, i) && !isDoorLikeCatalogTransportSegment(route, i)) { + if (shouldDeferDoorHandlingToTransport(route, i)) { continue; } if (hasDoorLikeSceneObjectOnSegment(from, to, playerLoc, radius)) { @@ -5076,7 +5061,7 @@ private static boolean handlePendingDoorBeforeRouteClick(List rawPat if (a.distanceTo2D(playerLoc) > HANDLER_RANGE && b.distanceTo2D(playerLoc) > HANDLER_RANGE) { continue; } - if (isCatalogBackedTransportSegment(rawPath, ri) && !isDoorLikeCatalogTransportSegment(rawPath, ri)) { + if (shouldDeferDoorHandlingToTransport(rawPath, ri)) { continue; } if (!hasDoorLikeSceneObjectOnSegment(a, b, playerLoc, HANDLER_RANGE)) { @@ -5134,7 +5119,7 @@ private static boolean handlePendingDoorNearRawPath(List rawPath, if (a.distanceTo2D(playerLoc) > HANDLER_RANGE && b.distanceTo2D(playerLoc) > HANDLER_RANGE) { continue; } - if (isCatalogBackedTransportSegment(rawPath, ri) && !isDoorLikeCatalogTransportSegment(rawPath, ri)) { + if (shouldDeferDoorHandlingToTransport(rawPath, ri)) { continue; } if (!hasDoorLikeSceneObjectOnSegment(a, b, playerLoc, HANDLER_RANGE)) { @@ -5173,7 +5158,7 @@ private static boolean handleUnresolvedDoorNearRawPath(List rawPath, if (from.distanceTo2D(playerLoc) > radiusTiles && to.distanceTo2D(playerLoc) > radiusTiles) { continue; } - if (isCatalogBackedTransportSegment(rawPath, ri) && !isDoorLikeCatalogTransportSegment(rawPath, ri)) { + if (shouldDeferDoorHandlingToTransport(rawPath, ri)) { continue; } if (!hasUnresolvedDoorLikeSceneObjectOnSegment(from, to, playerLoc, radiusTiles)) { @@ -5573,7 +5558,7 @@ private static boolean hasDoorCandidateOnRawSegment(List rawPath, in if (rawPath == null || index < 0 || index >= rawPath.size() - 1) { return false; } - if (isCatalogBackedTransportSegment(rawPath, index) && !isDoorLikeCatalogTransportSegment(rawPath, index)) { + if (shouldDeferDoorHandlingToTransport(rawPath, index)) { return false; } boolean isInstance = Microbot.getClient() @@ -5642,32 +5627,26 @@ private static boolean handleCurrentTileTransportTowardPath(List raw return false; } - // Snappy proximity: consider usable transports whose origin is reachable within a few tiles - // of the player, not just the one on the exact player tile. NPC/"Follow" transports (e.g. Elkoy - // in the Tree Gnome Village maze) roam and sit a tile off the planned path, so exact-tile - // matching never sees them. The destination-on-forward-route gate below keeps this safe against - // off-path loops, and getTransports() is already the usable (config/quest/level-filtered) set, - // so we never grab a transport the pathfinder excluded. + // Snappy proximity: consider exact planned transports whose origin is reachable within a few + // tiles of the player, not just one on the exact player tile. NPC/"Follow" transports (e.g. + // Elkoy in the Tree Gnome Village maze) roam and sit a tile off the planned path. The old code + // rescanned every usable catalog row and inferred selection from destination membership; that is + // ambiguous when multiple transports share an edge. The completed route now supplies both order + // and exact identity. final int NEARBY_TRANSPORT_REACH = 5; - Map> transportsByOrigin = Rs2PathApi.getTransports(); - Set transports = new HashSet<>(); - Set transportsOnPlayerTile = transportsByOrigin.get(playerLoc); - if (transportsOnPlayerTile != null) { - transports.addAll(transportsOnPlayerTile); - } - for (WorldPoint reachableTile : Rs2Tile.getReachableTilesFromTile(playerLoc, NEARBY_TRANSPORT_REACH).keySet()) { - Set ts = transportsByOrigin.get(reachableTile); - if (ts != null) { - transports.addAll(ts); - } - } - if (transports.isEmpty()) { + Set reachableOrigins = new HashSet<>( + Rs2Tile.getReachableTilesFromTile(playerLoc, NEARBY_TRANSPORT_REACH).keySet()); + reachableOrigins.add(playerLoc); + List plannedSelections = + Rs2PathApi.getActiveTransportSelections(rawPath); + if (plannedSelections.isEmpty()) { return false; } Map forwardIndex = new HashMap<>(); addForwardPathIndices(forwardIndex, rawPath, playerLoc); addForwardPathIndices(forwardIndex, path, playerLoc); + int rawClosestIndex = Math.max(0, getClosestTileIndex(rawPath, playerLoc)); WorldPoint priorOrigin = routeState.lastTransportOriginLocation; // Trust the pathfinder: only take a nearby transport whose destination is on the @@ -5678,21 +5657,29 @@ private static boolean handleCurrentTileTransportTowardPath(List raw // the pathfinder never chose: it looped forever on the Mor Ul Rek cave entrance/exit and // stalled clicking the Fossil Island rowboat. The pathfinder already routed every transport // it wants onto the path, so on-route membership is the correct, region-safe admission test. - List candidates = transports.stream() - .filter(t -> t.getDestination() != null) + List candidates = plannedSelections.stream() + // One-edge backtrack permits standing just past an interaction origin while preventing + // a repeated destination later in the route from reviving an already-passed transport. + .filter(selection -> selection.getPathIndex() >= Math.max(0, rawClosestIndex - 1)) + .filter(selection -> { + Transport transport = selection.getLocalExecutionTransport(); + WorldPoint origin = transport.getOrigin(); + return origin == null || reachableOrigins.contains(origin); + }) // Local adjacent same-plane edges (doors/gates) are handled by segment door/object // logic; current-tile transport probing can bounce on these and create loops. - .filter(t -> !isAdjacentSamePlaneTransport(t)) - .filter(t -> priorOrigin == null - || !t.getDestination().equals(priorOrigin)) - .filter(t -> target == null + .filter(selection -> !isAdjacentSamePlaneTransport(selection.getLocalExecutionTransport())) + .filter(selection -> priorOrigin == null + || !selection.getEdge().getDestination().equals(priorOrigin)) + .filter(selection -> target == null || playerLoc.getPlane() != target.getPlane() - || t.getDestination().getPlane() == target.getPlane()) - .filter(t -> forwardIndex.containsKey(t.getDestination())) - .sorted(Comparator.comparingInt(t -> forwardIndex.get(t.getDestination()))) + || selection.getEdge().getDestination().getPlane() == target.getPlane()) + .filter(selection -> forwardIndex.containsKey(selection.getEdge().getDestination())) + .sorted(Comparator.comparingInt(Rs2PathApi.ActiveTransportSelection::getPathIndex)) .collect(Collectors.toList()); - for (Transport transport : candidates) { + for (Rs2PathApi.ActiveTransportSelection selection : candidates) { + Transport transport = selection.getLocalExecutionTransport(); WorldPoint origin = transport.getOrigin() != null ? transport.getOrigin() : playerLoc; if (shouldThrottleCurrentTileTransportAttempt(origin, transport.getDestination())) { continue; @@ -5702,7 +5689,7 @@ private static boolean handleCurrentTileTransportTowardPath(List raw // Pass the transport's own origin so handleTransports walks the short hop to it before // interacting (NPC dispatch already auto-walks via canWalkTo + interact); object/door // interactions that can't be reached from here simply return false and we fall through. - if (handleTransports(Arrays.asList(origin, transport.getDestination()), 0)) { + if (handleSelectedTransport(Arrays.asList(origin, transport.getDestination()), 0, selection)) { if (didCurrentTileTransportProgress(before, transport.getDestination(), target)) { log.info("[Walker] Nearby transport handler resolved obstacle: origin={} dest={} (player {})", origin, transport.getDestination(), playerLoc); @@ -5851,7 +5838,7 @@ private static boolean handleDoors(List path, int index) { } private static boolean handleDoors(List path, int index, boolean allowSegmentProbe) { - if (Rs2PathApi.getPathfinder() == null || index >= path.size() - 1) return false; + if (!Rs2PathApi.getActiveRouteStatus().isPresent() || index >= path.size() - 1) return false; // Skip any door whose tile was blacklisted after a prior quest-lock detection — // avoid re-triggering the same failed interact loop this session. @@ -5895,7 +5882,7 @@ private static boolean handleDoors(List path, int index, boolean all return false; } - if (isCatalogBackedTransportSegment(path, index) && !isDoorLikeCatalogTransportSegment(path, index)) { + if (shouldDeferDoorHandlingToTransport(path, index)) { return false; } @@ -6053,9 +6040,7 @@ private static boolean handleDoors(List path, int index, boolean all probe, name, action, dialogue); sessionBlacklistedDoors.add(probe); Rs2Dialogue.clickContinue(); - if (Rs2PathApi.getPathfinderConfig() != null) { - Rs2PathApi.getPathfinderConfig().refresh(); - } + Rs2PathApi.refreshPlanningConfiguration(); recalculatePath(); // Resolved by rerouting; return before the wrong-traversal branch so a // quest/skill-locked door is never learned as a blocked edge (it unlocks when the @@ -6071,10 +6056,8 @@ private static boolean handleDoors(List path, int index, boolean all // so persist it as a learned block that survives restarts and reroutes future paths. // (Quest/skill-locked doors take the isQuestLockedDoorDialogue() branch above and are // deliberately NOT learned — they unlock when the requirement is met.) - if (Rs2PathApi.getPathfinderConfig() != null) { - Rs2PathApi.getPathfinderConfig().learnBlockedEdge(fromWp, toWp, - "wrong-traversal door @ " + compactWorldPoint(probe)); - } + Rs2PathApi.learnBlockedEdge(fromWp, toWp, + "wrong-traversal door @ " + compactWorldPoint(probe)); } if (doorStillHasAction(probe, fromWp, toWp, doorActions, action)) { log.debug("[Walker] Door interaction did not traverse; action still present at {} ({} -> {})", @@ -6200,9 +6183,7 @@ private static boolean tryHandleDoorObject(TileObject object, WorldPoint probe, probe, name, action, dialogue); sessionBlacklistedDoors.add(probe); Rs2Dialogue.clickContinue(); - if (Rs2PathApi.getPathfinderConfig() != null) { - Rs2PathApi.getPathfinderConfig().refresh(); - } + Rs2PathApi.refreshPlanningConfiguration(); recalculatePath(); return true; } @@ -6922,16 +6903,12 @@ private static boolean wasStationaryDoorOpenedRecently(WorldPoint doorTile) { return true; } - /** - * Strict catalog step: path tile equals transport origin in {@link Rs2PathApi#getTransports()} - * and next tile equals that row's destination. Used where the walker must dispatch {@code handleTransports} - * from the path index (origin keyed in the TSV-fed multimap). - */ + /** Exact selected transport step retained by the completed active route. */ private static boolean hasExplicitTransportStep(List path, int index) { if (path == null || index < 0 || index >= path.size() - 1) { return false; } - return matchesDirectedTransportCatalogEdge(path.get(index), path.get(index + 1)); + return Rs2PathApi.getActiveTransportEdge(path.get(index), path.get(index + 1)).isPresent(); } /** @@ -7036,27 +7013,41 @@ private static boolean isDoorLikeCatalogTransportSegment(WorldPoint from, WorldP || hasDoorLikeAdjacentOriginShortTransportHop(to, from); } - private static boolean matchesDirectedTransportCatalogEdge(WorldPoint origin, WorldPoint dest) { - if (origin == null || dest == null) { - return false; - } - Set transports = Rs2PathApi.getTransports().get(origin); - if (transports == null || transports.isEmpty()) { + /** + * Catalog transports normally bypass generic door probing so the exact selected edge keeps + * execution ownership. Door-like rows are the compatibility exception because many ordinary + * Open/Pass rows still rely on the door cascade. The Al Kharid toll gate has an explicit + * executor with dialogue and exact-landing semantics, so allowing the generic scanner to take + * it first creates two conflicting completion contracts. + */ + private static boolean shouldDeferDoorHandlingToTransport(List path, int index) { + if (!isCatalogBackedTransportSegment(path, index)) { return false; } - return transports.stream().anyMatch(t -> Objects.equals(t.getDestination(), dest)); + return !isDoorLikeCatalogTransportSegment(path, index) + || isAlKharidTollGateSegment(path.get(index), path.get(index + 1)); } + static boolean isAlKharidTollGateSegment(WorldPoint from, WorldPoint to) { + return from != null + && to != null + && AL_KHARID_TOLL_GATE_POINTS.contains(from) + && AL_KHARID_TOLL_GATE_POINTS.contains(to) + && from.getPlane() == to.getPlane() + && Math.abs(from.getX() - to.getX()) == 1 + && from.getY() == to.getY(); + } + + private static boolean matchesDirectedTransportCatalogEdge(WorldPoint origin, WorldPoint dest) { + return Rs2PathApi.hasCatalogTransportEdge(origin, dest); + } + private static boolean hasDoorLikeDirectedCatalogTransport(WorldPoint origin, WorldPoint dest) { if (origin == null || dest == null) { return false; } - Set transports = Rs2PathApi.getTransports().get(origin); - if (transports == null || transports.isEmpty()) { - return false; - } - return transports.stream() - .anyMatch(t -> Objects.equals(t.getDestination(), dest) && Rs2DoorProbe.isDoorLikeCatalogTransport(t)); + return Rs2PathApi.getCatalogTransportEdges(origin).stream() + .anyMatch(t -> Objects.equals(t.getDestination(), dest) && Rs2DoorProbe.isDoorLikeCatalogTransport(t)); } /** @@ -7073,12 +7064,8 @@ private static boolean matchesAdjacentOriginShortTransportHop(WorldPoint from, W continue; } WorldPoint catalogOrigin = new WorldPoint(from.getX() + dx, from.getY() + dy, from.getPlane()); - Set transports = Rs2PathApi.getTransports().get(catalogOrigin); - if (transports == null || transports.isEmpty()) { - continue; - } - for (Transport t : transports) { - if (Objects.equals(t.getDestination(), to) && isAdjacentSamePlaneTransport(t)) { + for (Rs2TransportEdge t : Rs2PathApi.getCatalogTransportEdges(catalogOrigin)) { + if (Objects.equals(t.getDestination(), to) && isAdjacentSamePlaneTransport(t)) { return true; } } @@ -7097,11 +7084,7 @@ private static boolean hasDoorLikeAdjacentOriginShortTransportHop(WorldPoint fro continue; } WorldPoint catalogOrigin = new WorldPoint(from.getX() + dx, from.getY() + dy, from.getPlane()); - Set transports = Rs2PathApi.getTransports().get(catalogOrigin); - if (transports == null || transports.isEmpty()) { - continue; - } - for (Transport t : transports) { + for (Rs2TransportEdge t : Rs2PathApi.getCatalogTransportEdges(catalogOrigin)) { if (Objects.equals(t.getDestination(), to) && isAdjacentSamePlaneTransport(t) && Rs2DoorProbe.isDoorLikeCatalogTransport(t)) { @@ -7293,7 +7276,7 @@ private static boolean hasUnresolvedDoorLikeObjectNearRawPath(List r if (from.distanceTo2D(playerLoc) > radiusTiles && to.distanceTo2D(playerLoc) > radiusTiles) { continue; } - if (isCatalogBackedTransportSegment(rawPath, ri) && !isDoorLikeCatalogTransportSegment(rawPath, ri)) { + if (shouldDeferDoorHandlingToTransport(rawPath, ri)) { continue; } if (hasUnresolvedDoorLikeSceneObjectOnSegment(from, to, playerLoc, radiusTiles)) { @@ -8358,16 +8341,60 @@ private static HashMap getClosestIndexReachableTiles(WorldP if (playerLoc == null) { return new HashMap<>(); } - HashMap tiles = Rs2Tile.getReachableTilesFromTile(playerLoc, CLOSEST_INDEX_REACHABLE_STEP_BUDGET); + HashMap tiles; + try { + tiles = Rs2Tile.getReachableTilesFromTile( + playerLoc, CLOSEST_INDEX_REACHABLE_STEP_BUDGET); + } catch (RuntimeException failure) { + if (!isClientThreadReadTimeout(failure)) { + throw failure; + } + WebWalkLog.spInfo("client_thread_timeout_fallback | op=closest_route_index"); + return nearbyTilesIgnoringCollision( + playerLoc, CLOSEST_INDEX_REACHABLE_STEP_BUDGET); + } // If an animation/shortcut puts the player on a collision-odd tile, keep route progress // anchored by distance instead of repeatedly recalculating an empty reachable set. if (tiles.isEmpty()) { - tiles = Rs2Tile.getReachableTilesFromTileIgnoreCollision(playerLoc, CLOSEST_INDEX_REACHABLE_STEP_BUDGET); + tiles = nearbyTilesIgnoringCollision( + playerLoc, CLOSEST_INDEX_REACHABLE_STEP_BUDGET); } return tiles; } + static boolean isClientThreadReadTimeout(Throwable failure) { + Throwable current = failure; + while (current != null) { + if (current instanceof TimeoutException) { + return true; + } + current = current.getCause(); + } + return false; + } + + static HashMap nearbyTilesIgnoringCollision( + WorldPoint origin, int radius) { + HashMap result = new HashMap<>(); + if (origin == null || radius < 0) { + return result; + } + int boundedRadius = Math.min(radius, CLOSEST_INDEX_REACHABLE_STEP_BUDGET); + for (int dx = -boundedRadius; dx <= boundedRadius; dx++) { + for (int dy = -boundedRadius; dy <= boundedRadius; dy++) { + int distance = Math.max(Math.abs(dx), Math.abs(dy)); + if (distance <= boundedRadius) { + result.put(new WorldPoint( + origin.getX() + dx, + origin.getY() + dy, + origin.getPlane()), distance); + } + } + } + return result; + } + static int stabilizeRouteProgressIndex(List path, int closestIdx, WorldPoint target, WorldPoint playerLoc) { if (path == null || path.isEmpty() || closestIdx < 0 || closestIdx >= path.size()) { return closestIdx; @@ -8544,13 +8571,53 @@ private static boolean isRecentTransportEdgeCandidate(WorldPoint objectLoc, Worl * Force the walker to recalculate path */ public static void recalculatePath() { + recalculatePath(Rs2PlannerShadowContext.Invocation.ACTIVE_REPLAN); + } + + /** + * Queue one deterministic recovery replan for the active live-test walk. + * + *

The request is consumed by {@code processWalk} on the walker thread so the normal recovery evidence + * context is updated. Production callers cannot enable this hook: it is inert unless the test runner set + * {@code microbot.test.mode=true} and an active target exists.

+ */ + public static boolean requestRecoveryReplanForTest() + { + if (!Boolean.getBoolean("microbot.test.mode") || currentTarget == null) + { + return false; + } + testRecoveryReplanRequests.incrementAndGet(); + return true; + } + + static boolean consumeRecoveryReplanForTest() + { + if (!Boolean.getBoolean("microbot.test.mode")) + { + testRecoveryReplanRequests.set(0); + return false; + } + return testRecoveryReplanRequests.getAndUpdate(value -> Math.max(0, value - 1)) > 0; + } + + private static void recalculatePathForRecovery() { + WalkEvidenceContext evidence = walkEvidenceContext.get(); + if (evidence != null) + { + evidence.recoveryTriggered = true; + } + recalculatePath(Rs2PlannerShadowContext.Invocation.RECOVERY_REPLAN); + } + + private static void recalculatePath(Rs2PlannerShadowContext.Invocation invocation) { WorldPoint goal = currentTarget; if (goal == null) { return; } // Must not call setTarget(null)+setTarget(goal): that briefly clears {@link #currentTarget}, // and processWalk on another thread treats null as cancel (isWalkCancelled). - Rs2WalkerLifecycleRuntime.applyWalkerDestination(goal); + Rs2WalkerLifecycleRuntime.applyWalkerDestination(goal, invocation); } /** @@ -8600,18 +8667,7 @@ public static void setTarget(WorldPoint target, String clearReasonWhenNull) { clearRecentTransportContext(); resetRouteProgress(); logRouteClear(clearReasonWhenNull); - synchronized (Rs2PathApi.getPathfinderMutex()) { - final Pathfinder pathfinder = Rs2PathApi.getPathfinder(); - if (pathfinder != null) { - pathfinder.cancel(); - } - Future pathfinderFuture = Rs2PathApi.getPathfinderFuture(); - if (pathfinderFuture != null && !pathfinderFuture.isDone()) { - pathfinderFuture.cancel(true); - } - Rs2PathApi.setPathfinderFuture(null); - Rs2PathApi.setPathfinder(null); - } + Rs2PathApi.cancelAndClearActiveRoute(); WorldMapPointManager wmm = Microbot.getWorldMapPointManager(); if (wmm != null) { @@ -8688,18 +8744,45 @@ public static Tile getTile(WorldPoint point) { * @return */ private static boolean handleTransports(List path, int indexOfStartPoint) { - if (path != null && indexOfStartPoint >= 0 && indexOfStartPoint < path.size() - 1 - && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(indexOfStartPoint + 1))) { + Optional selection = + Rs2PathApi.getActiveTransportSelection(path, indexOfStartPoint); + if (selection.isEmpty()) { + return false; + } + return handleSelectedTransport(path, indexOfStartPoint, selection.get()); + } + + /** + * Executes the exact transport retained by the active route through its registered Microbot executor. + * Candidate discovery must happen through immutable route steps, never by rescanning the mutable + * transport catalog. The local transport payload is isolated here because POH execution still carries + * subtype behavior that is not part of the planner-independent edge value. + */ + private static boolean handleSelectedTransport(List path, + int indexOfStartPoint, + Rs2PathApi.ActiveTransportSelection selection) { + if (selection == null || !selection.isExecutable()) { + if (selection != null) { + WebWalkLog.spWarn("selected transport has no executor | type={} origin={} dest={}", + selection.getEdge().getType(), + compactWorldPoint(selection.getEdge().getOrigin()), + compactWorldPoint(selection.getEdge().getDestination())); + } + return false; + } + Transport selectedTransport = selection.getLocalExecutionTransport(); + Rs2TerminalTravelMode terminalTravelMode = selection.getEdge().getTerminalTravelMode(); + if (path == null || selectedTransport == null + || indexOfStartPoint < 0 || indexOfStartPoint >= path.size()) { return false; } - Set transports = Rs2PathApi.getTransports().get(path.get(indexOfStartPoint)); - if (transports == null || transports.isEmpty()) { + if (path != null && indexOfStartPoint >= 0 && indexOfStartPoint < path.size() - 1 + && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(indexOfStartPoint + 1))) { return false; } if (log.isDebugEnabled()) { - log.debug("[Walker] handleTransports at {}: {} candidates — {}", path.get(indexOfStartPoint), - transports.size(), - transports.stream().map(Transport::getDisplayInfo).collect(Collectors.joining(", "))); + log.debug("[Walker] handleTransports at {}: exact planned candidate — {} executor={}", + path.get(indexOfStartPoint), selectedTransport.getDisplayInfo(), selection.getExecutor()); } // When the player is inside a POH instance, the player's raw world-location plane is // the instance-template plane and has no relationship to the POH-transport origin plane. @@ -8713,10 +8796,7 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i pathFirstIndex.putIfAbsent(path.get(idx), idx); } - List orderedTransports = new ArrayList<>(transports); - orderedTransports.sort(Comparator.comparingInt(Rs2Walker::transportHandlingPreference)); - - for (Transport transport : orderedTransports) { + for (Transport transport : Collections.singletonList(selectedTransport)) { Collection worldPointCollections; //in some cases the getOrigin is null, for teleports that start the player location if (transport.getOrigin() == null) { @@ -8732,6 +8812,7 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i } log.debug("[Walker] Considering transport: {} (type={}, origin={}, wpCount={})", transport.getDisplayInfo(), transport.getType(), transport.getOrigin(), worldPointCollections.size()); + originLoop: for (WorldPoint origin : worldPointCollections) { WorldPoint plOriginLoop = Rs2Player.getWorldLocation(); if (!inPohInstance && transport.getOrigin() != null && plOriginLoop != null @@ -8795,68 +8876,155 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i } 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()); + if (selection.getExecutor() == Rs2TransportExecutor.BARROWS_DIG) { + WorldPoint digOrigin = transport.getOrigin(); + WorldPoint playerAtMound = Rs2Player.getWorldLocation(); + if (digOrigin == null || playerAtMound == null || !playerAtMound.equals(digOrigin)) { + // Digging is tile-sensitive. Let the ordinary path click finish the + // approach instead of firing the spade from an adjacent mound tile. + return false; + } + boolean dug = attemptObserved(transport, + () -> Rs2Inventory.interact(ItemID.SPADE, "Dig")); + if (!dug) { + return false; + } + boolean enteredCrypt = Rs2WalkerRuntimeAwaits.awaitCondition( + () -> isPlayerWithinChebyshevOf( + transport.getDestination(), TRANSPORT_NEAR_LANDING_CHEBYSHEV), + TRANSPORT_LANDING_WAIT_POLL_MS, + TRANSPORT_LANDING_WAIT_TIMEOUT_MS); + if (enteredCrypt) { + return finishHandledTransport(transport); + } + WebWalkLog.spWarn( + "Barrows dig post-travel wait timed out ({}ms) dest={} at={}", + TRANSPORT_LANDING_WAIT_TIMEOUT_MS, + compactWorldPoint(transport.getDestination()), + compactWorldPoint(Rs2Player.getWorldLocation())); + return false; + } - // Wrap with observation so Leagues blocked-region chat can attribute this attempt. - if (attemptObserved(transport, () -> npc != null && 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 (isTerminalTravelTransport(transport.getType())) { + if (terminalTravelMode == Rs2TerminalTravelMode.UNSUPPORTED) { + WebWalkLog.spWarn( + "selected terminal travel has no supported interaction mode | type={} origin={} dest={}", + transport.getType(), compactWorldPoint(transport.getOrigin()), + compactWorldPoint(transport.getDestination())); + break originLoop; + } - 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)); + Rs2NpcModel npc = Rs2Npc.getNpc(transport.getName()); + if (npc != null && Rs2Npc.canWalkTo(npc, 20)) { + String npcAction = resolveTerminalNpcInteractionAction( + npc, transport); + if (npcAction.isEmpty()) { + WebWalkLog.spWarn( + "terminal NPC has no supported interaction action name={} configured={} dest={}", + transport.getName(), transport.getAction(), transport.getDisplayInfo()); + break originLoop; } - - if (Rs2Dialogue.clickOption("I'm just going to Pirates' cove")){ - sleepTickJitter(2); - Rs2Dialogue.clickContinue(); - } else if (Objects.equals(transport.getName(), "Mountain Guide")) { - Rs2Dialogue.clickOption(transport.getDisplayInfo()); + if (!markTerminalTravelAttempt(transport)) { + log.debug("[Walker] terminal travel edge already attempted this walk: {}", + transport.getDisplayInfo()); + break originLoop; } - sleepUntil(() -> !Rs2Player.isAnimating()); - boolean shipNearDest = sleepUntil( - () -> isPlayerWithinChebyshevOf(transport.getDestination(), TRANSPORT_NEAR_LANDING_CHEBYSHEV), - SHIP_NPC_BOAT_LANDING_WAIT_MS); - if (!shipNearDest) { - WebWalkLog.spWarn( - "ship/npc/boat post-travel wait timed out ({}ms) dest={} at={}", - SHIP_NPC_BOAT_LANDING_WAIT_MS, - compactWorldPoint(transport.getDestination()), - compactWorldPoint(Rs2Player.getWorldLocation())); + if (!npcAction.equalsIgnoreCase(transport.getAction())) { + WebWalkLog.spInfo( + "terminal NPC action fallback name={} configured={} selected={} dest={}", + transport.getName(), transport.getAction(), npcAction, + transport.getDisplayInfo()); } - boolean reachedDestination = shipNearDest; - sleepTickJitter(6); - if (reachedDestination) { - return finishHandledTransport(transport); + + // Wrap with observation so Leagues blocked-region chat can attribute this attempt. + if (attemptObserved(transport, () -> Rs2Npc.interact(npc, npcAction))) { + 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")) { + sleepTickJitter(2); + Rs2Dialogue.clickContinue(); + } + if (!selectTerminalTravelDialogueDestination( + transport, terminalTravelMode)) { + break originLoop; + } + final int terminalDestinationIndex = precomputedIndexOfDest; + if (awaitTerminalTravelLanding( + transport, path, terminalDestinationIndex)) { + return finishHandledTransport(transport); + } } } else { - WorldPoint originTile = path.get(i); - boolean clicked = Rs2Walker.walkFastCanvas(originTile); - if (!clicked) { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc != null) { - clicked = walkMiniMapToward(originTile, playerLoc, 13); + TileObject terminalObject = findTerminalTravelObject(transport); + if (terminalObject != null) { + String objectAction = resolveTransportObjectAction( + terminalObject, + Collections.singletonList(transport.getAction())) + .orElse(""); + if (objectAction.isEmpty()) { + WebWalkLog.spWarn( + "terminal object has no supported interaction action name={} configured={} dest={}", + transport.getName(), transport.getAction(), transport.getDisplayInfo()); + break originLoop; } + if (!markTerminalTravelAttempt(transport)) { + log.debug("[Walker] terminal travel edge already attempted this walk: {}", + transport.getDisplayInfo()); + break originLoop; + } + prepareTransportObjectForInteraction(terminalObject); + final TileObject selectedTerminalObject = terminalObject; + if (attemptObserved(transport, () -> Rs2GameObject.interact( + selectedTerminalObject, objectAction))) { + if (!selectTerminalTravelDialogueDestination( + transport, terminalTravelMode)) { + break originLoop; + } + final int terminalDestinationIndex = precomputedIndexOfDest; + if (awaitTerminalTravelLanding( + transport, path, terminalDestinationIndex)) { + return finishHandledTransport(transport); + } + } + } else { + WorldPoint originTile = path.get(i); + boolean clicked = Rs2Walker.walkFastCanvas(originTile); + if (!clicked) { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc != null) { + clicked = walkMiniMapToward(originTile, playerLoc, 13); + } + } + if (!clicked) { + clicked = Rs2Walker.walkMiniMap(originTile); + } + if (!clicked) { + log.debug("[Walker] terminal travel fallback click failed for {}", originTile); + } + sleep(1200, 1600); } - if (!clicked) { - clicked = Rs2Walker.walkMiniMap(originTile); - } - if (!clicked) { - log.debug("[Walker] ship/npc/boat fallback click failed for {}", originTile); - } - sleep(1200, 1600); } + + // Terminal travel is terminal for this transport scan. The exact edge can be + // clicked at most once in one top-level walk invocation; callers can start + // a fresh walk after a surfaced failure, but this invocation never spams the + // target for later path indices or another local-instance copy of the origin. + break originLoop; } if (transport.getType() == TransportType.CHARTER_SHIP) { @@ -8909,8 +9077,29 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i } } + if (transport.getType() == TransportType.HOT_AIR_BALLOON) { + if (attemptObserved(transport, () -> Rs2HotAirBalloon.handle(selection.getEdge()))) { + boolean balloonLanded = Rs2WalkerRuntimeAwaits.awaitCondition( + () -> isPlayerWithinChebyshevOf(transport.getDestination(), OFFSET), + TRANSPORT_LANDING_WAIT_POLL_MS, + TRANSPORT_LANDING_WAIT_TIMEOUT_MS); + if (balloonLanded) { + sleepTickJitter(2); + return finishHandledTransport(transport); + } + WebWalkLog.spWarn( + "hot-air balloon post-travel wait timed out ({}ms) dest={} at={}", + TRANSPORT_LANDING_WAIT_TIMEOUT_MS, + compactWorldPoint(transport.getDestination()), + compactWorldPoint(Rs2Player.getWorldLocation())); + } + // This is a specialized map interaction. Do not fall through to the generic + // object handler and click the same basket again during this walker tick. + return false; + } + if (transport.getType() == TransportType.SPIRIT_TREE) { - if (!Rs2PathApi.getPathfinderConfig().isUseSpiritTrees()) { + if (!Rs2PathApi.isSpiritTreeTravelEnabled()) { log.debug("[Walker] skip spirit tree transport — setting is off"); continue; } @@ -9049,13 +9238,24 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i // were queued, so the next run distinguishes "one slow scan" from "many scans". long objectScanStartedAt = System.currentTimeMillis(); final Integer legacyClosedId = OPEN_TO_CLOSED_MAPPINGS.get(transportObjectId); - // Id-only first: these are plain field reads, no composition resolution. - List matched = Rs2GameObject.getAll(o -> { - int id = o.getId(); - if (id == transportObjectId) return true; - if (allowAlKharidTollGateVariant && isAlKharidTollGateObjectId(id)) return true; - return legacyClosedId != null && id == legacyClosedId; - }, transport.getOrigin(), 10); + // Most catalog transports can use their stable object id. The Al Kharid gate cannot: + // its historical catalog ids collide with unrelated live objects in newer injected-client + // revisions. Select that edge by its transformed live composition and route geometry instead. + // This deliberately has no id fallback: clicking an unrelated object is worse than failing + // closed and replanning. + List matched; + if (allowAlKharidTollGateVariant) { + matched = Rs2GameObject.getAll( + o -> isAlKharidTollGateSceneCandidate(transport, o), + transport.getOrigin(), 3); + } else { + // Id-only first: these are plain field reads, no composition resolution. + matched = Rs2GameObject.getAll(o -> { + int id = o.getId(); + if (id == transportObjectId) return true; + return legacyClosedId != null && id == legacyClosedId; + }, transport.getOrigin(), 10); + } if (matched.isEmpty() && allowClosedVariant) { // Only now pay for compositions, and only on the transport's own tile: a closed // variant (trapdoor/manhole/grate/hatch) sits where the transport is, never ten @@ -9083,7 +9283,7 @@ && recentlyOpenedStationaryDoorOnSegment(path.get(indexOfStartPoint), path.get(i long objectScanMs = System.currentTimeMillis() - objectScanStartedAt; if (objectScanMs >= TRANSPORT_OBJECT_SCAN_SLOW_MS) { WebWalkLog.spInfo("transport_object_scan | slow scanMs={} objectId={} candidatesAtTile={} matches={} origin={}", - objectScanMs, transportObjectId, orderedTransports.size(), objects.size(), + objectScanMs, transportObjectId, 1, objects.size(), compactWorldPoint(transport.getOrigin())); } TileObject object = objects.stream().findFirst().orElse(null); @@ -9296,11 +9496,13 @@ private static List getTransportActionOptions(String action) { } private static Optional resolveTransportObjectAction(TileObject object, List actionOptions) { - ObjectComposition comp = Rs2GameObject.convertToObjectComposition(object); - if (comp == null || comp.getActions() == null) { - return Optional.empty(); - } - return resolveTransportObjectAction(comp.getActions(), actionOptions); + return Microbot.getClientThread().runOnClientThreadOptional(() -> { + ObjectComposition comp = Rs2DoorDetection.resolveCompositionForDoorProbe(object); + if (comp == null || comp.getActions() == null) { + return Optional.empty(); + } + return resolveTransportObjectAction(comp.getActions(), actionOptions); + }).orElse(Optional.empty()); } private static Optional resolveTransportObjectAction(String[] objectActions, List actionOptions) { @@ -9365,6 +9567,13 @@ private static boolean handleObject(Transport transport, TileObject tileObject, ensureRequiredItemBeforeTransport(transport); WorldPoint before = Rs2Player.getWorldLocation(); Rs2GameObject.interact(tileObject, action); + // Unlike the other exception handlers, a toll-gate interaction is not complete merely + // because the menu action was issued: it may first server-walk from several tiles away and + // then present a confirmation dialogue. Bubble an unobserved crossing back to the caller so + // it cannot emit a transport handoff for a player who is still west/east of the gate. + if (isAlKharidTollGateTransport(transport) && isPayTollAction(transport.getAction())) { + return handleAlKharidTollGate(transport); + } if (handleObjectExceptions(transport, tileObject)) return true; WorldPoint tdObj = transport.getDestination(); WorldPoint plObj = Rs2Player.getWorldLocation(); @@ -9459,13 +9668,21 @@ private static boolean handleObject(Transport transport, TileObject tileObject, } } - private static boolean isAdjacentSamePlaneTransport(Transport transport) { - return transport != null + private static boolean isAdjacentSamePlaneTransport(Transport transport) { + return transport != null && transport.getOrigin() != null && transport.getDestination() != null && transport.getOrigin().getPlane() == transport.getDestination().getPlane() - && transport.getOrigin().distanceTo(transport.getDestination()) <= 1; - } + && transport.getOrigin().distanceTo(transport.getDestination()) <= 1; + } + + private static boolean isAdjacentSamePlaneTransport(Rs2TransportEdge transport) { + return transport != null + && transport.getOrigin() != null + && transport.getDestination() != null + && transport.getOrigin().getPlane() == transport.getDestination().getPlane() + && transport.getOrigin().distanceTo(transport.getDestination()) <= 1; + } private static int[] mapSmoothedToRaw(List smoothed, List raw) { if (smoothed == null || raw == null || smoothed.isEmpty() || raw.isEmpty()) { @@ -9669,14 +9886,9 @@ private static boolean isObjectInteractionTransportStep(List rawPath if (rawPath == null || index < 0 || index >= rawPath.size() - 1) { return false; } - Set transports = Rs2PathApi.getTransports().get(rawPath.get(index)); - if (transports == null || transports.isEmpty()) { - return false; - } - WorldPoint next = rawPath.get(index + 1); - return transports.stream() - .filter(t -> t != null && Objects.equals(t.getDestination(), next)) - .anyMatch(t -> t.getType() == TransportType.TRANSPORT); + return Rs2PathApi.getActiveTransportEdge(rawPath.get(index), rawPath.get(index + 1)) + .map(edge -> edge.getType() == Rs2TransportType.TRANSPORT) + .orElse(false); } /** Config kill switch for ranged transport dispatch; on when the config is unavailable. */ @@ -9873,11 +10085,11 @@ private static boolean hasPrecomputedContinuationFromTransport(Transport transpo if (transport == null || transport.getDestination() == null) { return false; } - Pathfinder pathfinder = Rs2PathApi.getPathfinder(); - if (pathfinder == null || !pathfinder.isDone()) { + Rs2ActiveRouteStatus routeStatus = Rs2PathApi.getActiveRouteStatus(); + if (!routeStatus.isReady()) { return false; } - List walkPath = pathfinder.getWalkablePath(); + List walkPath = routeStatus.getWalkablePath(); if (walkPath == null || walkPath.size() < 2) { return false; } @@ -9930,11 +10142,196 @@ static Set adjacentSamePlaneTransportSuppressionPoints(Transport tra return points; } - private static int transportHandlingPreference(Transport transport) { - if (isAlKharidTollGateTransport(transport) && transport.getCurrencyAmount() > 0) { - return 1; + static boolean isTerminalTravelTransport(TransportType transportType) { + return transportType == TransportType.SHIP + || transportType == TransportType.NPC + || transportType == TransportType.BOAT; + } + + private static boolean selectTerminalTravelDialogueDestination( + Transport transport, Rs2TerminalTravelMode mode) { + if (mode == Rs2TerminalTravelMode.DIRECT) { + return true; + } + if (mode != Rs2TerminalTravelMode.DIALOGUE_DESTINATION + || transport == null + || transport.getDisplayInfo() == null + || transport.getDisplayInfo().isBlank()) { + return false; + } + if (!sleepUntil(Rs2Dialogue::hasSelectAnOption, 5000)) { + WebWalkLog.spWarn( + "terminal travel destination dialogue did not appear name={} dest={}", + transport.getName(), transport.getDisplayInfo()); + return false; + } + if (!Rs2Dialogue.clickOption(transport.getDisplayInfo())) { + WebWalkLog.spWarn( + "terminal travel destination option missing name={} dest={}", + transport.getName(), transport.getDisplayInfo()); + return false; + } + return true; + } + + private static TileObject findTerminalTravelObject(Transport transport) { + if (transport == null || transport.getOrigin() == null) { + return null; + } + TileObject object = Rs2GameObject.getAll( + candidate -> isTerminalTravelObjectSceneCandidate(transport, candidate), + transport.getOrigin(), 3).stream().findFirst().orElse(null); + if (object != null) { + WebWalkLog.spInfo( + "terminal travel object selected type={} name={} action={} origin={} dest={}", + transport.getType(), transport.getName(), transport.getAction(), + compactWorldPoint(transport.getOrigin()), + compactWorldPoint(transport.getDestination())); + } + return object; + } + + private static boolean isTerminalTravelObjectSceneCandidate(Transport transport, + TileObject object) { + if (object == null) { + return false; + } + return Microbot.getClientThread().runOnClientThreadOptional(() -> { + ObjectComposition composition = Rs2DoorDetection.resolveCompositionForDoorProbe(object); + return composition != null + && isTerminalTravelObjectCompositionCandidate( + transport, + object.getWorldLocation(), + composition.getName(), + composition.getActions()); + }).orElse(false); + } + + static boolean isTerminalTravelObjectCompositionCandidate(Transport transport, + WorldPoint objectLocation, + String objectName, + String[] objectActions) { + if (transport == null + || !isTerminalTravelTransport(transport.getType()) + || transport.getOrigin() == null + || objectLocation == null + || objectName == null + || transport.getName() == null + || transport.getAction() == null + || objectLocation.getPlane() != transport.getOrigin().getPlane() + || objectLocation.distanceTo2D(transport.getOrigin()) > 3 + || !Rs2UiHelper.stripColTags(objectName).trim().equalsIgnoreCase( + Rs2UiHelper.stripColTags(transport.getName()).trim())) { + return false; + } + return resolveTransportObjectAction( + objectActions, + Collections.singletonList(transport.getAction())).isPresent(); + } + + private static boolean awaitTerminalTravelLanding(Transport transport, + List path, + int destinationIndex) { + boolean landed = sleepUntil( + () -> hasReachedTerminalTravelLanding( + transport, path, destinationIndex, Rs2Player.getWorldLocation()), + SHIP_NPC_BOAT_LANDING_WAIT_MS); + if (!landed) { + WebWalkLog.spWarn( + "ship/npc/boat post-travel wait timed out ({}ms) dest={} at={}", + SHIP_NPC_BOAT_LANDING_WAIT_MS, + compactWorldPoint(transport.getDestination()), + compactWorldPoint(Rs2Player.getWorldLocation())); + } + return landed; + } + + /** + * Returns interaction actions in executor preference order. Some legacy ship rows encode their + * destination label as the direct NPC menu action. The current Port Sarim NPCs instead expose + * {@code Travel}; keep the configured label first for compatible clients, then use that observed + * live fallback. Explicit dialogue and quick-travel actions must never be replaced implicitly. + */ + static List terminalNpcInteractionCandidates(TransportType transportType, + String configuredAction) { + LinkedHashSet candidates = new LinkedHashSet<>(); + if (configuredAction != null && !configuredAction.isBlank()) { + candidates.add(configuredAction); + } + if (transportType == TransportType.SHIP + && !isExplicitShipMenuAction(configuredAction)) { + candidates.add("Travel"); + } + return List.copyOf(candidates); + } + + private static boolean isExplicitShipMenuAction(String action) { + return action != null + && (action.equalsIgnoreCase("Travel") + || action.equalsIgnoreCase("Talk-to") + || action.equalsIgnoreCase("Quick-Travel") + || action.equalsIgnoreCase("Take-boat")); + } + + private static String resolveTerminalNpcInteractionAction(Rs2NpcModel npc, Transport transport) { + if (npc == null || transport == null) { + return ""; + } + for (String candidate : terminalNpcInteractionCandidates( + transport.getType(), transport.getAction())) { + // Query one candidate at a time: Rs2Npc#getAvailableAction otherwise returns NPC-menu + // order, which commonly places Talk-to before the exact configured action. + String available = Rs2Npc.getAvailableAction(npc, Collections.singletonList(candidate)); + if (!available.isEmpty()) { + return available; + } + } + return ""; + } + + static boolean markTerminalTravelAttempt(Transport transport) { + if (transport == null || transport.getOrigin() == null || transport.getDestination() == null) { + return false; + } + String key = transport.getType() + + "|" + rangedTransportEdgeKey(transport.getOrigin(), transport.getDestination()) + + "|" + transport.getObjectId() + + "|" + Objects.toString(transport.getName(), "") + + "|" + Objects.toString(transport.getAction(), ""); + return TERMINAL_TRAVEL_ATTEMPTED_EDGES.add(key); + } + + /** + * Accepts the exact catalogued landing or the immediately following path point. The latter covers + * modern ship travel that skips an obsolete deck tile and completes the next gangplank step in one + * server action. It deliberately does not scan arbitrary later route points, which could report a + * false landing when a route loops near its origin. + */ + static boolean hasReachedTerminalTravelLanding(Transport transport, + List path, + int destinationIndex, + WorldPoint playerLocation) { + if (transport == null || playerLocation == null || transport.getDestination() == null) { + return false; + } + WorldPoint origin = transport.getOrigin(); + if (origin != null + && origin.getPlane() == playerLocation.getPlane() + && origin.distanceTo2D(playerLocation) <= 1) { + return false; + } + if (isNearSamePlane(playerLocation, transport.getDestination(), + TRANSPORT_NEAR_LANDING_CHEBYSHEV)) { + return true; } - return 0; + if (path == null || destinationIndex < 0 || destinationIndex + 1 >= path.size()) { + return false; + } + WorldPoint immediateContinuation = path.get(destinationIndex + 1); + return immediateContinuation != null + && !immediateContinuation.equals(transport.getDestination()) + && isNearSamePlane(playerLocation, immediateContinuation, + TRANSPORT_NEAR_LANDING_CHEBYSHEV); } private static boolean isAlKharidTollGateTransport(Transport transport) { @@ -9952,31 +10349,75 @@ private static boolean isPayTollAction(String action) { return action != null && action.toLowerCase(Locale.ROOT).startsWith("pay-toll"); } + private static boolean isAlKharidTollGateSceneCandidate(Transport transport, TileObject object) { + if (!(object instanceof WallObject) && !(object instanceof GameObject)) { + return false; + } + return Microbot.getClientThread().runOnClientThreadOptional(() -> { + ObjectComposition comp = Rs2DoorDetection.resolveCompositionForDoorProbe(object); + return comp != null + && isAlKharidTollGateCompositionCandidate( + transport, object.getWorldLocation(), comp.getName(), comp.getActions()) + && Rs2DoorGeometry.isDoorOnSegment( + object, transport.getOrigin(), transport.getDestination()); + }).orElse(false); + } + + static boolean isAlKharidTollGateCompositionCandidate(Transport transport, + WorldPoint objectLocation, + String objectName, + String[] objectActions) { + if (!isAlKharidTollGateTransport(transport) + || objectLocation == null + || !AL_KHARID_TOLL_GATE_POINTS.contains(objectLocation) + || objectName == null + || !objectName.toLowerCase(Locale.ROOT).contains("gate")) { + return false; + } + return resolveTransportObjectAction( + objectActions, getTransportActionOptions(transport.getAction())).isPresent(); + } + + static boolean hasReachedAlKharidTollDestination(Transport transport, WorldPoint playerLocation) { + return isAlKharidTollGateTransport(transport) + && playerLocation != null + && playerLocation.equals(transport.getDestination()); + } + private static boolean handleAlKharidTollGate(Transport transport) { - if (Rs2Player.isMoving()) { + // Object interaction can begin out of range. Wait for server-walking, the confirmation + // dialogue, or the crossing itself instead of sampling isMoving() immediately after click. + sleepUntil(() -> Rs2Player.isMoving() + || Rs2Dialogue.hasSelectAnOption() + || hasReachedAlKharidTollDestination(transport, Rs2Player.getWorldLocation()), + AL_KHARID_TOLL_INTERACTION_START_WAIT_MS); + + if (Rs2Player.isMoving() + && !hasReachedAlKharidTollDestination(transport, Rs2Player.getWorldLocation())) { Rs2Player.waitForWalking(); } boolean confirmed = false; - if (sleepUntil(Rs2Dialogue::hasSelectAnOption, 2500)) { + if (!hasReachedAlKharidTollDestination(transport, Rs2Player.getWorldLocation()) + && (Rs2Dialogue.hasSelectAnOption() + || sleepUntil(Rs2Dialogue::hasSelectAnOption, + AL_KHARID_TOLL_INTERACTION_START_WAIT_MS))) { confirmed = Rs2Dialogue.clickOption("Yes, okay", "Yes"); } - boolean reachedDestination = sleepUntil(() -> { - WorldPoint now = Rs2Player.getWorldLocation(); - WorldPoint destination = transport.getDestination(); - return now != null - && destination != null - && now.getPlane() == destination.getPlane() - && now.distanceTo2D(destination) <= 1; - }, POST_HANDLE_OBJECT_LANDING_WAIT_MS); - if (!confirmed && !reachedDestination) { + boolean reachedDestination = hasReachedAlKharidTollDestination( + transport, Rs2Player.getWorldLocation()) + || sleepUntil(() -> hasReachedAlKharidTollDestination( + transport, Rs2Player.getWorldLocation()), + POST_HANDLE_OBJECT_LANDING_WAIT_MS); + if (!reachedDestination) { WebWalkLog.spWarn( - "Al Kharid toll gate confirmation unresolved dest={} at={}", + "Al Kharid toll gate crossing unresolved confirmed={} dest={} at={}", + confirmed, compactWorldPoint(transport.getDestination()), compactWorldPoint(Rs2Player.getWorldLocation())); } - return true; + return reachedDestination; } private static boolean handleObjectExceptions(Transport transport, TileObject tileObject) { @@ -10010,10 +10451,6 @@ private static boolean handleObjectExceptions(Transport transport, TileObject ti } } - if (isAlKharidTollGateTransport(transport) && isPayTollAction(transport.getAction())) { - return handleAlKharidTollGate(transport); - } - //Al kharid broken wall will animate once and then stop and then animate again if (tileObject.getId() == ObjectID.KHARID_POSHWALL_TOPLESS || tileObject.getId() == ObjectID.KHARID_BIGWINDOW) { Rs2Player.waitForAnimation(); @@ -10179,7 +10616,9 @@ private static boolean handleWildernessObelisk(Transport transport) { } private static boolean handleTeleportSpell(Transport transport) { - if (Rs2Pvp.isInWilderness() && (Rs2Pvp.getWildernessLevelFrom(Rs2Player.getWorldLocation()) > (transport.getMaxWildernessLevel() + 1))) return false; + if (Rs2Pvp.isInWilderness() && !isTeleportAllowedAtWildernessLevel( + Rs2Pvp.getWildernessLevelFrom(Rs2Player.getWorldLocation()), transport.getMaxWildernessLevel())) return false; + if (!prepareTeleportSpellProviders(transport)) return false; boolean hasMultipleDestination = transport.getDisplayInfo().contains(":"); String spellName = hasMultipleDestination @@ -10194,16 +10633,84 @@ private static boolean handleTeleportSpell(Transport transport) { ? 2 : 1; + Optional homeTeleport = + TransportExecutionRegistry.homeTeleportFor(transport.getDisplayInfo()); + if (homeTeleport.isPresent()) { + return Rs2Magic.quickCast(homeTeleport.get().getDisplayName()); + } + MagicAction magicSpell = Arrays.stream(MagicAction.values()).filter(x -> x.getName().toLowerCase().contains(spellName)).findFirst().orElse(null); if (magicSpell != null) { - if (magicSpell == MagicAction.LUMBRIDGE_HOME_TELEPORT) { - return Rs2Magic.quickCast(magicSpell); - } return Rs2Magic.cast(magicSpell, option, identifier); } return false; } + /** + * Equip any inventory staff/tome selected by a source-aware upstream spell requirement before + * casting. An item merely present in the inventory never acts as an infinite rune provider. + */ + private static boolean prepareTeleportSpellProviders(Transport transport) { + List requirements = transport.getItemRequirements(); + if (requirements == null || requirements.isEmpty()) { + return true; + } + + Map runeQuantities = new HashMap<>(); + Rs2Magic.getRunes().forEach((rune, quantity) -> + runeQuantities.put(rune.getItemId(), quantity)); + java.util.function.IntUnaryOperator currentQuantity = itemId -> { + Runes rune = Runes.byItemId(itemId); + if (rune != null) { + return runeQuantities.getOrDefault(itemId, 0); + } + int quantity = Rs2Inventory.itemQuantity(itemId); + Rs2ItemModel equipped = Rs2Equipment.get(itemId); + return equipped == null ? quantity : quantity + Math.max(1, equipped.getQuantity()); + }; + + TransportItemRequirement.ProviderSelection providers = + TransportItemRequirement.selectProviders( + requirements, + currentQuantity, + itemId -> Rs2Equipment.isWearing(itemId) || Rs2Inventory.hasItem(itemId), + itemId -> Rs2Equipment.isWearing(itemId) || Rs2Inventory.hasItem(itemId)) + .orElse(null); + if (providers == null) { + return false; + } + if (!equipTransportProvider(providers.getStaffItemId()) + || !equipTransportProvider(providers.getOffhandItemId())) { + return false; + } + + Map verifiedRuneQuantities = new HashMap<>(); + Rs2Magic.getRunes().forEach((rune, quantity) -> + verifiedRuneQuantities.put(rune.getItemId(), quantity)); + return TransportItemRequirement.selectProviders( + requirements, + itemId -> { + Runes rune = Runes.byItemId(itemId); + if (rune != null) { + return verifiedRuneQuantities.getOrDefault(itemId, 0); + } + int quantity = Rs2Inventory.itemQuantity(itemId); + Rs2ItemModel equipped = Rs2Equipment.get(itemId); + return equipped == null ? quantity : quantity + Math.max(1, equipped.getQuantity()); + }, + Rs2Equipment::isWearing, + Rs2Equipment::isWearing).isPresent(); + } + + private static boolean equipTransportProvider(int itemId) { + if (itemId <= 0 || Rs2Equipment.isWearing(itemId)) { + return true; + } + return Rs2Inventory.hasItem(itemId) + && Rs2Inventory.wield(itemId) + && sleepUntil(() -> Rs2Equipment.isWearing(itemId), 3000); + } + private static boolean isLumbridgeHomeTeleport(Transport transport) { return transport.getDisplayInfo() != null && transport.getDisplayInfo().toLowerCase().startsWith("lumbridge home teleport"); @@ -10212,7 +10719,8 @@ private static boolean isLumbridgeHomeTeleport(Transport transport) { private static boolean handleTeleportItem(Transport transport) { WorldPoint plWild = Rs2Player.getWorldLocation(); if (Rs2Pvp.isInWilderness() && plWild != null - && Rs2Pvp.getWildernessLevelFrom(plWild) > (transport.getMaxWildernessLevel() + 1)) { + && !isTeleportAllowedAtWildernessLevel( + Rs2Pvp.getWildernessLevelFrom(plWild), transport.getMaxWildernessLevel())) { return false; } boolean succesfullAction = false; @@ -10246,11 +10754,9 @@ private static boolean handleInventoryTeleports(Transport transport, int itemId) // Return true when the item does not use a generic keyword to teleport to its destination boolean hasParsableDestination = transport.getDisplayInfo().contains(":"); - String destination = hasParsableDestination - ? transport.getDisplayInfo().split(":")[1].trim().toLowerCase() - : transport.getDisplayInfo().trim().toLowerCase(); + String destination = teleportItemLeafAction(transport.getDisplayInfo()); - boolean wildernessTransport = PathfinderConfig.isInWilderness(WorldPointUtil.packWorldPoint(transport.getDestination())); + boolean wildernessTransport = Rs2PathApi.isInWilderness(transport.getDestination()); log.debug("Trying to find action for destination={}", destination); // Check if item has destination as direct action @@ -10322,8 +10828,7 @@ private static boolean handleWearableTeleports(Transport transport, int itemId) Rs2ItemModel rs2Item = Rs2Equipment.get(itemId); if (rs2Item == null) return false; if (transport.getDisplayInfo().contains(":")) { - String[] values = transport.getDisplayInfo().split(":"); - String destination = values[1].trim().toLowerCase(); + String destination = teleportItemLeafAction(transport.getDisplayInfo()); if (transport.getDisplayInfo().toLowerCase().contains("slayer ring")) { Rs2Equipment.invokeMenu(rs2Item, "teleport"); @@ -10342,6 +10847,23 @@ private static boolean handleWearableTeleports(Transport transport, int itemId) return false; } + /** + * Returns the executable leaf from a display hierarchy. Upstream labels may describe nested + * categories (for example {@code Max cape: POH Portals: Rimmington}); RuneLite item sub-ops are + * looked up by their leaf action, not by the intermediate display category. + */ + static String teleportItemLeafAction(String displayInfo) { + if (displayInfo == null) { + return ""; + } + String[] segments = displayInfo.split(":"); + return segments[segments.length - 1].trim().toLowerCase(Locale.ROOT); + } + + static boolean isTeleportAllowedAtWildernessLevel(int currentLevel, int maximumLevel) { + return currentLevel <= maximumLevel; + } + /** * Checks if the teleport item requires dialogue-based destination selection. * These are items that, when rubbed/activated, show a dialogue menu to choose destination. @@ -10406,10 +10928,9 @@ public static boolean isInArea(WorldPoint centerOfArea, int range) { } public static boolean isNear() { - final Pathfinder pathfinder = Rs2PathApi.getPathfinder(); - if (pathfinder == null) return false; // idk are we near if we don't have a path? - final List path = pathfinder.getPath(); - if (path == null) return false; + final Rs2ActiveRouteStatus routeStatus = Rs2PathApi.getActiveRouteStatus(); + if (!routeStatus.isPresent()) return false; // idk are we near if we don't have a path? + final List path = routeStatus.getRawPath(); WorldPoint playerLocation = Rs2Player.getWorldLocation(); if (playerLocation == null) { @@ -10435,11 +10956,11 @@ public static boolean isNear(WorldPoint target) { } public static boolean isNearPath() { - final Pathfinder pathfinder = Rs2PathApi.getPathfinder(); - if (pathfinder == null) return true; + final Rs2ActiveRouteStatus routeStatus = Rs2PathApi.getActiveRouteStatus(); + if (!routeStatus.isPresent()) return true; - final List path = pathfinder.getWalkablePath(); - if (path == null || path.isEmpty()) return true; + final List path = routeStatus.getWalkablePath(); + if (path.isEmpty()) return true; final WorldPoint loc = Rs2Player.getWorldLocation(); if (loc == null) return true; @@ -10669,12 +11190,12 @@ private static void checkIfStuck() { private static final long MINIMAP_CLICK_STALL_GRACE_MS = 12_000L; private static boolean interactingActorNearWalkablePath() { - Pathfinder pf = Rs2PathApi.getPathfinder(); - if (pf == null) { + Rs2ActiveRouteStatus routeStatus = Rs2PathApi.getActiveRouteStatus(); + if (!routeStatus.isPresent()) { return false; } - List path = pf.getWalkablePath(); - if (path == null || path.isEmpty()) { + List path = routeStatus.getWalkablePath(); + if (path.isEmpty()) { return false; } Actor actor = Rs2Player.getInteracting(); @@ -10728,11 +11249,10 @@ private static boolean isStuckTooLong() { * @param start */ public void setStart(WorldPoint start) { - Pathfinder pathfinder = Rs2PathApi.getPathfinder(); - if (pathfinder == null) { + Set targets = Rs2PathApi.getActiveRouteTargets(); + if (targets.isEmpty()) { return; } - Set targets = pathfinder.getTargets(); Rs2PathApi.setStartPointSet(true); if (isClientThread()) { Microbot.getClientThread().runOnSeperateThread(() -> restartPathfinding(start, targets)); @@ -10768,15 +11288,10 @@ public static WorldPoint nearestReachable(WorldPoint start, Collection path = pathfinder.getPath(); - if (path == null || path.isEmpty()) { - return null; - } // A partial path ends somewhere that is NOT a target; only trust an endpoint we asked for. - WorldPoint endpoint = path.get(path.size() - 1); - return targets.contains(endpoint) ? endpoint : null; + return Rs2PathApi.plan(Rs2RouteRequest.toAny(start, targets)) + .getReachedTarget(0) + .orElse(null); } /** @@ -10787,10 +11302,7 @@ public static WorldPoint nearestReachable(WorldPoint start, Collection ends = Set.of(endpoint); - Pathfinder pathfinder = new Pathfinder(Rs2PathApi.getPathfinderConfig(), startpoint, ends); - pathfinder.run(); - return pathfinder.getPath().size(); + return Rs2PathApi.plan(Rs2RouteRequest.to(startpoint, endpoint)).getPath().size(); } /** @@ -11071,6 +11583,26 @@ private static boolean handleMinigameTeleport(Transport transport) { return sleepUntilTrue(() -> !Rs2Player.isAnimating() && teleportGraphics.stream().noneMatch(Rs2Player::hasSpotAnimation), 100, 20000); } + static int canoeMapMainComponentId(int stationObjectId) { + if (stationObjectId >= 60845 && stationObjectId <= 60849) { + return InterfaceID.CanoeMapDougne.MAIN_MAP; + } + if ((stationObjectId >= 12163 && stationObjectId <= 12166) || stationObjectId == 39638) { + return InterfaceID.CanoeMapLum.MAIN_MAP; + } + return -1; + } + + static int canoeMapDestinationsComponentId(int stationObjectId) { + if (stationObjectId >= 60845 && stationObjectId <= 60849) { + return InterfaceID.CanoeMapDougne.DESTINATIONS; + } + if ((stationObjectId >= 12163 && stationObjectId <= 12166) || stationObjectId == 39638) { + return InterfaceID.CanoeMapLum.DESTINATIONS; + } + return -1; + } + private static boolean handleCanoe(Transport transport) { String displayInfo = transport.getDisplayInfo(); if (displayInfo == null || displayInfo.isEmpty()) return false; @@ -11144,6 +11676,12 @@ private static boolean handleCanoe(Transport transport) { return Arrays.stream(composition.getActions()).filter(Objects::nonNull).noneMatch(currentAction::equals) && !Rs2Player.isAnimating(); }, 300, 10000); case "Paddle Canoe": + int canoeMapMain = canoeMapMainComponentId(transport.getObjectId()); + int canoeMapDestinations = canoeMapDestinationsComponentId(transport.getObjectId()); + if (canoeMapMain < 0 || canoeMapDestinations < 0) { + log.error("Unsupported canoe station object id: {}", transport.getObjectId()); + return false; + } if (!Rs2GameObject.interact(transport.getObjectId(), "Paddle Canoe")) { log.error("Failed to interact with canoe station"); return false; @@ -11155,18 +11693,17 @@ private static boolean handleCanoe(Transport transport) { sleepUntil(Rs2Player::isMoving, 2000); sleepUntilTrue(() -> !Rs2Player.isMoving(), 100, 30000); - // OSRS update moved the canoe destination map from group 647 to - // CanoeMapLum (953) for the river Lum chain. CanoeMapDougne (952) - // is for a different chain not currently used by canoes.tsv. + // OSRS uses separate interfaces for the River Lum and River Dougne chains. boolean isDestinationMapVisible = sleepUntilTrue( - () -> Rs2Widget.isWidgetVisible(InterfaceID.CanoeMapLum.MAIN_MAP), + () -> Rs2Widget.isWidgetVisible(canoeMapMain), 100, 10000); if (!isDestinationMapVisible) { - log.error("Canoe destination map (CanoeMapLum) not visible within timeout period"); + log.error("Canoe destination map not visible within timeout period for station {}", + transport.getObjectId()); return false; } - Widget destinationListWidget = Rs2Widget.getWidget(InterfaceID.CanoeMapLum.DESTINATIONS); + Widget destinationListWidget = Rs2Widget.getWidget(canoeMapDestinations); if (destinationListWidget == null) return false; Widget destination = Rs2Widget.findWidget("Travel to " + displayInfo, List.of(destinationListWidget), false); if (destination == null) { @@ -11208,16 +11745,16 @@ private static String pickQuetzalWhistleInventoryMenuAction(Rs2ItemModel rs2Item /** * Labels match {@code quetzals.tsv} destination rows (map icon text). */ - private static String quetzalMapLabelForDestination(WorldPoint dest) { + static String quetzalMapLabelForDestination(WorldPoint dest) { assert dest != null; final int[][] coords = { - {1389, 2901, 0}, {1697, 3140, 0}, {1585, 3053, 0}, {1510, 3221, 0}, {1548, 2995, 0}, + {1389, 2901, 0}, {1697, 3140, 0}, {1585, 3053, 0}, {1510, 3222, 0}, {1548, 2995, 0}, {1437, 3171, 0}, {1779, 3111, 0}, {1700, 3037, 0}, {1670, 2933, 0}, {1446, 3108, 0}, {1613, 3300, 0}, {1226, 3091, 0}, {1344, 3022, 0}, {1411, 3361, 0}, }; final String[] labels = { "Aldarin", "Civitas illa Fortis", "Hunter Guild", "Quetzacalli Gorge", "Sunset Coast", - "The Teomat", "Fortis Colosseum", "Outer Fortis", "Colossal Wyrm Remains", "Cam Torum Entrance", + "The Teomat", "Fortis Colosseum", "Outer Fortis", "Colossal Wyrm Remains", "Cam Torum", "Salvager Overlook", "Tal Teklan", "Kastori", "Auburnvale", }; assert coords.length == labels.length; @@ -11857,24 +12394,10 @@ private static int getDesiredRotation(char letter) { * @return true if the item is a teleportation item, false otherwise */ public static boolean isTeleportItem(int itemId) { - if (Rs2PathApi.getPathfinderConfig().getAllTransports().isEmpty()) { - Rs2PathApi.getPathfinderConfig().refresh(); - } - - Set teleportItemIds = Rs2PathApi.getPathfinderConfig().getAllTransports().values() - .stream() - .flatMap(Set::stream) - .filter(t -> TransportType.isTeleport(t.getType(), t.getOrigin())) - .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); - - return teleportItemIds.contains(itemId); + return Rs2PathApi.isTeleportItem( + itemId, + ItemID.DRAMEN_STAFF, + ItemID.LUNAR_MOONCLAN_LIMINAL_STAFF); } @@ -11901,47 +12424,28 @@ public static int findNearestAccessibleTarget(WorldPoint startPoint, List targetSet = new HashSet<>(targets); - - // Store original configuration to restore later - boolean originalUseBankItems = Rs2PathApi.getPathfinderConfig().isUseBankItems(); - try { - Rs2PathApi.getPathfinderConfig().setUseBankItems(useBankItems); - // Configure pathfinder - Rs2PathApi.getPathfinderConfig().refresh(); - // Run pathfinder - Pathfinder pf = new Pathfinder(Rs2PathApi.getPathfinderConfig(), startPoint, targetSet); - pf.run(); - - List path = pf.getPath(); - if (path.isEmpty()) { - log.debug("Unable to find path to any target from starting point: " + startPoint); - return -1; - } - - // Find which target corresponds to the end of the path - WorldPoint nearestTile = path.get(path.size() - 1); - WorldArea nearestTileArea = new WorldArea(nearestTile, tolerance, tolerance); - - // Find the target that matches the final path destination - for (int i = 0; i < targets.size(); i++) { - WorldPoint target = targets.get(i); - WorldArea targetArea = new WorldArea(target, tolerance, tolerance); - if (targetArea.intersectsWith2D(nearestTileArea)) { - log.debug("Found nearest accessible target at index " + i + ": " + target + " (path ended at: " + nearestTile + ")"); - return i; - } - } - - log.debug("Path found but no target matched the destination: " + nearestTile); + Rs2RouteResult route = Rs2PathApi.plan( + Rs2RouteRequest.toAny(startPoint, targetSet).withBankItems(useBankItems)); + WorldPoint nearestTile = route.getEndpoint().orElse(null); + if (nearestTile == null) { + log.debug("Unable to find path to any target from starting point: " + startPoint); return -1; + } - } finally { - // Always restore original configuration - Rs2PathApi.getPathfinderConfig().setUseBankItems(originalUseBankItems); - Rs2PathApi.getPathfinderConfig().refresh(); + WorldArea nearestTileArea = new WorldArea(nearestTile, tolerance, tolerance); + for (int i = 0; i < targets.size(); i++) { + WorldPoint target = targets.get(i); + WorldArea targetArea = new WorldArea(target, tolerance, tolerance); + if (targetArea.intersectsWith2D(nearestTileArea)) { + log.debug("Found nearest accessible target at index " + i + ": " + target + " (path ended at: " + nearestTile + ")"); + return i; + } } + + log.debug("Path found but no target matched the destination: " + nearestTile); + return -1; } /** @@ -11991,6 +12495,16 @@ public static List getTransportsForDestination(WorldPoint destination return getTransportsForDestination(destination, useBankItems, TransportType.TELEPORTATION_ITEM); } + /** + * Planner-independent counterpart to {@link #getTransportsForDestination(WorldPoint, boolean)}. + * New banking and execution code must use this exact selected-edge view. + */ + public static List getTransportEdgesForDestination( + WorldPoint destination, boolean useBankItems) + { + return Rs2WalkerBankingPlanner.getTransportEdgesForDestination(destination, useBankItems); + } + /** * Prepares and analyzes required transport items for reaching a destination. * Similar but improved to Rs2Slayer.prepareItemTransports() @@ -12037,6 +12551,12 @@ public static List getMissingTransports(List transports) { return Rs2WalkerBankingPlanner.getMissingTransports(transports); } + public static List getMissingTransportEdges( + List transports) + { + return Rs2WalkerBankingPlanner.getMissingTransportEdges(transports); + } + /** * Extracts item IDs and their required quantities for the given transports that are missing and available in bank. * Enhanced version that uses Rs2Magic and Rs2Spells systems for actual rune quantities on teleportation spells. @@ -12048,6 +12568,18 @@ public static Map getMissingTransportItemIdsWithQuantities(Lis return Rs2WalkerBankingPlanner.getMissingTransportItemIdsWithQuantities(transports); } + public static Map getMissingTransportEdgeItemIdsWithQuantities( + List transports) + { + return Rs2WalkerBankingPlanner.getMissingTransportEdgeItemIdsWithQuantities(transports); + } + + public static Rs2TransportLoadout getMissingTransportEdgeLoadout( + List transports) + { + return Rs2WalkerBankingPlanner.getMissingTransportEdgeLoadout(transports); + } + /** * Extracts item IDs that are missing for the given transports and available in bank. * Legacy method maintained for backward compatibility. @@ -12152,7 +12684,9 @@ public static WalkerState walkWithBankedTransportsAndState(WorldPoint target, in } } try { - return walkWithBankedTransportsAndStateLocked(target, distance, forceBanking); + return withShadowExecutionEvidence( + () -> walkWithBankedTransportsAndStateLocked( + target, distance, forceBanking)); } finally { walkerLock.unlock(); } @@ -12173,8 +12707,8 @@ private static WalkerState walkWithBankedTransportsAndStateLocked(WorldPoint tar if (Rs2Tile.getReachableTilesFromTile(pl, distance).containsKey(target) || nearUnwalkableGoal) { return WalkerState.ARRIVED; } - final Pathfinder pathfinder = Rs2PathApi.getPathfinder(); - if (pathfinder != null && !pathfinder.isDone()) + final Rs2ActiveRouteStatus routeStatus = Rs2PathApi.getActiveRouteStatus(); + if (routeStatus.isCalculating()) return WalkerState.MOVING; boolean bankTripWhenCacheUnavailable = config == null || config.bankTripWhenCacheUnavailable(); @@ -12213,15 +12747,22 @@ private static WalkerState walkWithBankedTransportsAndStateLocked(WorldPoint tar TransportRouteAnalysis comparison = compareRoutes(target); WebWalkLog.tmark("compare_done", System.currentTimeMillis() - compareStartedAt, target, pl, "direct=" + comparison.getDirectDistance() + " bank=" + comparison.getBankingRouteDistance()); - List missingTransports = getMissingTransports(getTransportsForDestination(target, true, TransportType.TELEPORTATION_SPELL)); + List missingTransports = getMissingTransportEdges( + Rs2WalkerBankingPlanner.getRequiredTransportEdgesFromBank(comparison)); - Map missingItemsWithQuantities = getMissingTransportItemIdsWithQuantities(missingTransports); + Rs2TransportLoadout transportLoadout = getMissingTransportEdgeLoadout(missingTransports); + Map missingItemsWithQuantities = transportLoadout.getWithdrawals(); if (!missingTransports.isEmpty()) { - WebWalkLog.bankWalkDebug("missing_items nTrans={} to={} missingKinds={}", - missingTransports.size(), target, missingItemsWithQuantities.size()); + WebWalkLog.bankWalkDebug("missing_items nTrans={} to={} missingKinds={} equipKinds={} satisfiable={}", + missingTransports.size(), target, missingItemsWithQuantities.size(), + transportLoadout.getEquipmentItemIds().size(), transportLoadout.isSatisfiable()); + } + if (!transportLoadout.isSatisfiable()) { + WebWalkLog.spWarn("bank_walk | selected bank route has no executable loadout goal={}", target); + return forceBanking ? WalkerState.EXIT : walkWithStateInternal(target, distance); } // If no missing transport items, go directly - if (missingItemsWithQuantities.isEmpty() && !forceBanking) { + if (transportLoadout.isEmpty() && !forceBanking) { WebWalkLog.spInfo("bank_walk | direct_no_missing_items goal={}", target); WalkerState state = walkWithStateInternal(target, distance); if (state == WalkerState.ARRIVED) { @@ -12248,7 +12789,8 @@ private static WalkerState walkWithBankedTransportsAndStateLocked(WorldPoint tar log.info("\n\tUsing banking route: \n\t\tStart: {} -> Bank: {} -> Target: {}", Rs2Player.getWorldLocation(), comparison.getBankLocation(), target); // Handle the complete banking workflow using legacy walkTo approach - return walkWithBankingState(comparison.getBankLocation(), missingItemsWithQuantities, target, distance); + return walkWithBankingState( + comparison.getBankLocation(), transportLoadout, target, distance); } else { log.warn("\n\tBanking route requested but no accessible bank found, trying direct route"); return walkWithStateInternal(target, distance); @@ -12329,31 +12871,20 @@ private static WalkerState bootstrapBankMirrorForBankedPathing(int distance) { /** - * Handles the complete banking workflow using legacy walkTo: walk to bank, open, withdraw items, close, continue to target. - * Enhanced version that accepts a map of item IDs with their required quantities and returns boolean. - * - * @param bankLocation The bank location to visit - * @param missingItemsWithQuantities Map of item IDs and their required quantities - * @param finalTarget The final destination after banking - * @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; - } - - /** - * Handles the complete banking workflow using walkWithState: walk to bank, open, withdraw items, close, continue to target. - * Enhanced version that accepts a map of item IDs with their required quantities and returns WalkerState. + * Handles the complete banking workflow using the immutable preparation selected for the exact + * bank-to-target route: walk to bank, withdraw, equip, close, refresh inventory-only policy and + * continue to the target. * - * @param missingItemsWithQuantities Map of item IDs and their required quantities + * @param transportLoadout Withdrawals and equipment changes required by the selected route * @param finalTarget The final destination after banking * @return WalkerState indicating the result of the banking workflow */ private static WalkerState walkWithBankingState(WorldPoint bankLocation, - Map missingItemsWithQuantities, + Rs2TransportLoadout transportLoadout, WorldPoint finalTarget,int distance) { try { - if (bankLocation == null || finalTarget == null) { + if (bankLocation == null || finalTarget == null || transportLoadout == null + || !transportLoadout.isSatisfiable()) { log.warn("Cannot perform banking workflow with null locations"); return WalkerState.EXIT; } @@ -12376,6 +12907,7 @@ private static WalkerState walkWithBankingState(WorldPoint bankLocation, } // Step 3: Withdraw missing transport items + Map missingItemsWithQuantities = transportLoadout.getWithdrawals(); if (!missingItemsWithQuantities.isEmpty()) { log.debug("Withdrawing transport items with quantities: " + missingItemsWithQuantities); @@ -12389,11 +12921,17 @@ private static WalkerState walkWithBankingState(WorldPoint bankLocation, if (amountToWithdraw > 0) { if (Rs2Bank.hasBankItem(itemId, amountToWithdraw)) { log.debug("Withdrawing {} x {} (item ID: {})", amountToWithdraw, itemId, itemId); - Rs2Bank.withdrawX(itemId, amountToWithdraw); - sleepUntil(() -> Rs2Inventory.count(itemId) >= currentCount + amountToWithdraw, 3000); + if (!Rs2Bank.withdrawX(itemId, amountToWithdraw) + || !sleepUntil(() -> Rs2Inventory.count(itemId) + >= currentCount + amountToWithdraw, 3000)) { + log.warn("Failed to withdraw required transport item {} x{}", + itemId, amountToWithdraw); + return WalkerState.EXIT; + } } else { log.warn("Required transport item {} not found in bank (need {} but bank has less)", itemId, amountToWithdraw); + return WalkerState.EXIT; } } else { log.debug("Already have enough of item {}: {} (need {})", itemId, currentCount, amountNeeded); @@ -12404,6 +12942,18 @@ private static WalkerState walkWithBankingState(WorldPoint bankLocation, sleepTickJitter(1); } + for (Integer equipmentItemId : transportLoadout.getEquipmentItemIds()) { + if (Rs2Equipment.isWearing(equipmentItemId)) { + continue; + } + if (!Rs2Inventory.hasItem(equipmentItemId) + || !Rs2Bank.wearItem(equipmentItemId) + || !sleepUntil(() -> Rs2Equipment.isWearing(equipmentItemId), 3000)) { + log.warn("Failed to equip required transport provider {}", equipmentItemId); + return WalkerState.EXIT; + } + } + // Step 4: Close bank Rs2Bank.closeBank(); sleepUntil(() -> !Rs2Bank.isOpen(), 3000); @@ -12411,8 +12961,10 @@ private static WalkerState walkWithBankingState(WorldPoint bankLocation, log.warn("Failed to close bank after withdrawals"); return WalkerState.EXIT; } - Rs2PathApi.getPathfinderConfig().setUseBankItems(false); - Rs2PathApi.getPathfinderConfig().refresh(finalTarget); + if (!Rs2PathApi.prepareInventoryOnlyRoute(finalTarget)) { + log.warn("Shortest-path configuration unavailable after bank withdrawals"); + return WalkerState.EXIT; + } // Step 5: Continue to final target log.debug("Banking complete, continuing to final target: " + finalTarget); return walkWithStateInternal(finalTarget, distance); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerShadowExecutionStats.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerShadowExecutionStats.java new file mode 100644 index 00000000000..b880a0bc751 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2WalkerShadowExecutionStats.java @@ -0,0 +1,50 @@ +package net.runelite.client.plugins.microbot.util.walker; + +/** Coordinate-free terminal outcomes for blocking walks observed while shadow mode was enabled. */ +public final class Rs2WalkerShadowExecutionStats +{ + private final long arrived; + private final long unreachable; + private final long exited; + private final long recoveryArrived; + private final long recoveryUnreachable; + private final long recoveryExited; + + Rs2WalkerShadowExecutionStats( + long arrived, + long unreachable, + long exited, + long recoveryArrived, + long recoveryUnreachable, + long recoveryExited) + { + this.arrived = requireNonNegative(arrived, "arrived"); + this.unreachable = requireNonNegative(unreachable, "unreachable"); + this.exited = requireNonNegative(exited, "exited"); + this.recoveryArrived = requireNonNegative(recoveryArrived, "recoveryArrived"); + this.recoveryUnreachable = requireNonNegative( + recoveryUnreachable, "recoveryUnreachable"); + this.recoveryExited = requireNonNegative(recoveryExited, "recoveryExited"); + } + + private static long requireNonNegative(long value, String name) + { + if (value < 0) + { + throw new IllegalArgumentException(name + " must be non-negative"); + } + return value; + } + + public long getArrived() { return arrived; } + public long getUnreachable() { return unreachable; } + public long getExited() { return exited; } + public long getRecoveryArrived() { return recoveryArrived; } + public long getRecoveryUnreachable() { return recoveryUnreachable; } + public long getRecoveryExited() { return recoveryExited; } + public long getTerminal() { return arrived + unreachable + exited; } + public long getRecoveryTerminal() + { + return recoveryArrived + recoveryUnreachable + recoveryExited; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/TransportRouteAnalysis.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/TransportRouteAnalysis.java index e7d53f334df..1db22e71bb2 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/TransportRouteAnalysis.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/TransportRouteAnalysis.java @@ -20,6 +20,12 @@ public class TransportRouteAnalysis { /** Complete path of WorldPoints representing the direct route to destination */ private final List directPath; + + /** Exact immutable edge sequence selected for the direct route, when captured by the planner */ + private final List directRouteSteps; + + /** Whether {@link #directRouteSteps} is an exact planner result rather than a legacy omission */ + private final boolean directRouteStepsExact; /** Reference to the nearest accessible BankLocation object, null if no bank is accessible */ private final BankLocation nearestBank; @@ -29,10 +35,22 @@ public class TransportRouteAnalysis { /** Path of WorldPoints from starting point to the nearest bank */ private final List pathToBank; + + /** Exact immutable edge sequence selected from the start to the bank */ + private final List routeToBankSteps; + + /** Whether {@link #routeToBankSteps} is an exact planner result */ + private final boolean routeToBankStepsExact; /** Path of WorldPoints from bank to destination, accounting for items available in bank */ private final List pathFromBank; + /** Exact immutable edge sequence selected from the bank to the destination */ + private final List routeFromBankSteps; + + /** Whether {@link #routeFromBankSteps} is an exact planner result */ + private final boolean routeFromBankStepsExact; + /** Explicit direct distance captured at analysis time (tiles), or -1 if unavailable */ private final int directDistance; @@ -57,21 +75,103 @@ public TransportRouteAnalysis(List directPath, List pathFromBank,String analysis) { this(directPath, nearestBank, bankLocation, pathToBank, pathFromBank, analysis, deriveRouteDistance(directPath), - deriveBankingRouteDistance(pathToBank, pathFromBank)); + deriveBankingRouteDistance(pathToBank, pathFromBank), + null, null, null); } public TransportRouteAnalysis(List directPath, BankLocation nearestBank, WorldPoint bankLocation, List pathToBank, List pathFromBank, String analysis, int directDistance, int bankingRouteDistance) { - this.directPath = directPath; + this(directPath, nearestBank, bankLocation, pathToBank, pathFromBank, analysis, + directDistance, bankingRouteDistance, null, null, null); + } + + /** + * Constructs an analysis carrying the exact immutable route steps selected by each search. + * + *

The appended step parameters preserve the two historical constructor descriptors for Hub + * compatibility. New Microbot code must use this form so banking and execution never infer a + * transport later by rescanning mutable catalog endpoints.

+ */ + public TransportRouteAnalysis(List directPath, + BankLocation nearestBank, WorldPoint bankLocation, List pathToBank, + List pathFromBank, String analysis, + int directDistance, int bankingRouteDistance, + List directRouteSteps, + List routeToBankSteps, + List routeFromBankSteps) { + this.directPath = immutablePath(directPath); this.nearestBank = nearestBank; this.bankLocation = bankLocation; - this.pathToBank = pathToBank; - this.pathFromBank = pathFromBank; + this.pathToBank = immutablePath(pathToBank); + this.pathFromBank = immutablePath(pathFromBank); this.analysis = analysis; this.directDistance = directDistance; this.bankingRouteDistance = bankingRouteDistance; + this.directRouteStepsExact = directRouteSteps != null; + this.routeToBankStepsExact = routeToBankSteps != null; + this.routeFromBankStepsExact = routeFromBankSteps != null; + this.directRouteSteps = immutableSteps("direct", this.directPath, directRouteSteps); + this.routeToBankSteps = immutableSteps("to-bank", this.pathToBank, routeToBankSteps); + this.routeFromBankSteps = immutableSteps("from-bank", this.pathFromBank, routeFromBankSteps); + } + + private static List immutablePath(List path) { + return path == null ? List.of() : List.copyOf(path); + } + + private static List immutableSteps( + String label, List path, List steps) { + if (steps == null) { + return List.of(); + } + List immutable = List.copyOf(steps); + int expected = Math.max(0, path.size() - 1); + if (immutable.size() != expected) { + throw new IllegalArgumentException(label + " steps must describe every path edge: expected " + + expected + ", got " + immutable.size()); + } + for (int index = 0; index < immutable.size(); index++) { + Rs2RouteStep step = immutable.get(index); + if (!path.get(index).equals(step.getFrom()) || !path.get(index + 1).equals(step.getTo())) { + throw new IllegalArgumentException(label + " step " + index + " is not contiguous with path"); + } + } + return immutable; + } + + /** Exact selected transport edges for the direct route, in route order. */ + public List getDirectTransportEdges() { + return transportEdges(directRouteSteps); + } + + /** Exact selected transport edges for the start-to-bank leg, in route order. */ + public List getTransportEdgesToBank() { + return transportEdges(routeToBankSteps); + } + + /** Exact selected transport edges for the bank-to-target leg, in route order. */ + public List getTransportEdgesFromBank() { + return transportEdges(routeFromBankSteps); + } + + /** Exact selected transport edges for both banking legs, in route order. */ + public List getBankingTransportEdges() { + List combined = new ArrayList<>(); + combined.addAll(getTransportEdgesToBank()); + combined.addAll(getTransportEdgesFromBank()); + return List.copyOf(combined); + } + + private static List transportEdges(List steps) { + List transports = new ArrayList<>(); + for (Rs2RouteStep step : steps) { + if (step.isTransport()) { + transports.add(step.getTransport().orElseThrow(IllegalStateException::new)); + } + } + return List.copyOf(transports); } /** @@ -161,6 +261,7 @@ public String toString() { * Gets all required transports for the direct path with default parameters. * @return List of required transports for direct path */ + @Deprecated public List getTransportsForDirectPath(){ return getTransportsForDirectPath(0, TransportType.TELEPORTATION_ITEM, true); } @@ -172,6 +273,7 @@ public List getTransportsForDirectPath(){ * @param applyFiltering Whether to apply filtering * @return List of required transports for direct path */ + @Deprecated public List getTransportsForDirectPath(int startIndex, TransportType prefTransportType, boolean applyFiltering){ List transports = Rs2Walker.getTransportsForPath(directPath, startIndex, prefTransportType, applyFiltering); return transports; @@ -181,6 +283,7 @@ public List getTransportsForDirectPath(int startIndex, TransportType * Gets all required transports for the banking route with default parameters. * @return List of required transports for banking route (to and from bank) */ + @Deprecated public List getTransportsForBankingPath(){ return getTransportsForBankingPath(0, TransportType.TELEPORTATION_ITEM, true); } @@ -192,6 +295,7 @@ public List getTransportsForBankingPath(){ * @param applyFiltering Whether to apply filtering * @return List of required transports for banking route (to and from bank) */ + @Deprecated public List getTransportsForBankingPath(int startIndex, TransportType prefTransportType, boolean applyFiltering){ List transportsToTargetToBank = Rs2Walker.getTransportsForPath(pathToBank, startIndex, prefTransportType, applyFiltering); List transportsToTargetFromBank = Rs2Walker.getTransportsForPath(pathFromBank, startIndex, prefTransportType, applyFiltering); @@ -205,6 +309,7 @@ public List getTransportsForBankingPath(int startIndex, TransportType * Gets missing transports for the direct path. * @return List of missing transports for direct path */ + @Deprecated public List getMissingTransportsForDirectPath(){ List missingTransports = Rs2Walker.getMissingTransports(getTransportsForDirectPath()); return missingTransports; @@ -214,6 +319,7 @@ public List getMissingTransportsForDirectPath(){ * Gets missing transport items with their quantities for the direct path. * @return Map of item IDs to their required quantities */ + @Deprecated public Map getMissingTransportsItemsWithQuantitiesForDirectPath(){ List missingTransports = getMissingTransportsForDirectPath(); Map missingItemsWithQuantities = Rs2Walker.getMissingTransportItemIdsWithQuantities(missingTransports); @@ -224,6 +330,7 @@ public Map getMissingTransportsItemsWithQuantitiesForDirectPat * Gets missing transports for the banking route (to and from bank). * @return List of missing transports for the banking route */ + @Deprecated public List getMissingTransportsForBankingRoute(){ List missingTransports = Rs2Walker.getMissingTransports(getTransportsForBankingPath(0, TransportType.TELEPORTATION_ITEM, true)); return missingTransports; @@ -233,6 +340,7 @@ public List getMissingTransportsForBankingRoute(){ * Gets missing transport items with their quantities for the banking route. * @return Map of item IDs to their required quantities */ + @Deprecated public Map getMissingTransportsItemsWithQuantitiesForBankingRoute(){ List missingTransports = getMissingTransportsForBankingRoute(); Map missingItemsWithQuantities = Rs2Walker.getMissingTransportItemIdsWithQuantities(missingTransports); @@ -243,6 +351,7 @@ public Map getMissingTransportsItemsWithQuantitiesForBankingRo * Gets all required transports for the path to bank with default parameters. * @return List of required transports for path to bank */ + @Deprecated public List getTransportsForPathToBank() { return getTransportsForPathToBank(0, TransportType.TELEPORTATION_ITEM, true); } @@ -254,6 +363,7 @@ public List getTransportsForPathToBank() { * @param applyFiltering Whether to apply filtering * @return List of required transports for path to bank */ + @Deprecated public List getTransportsForPathToBank(int startIndex, TransportType prefTransportType, boolean applyFiltering) { return Rs2Walker.getTransportsForPath(pathToBank, startIndex, prefTransportType, applyFiltering); } @@ -262,6 +372,7 @@ public List getTransportsForPathToBank(int startIndex, TransportType * Gets all required transports for the path from bank with default parameters. * @return List of required transports for path from bank */ + @Deprecated public List getTransportsForPathFromBank() { return getTransportsForPathFromBank(0, TransportType.TELEPORTATION_ITEM, true); } @@ -273,6 +384,7 @@ public List getTransportsForPathFromBank() { * @param applyFiltering Whether to apply filtering * @return List of required transports for path from bank */ + @Deprecated public List getTransportsForPathFromBank(int startIndex, TransportType prefTransportType, boolean applyFiltering) { return Rs2Walker.getTransportsForPath(pathFromBank, startIndex, prefTransportType, applyFiltering); } @@ -281,6 +393,7 @@ public List getTransportsForPathFromBank(int startIndex, TransportTyp * Gets missing transports specifically for the path from bank to destination. * @return List of missing transports for path from bank */ + @Deprecated public List getMissingTransportsForPathFromBank() { List missingTransports = Rs2Walker.getMissingTransports(getTransportsForPathFromBank()); return missingTransports; @@ -290,10 +403,10 @@ public List getMissingTransportsForPathFromBank() { * Gets missing transport items with their quantities specifically for the path from bank to destination. * @return Map of item IDs to their required quantities for path from bank */ + @Deprecated public Map getMissingTransportsItemsWithQuantitiesForPathFromBank() { List missingTransports = getMissingTransportsForPathFromBank(); Map missingItemsWithQuantities = Rs2Walker.getMissingTransportItemIdsWithQuantities(missingTransports); return missingItemsWithQuantities; } } - diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/UpstreamRoutePlanner.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/UpstreamRoutePlanner.java new file mode 100644 index 00000000000..f9bdb0d07c6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/UpstreamRoutePlanner.java @@ -0,0 +1,372 @@ +package net.runelite.client.plugins.microbot.util.walker; + +import net.runelite.api.coords.WorldPoint; +import shortestpath.DestinationRequirements; +import shortestpath.ShortestPathConfig; +import shortestpath.WorldPointUtil; +import shortestpath.pathfinder.CollisionMap; +import shortestpath.pathfinder.PathStep; +import shortestpath.pathfinder.Pathfinder; +import shortestpath.pathfinder.PathfinderConfig; +import shortestpath.pathfinder.PathfinderResult; +import shortestpath.pathfinder.SplitFlagMap; +import shortestpath.pathfinder.TransportAvailability; +import shortestpath.pathfinder.WildernessChecker; +import shortestpath.transport.Transport; +import shortestpath.transport.TransportType; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** Production-packaged adapter for the pinned reviewed upstream planner core. */ +final class UpstreamRoutePlanner implements Rs2RoutePlanner +{ + static final String REVISION = "ff8e961b32120175709df9630ece9468cc11347f"; + private static final ShortestPathConfig EMPTY_CONFIG = new ShortestPathConfig() + { + @Override + public void setBuiltTeleportationBoxes(String content) + { + } + + @Override + public void setBuiltTeleportationPortalsPoh(String content) + { + } + }; + + private static final class StaticMapHolder + { + private static final SplitFlagMap INSTANCE = SplitFlagMap.fromResources(); + } + + @Override + public String getEngineId() + { + return "shortest-path-upstream@" + REVISION; + } + + @Override + public Rs2RouteResult plan(Rs2RouteRequest request, Rs2PlanningSnapshot snapshot) + { + Objects.requireNonNull(request, "request"); + Objects.requireNonNull(snapshot, "snapshot"); + Rs2RoutePolicy policy = request.getPolicy().orElseThrow( + () -> new IllegalArgumentException("upstream planner requires a resolved policy")); + if (snapshot.getPolicy() != policy) + { + throw new IllegalArgumentException("route request and planning snapshot policy differ"); + } + + IdentityHashMap exactEdges = new IdentityHashMap<>(); + TransportAvailability.Builder catalog = new TransportAvailability.Builder( + Math.max(1, snapshot.getAdmittedTransports().size())); + for (Rs2TransportEdge edge : snapshot.getAdmittedTransports()) + { + if ((policy.isIgnoreTeleportAndItems() || policy.isTeleportsDisabled()) + && edge.getOrigin() == null && edge.isTeleport()) + { + continue; + } + Transport converted = convert(edge); + catalog.add(converted); + exactEdges.put(converted, edge); + } + + Set packedTargets = new LinkedHashSet<>(); + for (WorldPoint target : request.getTargets()) + { + packedTargets.add(WorldPointUtil.packWorldPoint(target)); + } + UpstreamConfig config = new UpstreamConfig( + request, snapshot, catalog.build()); + Pathfinder pathfinder = new Pathfinder( + config, WorldPointUtil.packWorldPoint(request.getStart()), packedTargets); + pathfinder.run(); + PathfinderResult result = pathfinder.getResult(); + if (result == null) + { + throw new IllegalStateException("pinned upstream planner did not publish a result"); + } + return toResult(request, snapshot, result, exactEdges); + } + + private static Transport convert(Rs2TransportEdge edge) + { + int origin = edge.getOrigin() == null + ? WorldPointUtil.UNDEFINED : WorldPointUtil.packWorldPoint(edge.getOrigin()); + return new Transport.TransportBuilder() + .origin(origin) + .destination(WorldPointUtil.packWorldPoint(edge.getDestination())) + .type(mapType(edge)) + .duration(edge.getDuration()) + .displayInfo(edge.getDisplayInfo()) + .isConsumable(edge.isConsumable()) + .maxWildernessLevel(edge.getMaxWildernessLevel()) + .build(); + } + + /** + * Project the planner-independent type into the pinned upstream schema. + * + *

Keep this switch exhaustive. A newly introduced Microbot transport category must be reviewed + * before it can enter the upstream catalog; silently flattening it to {@code TRANSPORT} can alter + * teleport admission, wilderness limits, delayed visits, or transport cost.

+ */ + static TransportType mapType(Rs2TransportEdge edge) + { + if (edge.getOrigin() == null) + { + switch (edge.getType()) + { + case QUETZAL_WHISTLE: + return TransportType.QUETZAL_WHISTLE; + case SEASONAL_TRANSPORT: + // Upstream's seasonal category is anchored; originless seasonal rows are + // Microbot teleports and therefore use upstream teleport admission/costing. + return TransportType.TELEPORTATION_ITEM; + case TELEPORTATION_ITEM: + return TransportType.TELEPORTATION_ITEM; + case TELEPORTATION_MINIGAME: + return TransportType.TELEPORTATION_MINIGAME; + case TELEPORTATION_SPELL: + return TransportType.TELEPORTATION_SPELL; + case TELEPORTATION_SPELL_HOME: + return TransportType.TELEPORTATION_SPELL_HOME; + default: + throw unsupportedType(edge, "originless transport category is not an upstream teleport"); + } + } + + switch (edge.getType()) + { + case TRANSPORT: + return TransportType.TRANSPORT; + case AGILITY_SHORTCUT: + return TransportType.AGILITY_SHORTCUT; + case GRAPPLE_SHORTCUT: + return TransportType.GRAPPLE_SHORTCUT; + case BOAT: + return TransportType.BOAT; + case CANOE: + return TransportType.CANOE; + case CHARTER_SHIP: + return TransportType.CHARTER_SHIP; + case SHIP: + return TransportType.SHIP; + case FAIRY_RING: + return TransportType.FAIRY_RING; + case QUETZAL: + return TransportType.QUETZAL; + case QUETZAL_WHISTLE: + return TransportType.QUETZAL_WHISTLE; + case GNOME_GLIDER: + return TransportType.GNOME_GLIDER; + case MINECART: + return TransportType.MINECART; + case POH: + case NPC: + // These Microbot-owned execution families have no upstream type. They remain + // distinct on the exact Rs2TransportEdge retained by the adapter. + return TransportType.TRANSPORT; + case SPIRIT_TREE: + return TransportType.SPIRIT_TREE; + case TELEPORTATION_BOX: + return TransportType.TELEPORTATION_BOX; + case TELEPORTATION_LEVER: + return TransportType.TELEPORTATION_LEVER; + case TELEPORTATION_PORTAL: + return TransportType.TELEPORTATION_PORTAL; + case TELEPORTATION_PORTAL_POH: + return TransportType.TELEPORTATION_PORTAL_POH; + case TELEPORTATION_MINIGAME: + return TransportType.TELEPORTATION_MINIGAME; + case TELEPORTATION_ITEM: + return TransportType.TELEPORTATION_ITEM; + case TELEPORTATION_SPELL: + return TransportType.TELEPORTATION_SPELL; + case TELEPORTATION_SPELL_HOME: + return TransportType.TELEPORTATION_SPELL_HOME; + case WILDERNESS_OBELISK: + return TransportType.WILDERNESS_OBELISK; + case MAGIC_CARPET: + return TransportType.MAGIC_CARPET; + case HOT_AIR_BALLOON: + return TransportType.HOT_AIR_BALLOON; + case MAGIC_MUSHTREE: + return TransportType.MAGIC_MUSHTREE; + case SEASONAL_TRANSPORT: + return TransportType.SEASONAL_TRANSPORTS; + case UNKNOWN: + default: + throw unsupportedType(edge, "transport category has no reviewed upstream projection"); + } + } + + private static IllegalArgumentException unsupportedType(Rs2TransportEdge edge, String reason) + { + return new IllegalArgumentException(reason + ": " + edge.getType()); + } + + private static Rs2RouteResult toResult( + Rs2RouteRequest request, + Rs2PlanningSnapshot snapshot, + PathfinderResult result, + IdentityHashMap exactEdges) + { + List sourcePath = result.getPathSteps() == null + ? Collections.emptyList() : result.getPathSteps(); + List path = new ArrayList<>(sourcePath.size()); + for (PathStep step : sourcePath) + { + path.add(WorldPointUtil.unpackWorldPoint(step.getPackedPosition())); + } + List steps = new ArrayList<>(Math.max(0, path.size() - 1)); + long cost = 0L; + for (int i = 1; i < sourcePath.size(); i++) + { + WorldPoint from = path.get(i - 1); + WorldPoint to = path.get(i); + Transport selected = sourcePath.get(i).getTransport(); + if (selected == null) + { + steps.add(Rs2RouteStep.walk(from, to)); + cost += WorldPointUtil.distanceBetween( + sourcePath.get(i - 1).getPackedPosition(), sourcePath.get(i).getPackedPosition()); + cost += snapshot.getAdditionalWalkingCost( + sourcePath.get(i).getPackedPosition(), request.getTargets()); + } + else + { + Rs2TransportEdge exact = exactEdges.get(selected); + if (exact == null) + { + throw new IllegalStateException( + "upstream selected a transport outside the projected catalog"); + } + steps.add(Rs2RouteStep.transport(from, to, exact)); + cost += selected.getDuration(); + if (selected.getOrigin() == WorldPointUtil.UNDEFINED) + { + cost += snapshot.getPolicy().getDistanceBeforeUsingTeleport(); + } + } + } + return new Rs2RouteResult( + request.getStart(), + request.getTargets(), + path, + steps, + Rs2RouteTermination.valueOf(result.getTerminationReason().name()), + new Rs2RouteMetrics( + result.getElapsedNanos(), + cost, + result.getNodesChecked(), + result.getTransportsChecked())); + } + + private static final class UpstreamConfig extends PathfinderConfig + { + private final Rs2RouteRequest request; + private final Rs2PlanningSnapshot snapshot; + private final TransportAvailability availability; + private final CollisionMap collisionMap; + private final Set restricted; + + private UpstreamConfig( + Rs2RouteRequest request, + Rs2PlanningSnapshot snapshot, + TransportAvailability availability) + { + super(null, EMPTY_CONFIG, StaticMapHolder.INSTANCE, + Collections.emptyMap(), + Collections.>emptyMap(), + Collections.>emptyMap(), + Collections.emptyMap()); + this.request = request; + this.snapshot = snapshot; + this.availability = availability; + this.collisionMap = new CollisionMap( + StaticMapHolder.INSTANCE, snapshot::collisionOverride); + Set packedRestricted = new LinkedHashSet<>(); + for (WorldPoint point : snapshot.getPolicy().getRestrictedPoints()) + { + packedRestricted.add(WorldPointUtil.packWorldPoint(point)); + } + this.restricted = Collections.unmodifiableSet(packedRestricted); + } + + @Override + public CollisionMap getMap() + { + return collisionMap; + } + + @Override + public TransportAvailability getTransportAvailability(boolean bankVisited) + { + return availability; + } + + @Override + public boolean isBankPathEnabled() + { + return false; + } + + @Override + public boolean bankAccessible(int packedPosition) + { + return false; + } + + @Override + public long getCalculationCutoffMillis() + { + return snapshot.getPolicy().getCalculationCutoffMillis(); + } + + @Override + public boolean avoidWilderness( + int packedPosition, int packedNeighborPosition, boolean targetInWilderness) + { + return snapshot.getPolicy().isAvoidWilderness() + && !targetInWilderness + && !WildernessChecker.isInWilderness(packedPosition) + && WildernessChecker.isInWilderness(packedNeighborPosition); + } + + @Override + public boolean avoidBlockedRegion( + int packedPosition, int packedNeighborPosition, boolean targetInBlockedRegion) + { + return restricted.contains(packedNeighborPosition) + || snapshot.isWalkingEdgeBlocked(packedPosition, packedNeighborPosition); + } + + @Override + public int getAdditionalWalkingCost(int packedDestination) + { + return snapshot.getAdditionalWalkingCost( + packedDestination, request.getTargets()); + } + + @Override + public int getAdditionalTransportCost(Transport transport) + { + return transport.getOrigin() == WorldPointUtil.UNDEFINED + ? snapshot.getPolicy().getDistanceBeforeUsingTeleport() : 0; + } + + @Override + public int getDifferentialCost(Transport transport) + { + return 0; + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/awaits/Rs2WalkerRuntimeAwaits.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/awaits/Rs2WalkerRuntimeAwaits.java index 635253f34b4..0094d6bd0e7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/awaits/Rs2WalkerRuntimeAwaits.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/awaits/Rs2WalkerRuntimeAwaits.java @@ -1,6 +1,5 @@ package net.runelite.client.plugins.microbot.util.walker.awaits; -import net.runelite.client.plugins.microbot.shortestpath.pathfinder.Pathfinder; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import java.util.function.BooleanSupplier; @@ -11,13 +10,6 @@ public final class Rs2WalkerRuntimeAwaits { private Rs2WalkerRuntimeAwaits() { } - public static boolean awaitPathfinderDone(Pathfinder pathfinder, int timeoutMs) { - if (pathfinder == null) { - return false; - } - return sleepUntilTrue(pathfinder::isDone, 100, timeoutMs); - } - public static boolean awaitCondition(BooleanSupplier condition, int pollMs, int timeoutMs) { if (condition == null) { return false; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/banking/Rs2WalkerBankingPlanner.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/banking/Rs2WalkerBankingPlanner.java index d392df64339..4f626b79c46 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/banking/Rs2WalkerBankingPlanner.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/banking/Rs2WalkerBankingPlanner.java @@ -5,17 +5,26 @@ import net.runelite.api.gameval.ItemID; import net.runelite.api.gameval.VarbitID; import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.shortestpath.pathfinder.Pathfinder; import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; import net.runelite.client.plugins.microbot.util.magic.Rs2Spells; +import net.runelite.client.plugins.microbot.util.magic.RuneFilter; import net.runelite.client.plugins.microbot.util.walker.Rs2PathApi; +import net.runelite.client.plugins.microbot.util.walker.Rs2RouteRequest; +import net.runelite.client.plugins.microbot.util.walker.Rs2RouteResult; +import net.runelite.client.plugins.microbot.util.walker.Rs2RouteStep; +import net.runelite.client.plugins.microbot.util.walker.Rs2TransportEdge; +import net.runelite.client.plugins.microbot.util.walker.Rs2TransportItemRequirement; +import net.runelite.client.plugins.microbot.util.walker.Rs2TransportLoadout; +import net.runelite.client.plugins.microbot.util.walker.Rs2TransportType; import net.runelite.client.plugins.microbot.shortestpath.PurchasableItemCatalog; import net.runelite.client.plugins.microbot.shortestpath.Transport; +import net.runelite.client.plugins.microbot.shortestpath.TransportItemRequirement; import net.runelite.client.plugins.microbot.shortestpath.TransportType; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; 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.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.leaguetransport.Rs2LeaguesTransport; import net.runelite.client.plugins.microbot.util.magic.Runes; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import net.runelite.client.plugins.microbot.util.walker.TransportRouteAnalysis; @@ -23,11 +32,15 @@ import net.runelite.client.plugins.microbot.util.player.Rs2Player; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.function.IntPredicate; +import java.util.function.IntUnaryOperator; import java.util.stream.Collectors; @Slf4j @@ -36,31 +49,94 @@ public final class Rs2WalkerBankingPlanner { private Rs2WalkerBankingPlanner() { } + /** + * Plan the destination and retain only the immutable transport edges selected by that search. + * + *

This is the planner-independent banking contract. In particular, it does not rescan the + * mutable transport catalog by origin/destination after pathfinding, so two transports sharing an + * edge cannot be confused.

+ */ + public static List getTransportEdgesForDestination( + WorldPoint destination, boolean useBankItems) + { + if (destination == null) + { + return List.of(); + } + WorldPoint start = Rs2Player.getWorldLocation(); + if (start == null) + { + log.debug("Unable to plan transport edges without a player location"); + return List.of(); + } + + Rs2RouteResult route = Rs2PathApi.plan( + Rs2RouteRequest.to(start, destination) + .withBankItems(useBankItems)); + if (route.getPath().isEmpty()) + { + log.debug("Unable to find path to destination: {}", destination); + return List.of(); + } + + List selected = route.getTransportSteps().stream() + .map(Rs2RouteStep::getTransport) + .map(transport -> transport.orElseThrow( + () -> new IllegalStateException("typed route step has no transport metadata"))) + .collect(Collectors.toList()); + List transports = applyTransportEdgeFiltering(selected); + transports.forEach(transport -> log.debug("Transport edge found: {} -> {} ({})", + transport.getOrigin(), transport.getDestination(), transport.getType())); + return transports; + } + + /** + * Return the bank-to-target transport requirements from the exact route already compared. + * + *

This must not perform another search from the player's current pre-bank location. Doing so + * can select a different transport network from the route whose distance caused the banking + * decision, and then withdraw items for a route that will never be executed.

+ */ + public static List getRequiredTransportEdgesFromBank( + TransportRouteAnalysis analysis) + { + if (analysis == null || !analysis.isRouteFromBankStepsExact()) + { + return List.of(); + } + return applyTransportEdgeFiltering(analysis.getTransportEdgesFromBank()); + } + + /** + * Compatibility API for Hub plugins compiled against concrete shortest-path transports. + * New code must use {@link #getTransportEdgesForDestination(WorldPoint, boolean)}. + */ + @Deprecated public static List getTransportsForDestination(WorldPoint destination, boolean useBankItems, TransportType prefTransportType) { if (destination == null) { return new ArrayList<>(); } + WorldPoint start = Rs2Player.getWorldLocation(); + if (start == null) { + return new ArrayList<>(); + } - boolean originalUseBankItems = Rs2PathApi.getPathfinderConfig().isUseBankItems(); - try { - Rs2PathApi.getPathfinderConfig().setUseBankItems(useBankItems); - Rs2PathApi.getPathfinderConfig().refresh(); - Pathfinder pf = new Pathfinder(Rs2PathApi.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<>(); - } - - List transports = Rs2Walker.getTransportsForPath(path, 0, prefTransportType, true); - transports.forEach(t -> log.debug("Transport found: " + t)); - return transports; - } finally { - Rs2PathApi.getPathfinderConfig().setUseBankItems(originalUseBankItems); - Rs2PathApi.getPathfinderConfig().refresh(); + Rs2RouteResult route = Rs2PathApi.plan( + Rs2RouteRequest.to(start, destination) + .withBankItems(useBankItems)); + List path = route.getPath(); + if (path.isEmpty()) { + log.debug("Unable to find path to destination: " + destination); + return new ArrayList<>(); } + + // This deprecated return type cannot carry the planner-owned immutable edge, so retain the + // historical catalog view for binary compatibility. The active banking path above uses exact + // Rs2TransportEdge instances and never enters this endpoint-based adapter. + List transports = Rs2Walker.getTransportsForPath( + path, 0, prefTransportType, true); + transports.forEach(t -> log.debug("Transport found: " + t)); + return transports; } /** @@ -86,6 +162,82 @@ static boolean planningCoversPlainTransport(Transport transport) { || (transport.getItemIdRequirements() != null && !transport.getItemIdRequirements().isEmpty()); } + /** Legacy concrete-transport filter while banking consumers migrate to immutable edge views. */ + public 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.SPIRIT_TREE + || planningCoversPlainTransport(t) + || t.getType() == TransportType.SEASONAL_TRANSPORT + && Rs2LeaguesTransport.isLeaguesActive() + && t.getDisplayInfo() != null + && t.getDisplayInfo().toLowerCase().startsWith("leagues area:")) + .peek(t -> { + if (t.getType() == TransportType.FAIRY_RING + && (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))); + } + if (isCurrencyBasedTransport(t.getType()) + && (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); + } + } + }) + .collect(Collectors.toList()); + } + + static boolean planningCoversPlainTransportEdge(Rs2TransportEdge transport) + { + return transport != null + && transport.getType() == Rs2TransportType.TRANSPORT + && (transport.getCurrencyAmount() > 0 || !transport.getItemRequirements().isEmpty()); + } + + /** Filter selected immutable route edges down to transports relevant to bank preparation. */ + public static List applyTransportEdgeFiltering( + List transports) + { + if (transports == null) + { + return List.of(); + } + return transports.stream() + .filter(transport -> transport.getType() == Rs2TransportType.TELEPORTATION_ITEM + || transport.getType() == Rs2TransportType.FAIRY_RING + || transport.getType() == Rs2TransportType.TELEPORTATION_SPELL + || transport.getType() == Rs2TransportType.CANOE + || transport.getType() == Rs2TransportType.BOAT + || transport.getType() == Rs2TransportType.CHARTER_SHIP + || transport.getType() == Rs2TransportType.SHIP + || transport.getType() == Rs2TransportType.MINECART + || transport.getType() == Rs2TransportType.MAGIC_CARPET + || transport.getType() == Rs2TransportType.SPIRIT_TREE + || planningCoversPlainTransportEdge(transport) + || transport.getType() == Rs2TransportType.SEASONAL_TRANSPORT + && Rs2LeaguesTransport.isLeaguesActive() + && transport.getDisplayInfo() != null + && transport.getDisplayInfo().toLowerCase(Locale.ROOT) + .startsWith("leagues area:")) + .collect(Collectors.toUnmodifiableList()); + } + public static boolean hasRequiredTransportItems(Transport transport) { if (transport == null) { return false; @@ -107,6 +259,14 @@ public static boolean hasRequiredTransportItems(Transport transport) { || transport.getType() == TransportType.MAGIC_CARPET || planningCoversPlainTransport(transport)) { if (transport.getType() == TransportType.TELEPORTATION_SPELL && transport.getDisplayInfo() != null) { + if (!transport.getItemRequirements().isEmpty()) { + return TransportItemRequirement.selectProviders( + transport.getItemRequirements(), + Rs2WalkerBankingPlanner::carriedRequirementItemQuantity, + itemId -> Rs2Equipment.isWearing(itemId) || Rs2Inventory.hasItem(itemId), + itemId -> Rs2Equipment.isWearing(itemId) || Rs2Inventory.hasItem(itemId)) + .isPresent(); + } String spellName = transport.getDisplayInfo().contains(":") ? transport.getDisplayInfo().split(":")[0].trim() : transport.getDisplayInfo().trim(); @@ -124,21 +284,97 @@ public static boolean hasRequiredTransportItems(Transport transport) { && !transport.getCurrencyName().isEmpty() && transport.getCurrencyAmount() > 0) { int currencyItemId = getCurrencyItemId(transport.getCurrencyName()); - return Rs2Inventory.count(currencyItemId) >= transport.getCurrencyAmount(); + return Rs2Inventory.itemQuantity(currencyItemId) >= transport.getCurrencyAmount(); } if (transport.getItemIdRequirements() == null || transport.getItemIdRequirements().isEmpty()) { return true; } - return transport.getItemIdRequirements() - .stream() - .flatMap(Collection::stream) - .anyMatch(itemId -> Rs2Equipment.isWearing(itemId) || Rs2Inventory.hasItem(itemId)); + return transport.getItemRequirements().stream() + .allMatch(requirement -> requirement.isSatisfiedBy( + Rs2WalkerBankingPlanner::carriedItemQuantity)); } return true; } + public static boolean hasRequiredTransportEdgeItems(Rs2TransportEdge transport) + { + if (transport == null) + { + return false; + } + if (transport.getType() == Rs2TransportType.FAIRY_RING) + { + return hasFairyRingAccess(); + } + if (!isBankPlanningTransport(transport)) + { + return true; + } + if (isSpellTransport(transport) && transport.getDisplayInfo() != null) + { + if (!transport.getItemRequirements().isEmpty()) + { + return Rs2TransportItemRequirement.selectProviders( + transport.getItemRequirements(), + Rs2WalkerBankingPlanner::carriedRequirementItemQuantity, + itemId -> Rs2Equipment.isWearing(itemId) || Rs2Inventory.hasItem(itemId), + itemId -> Rs2Equipment.isWearing(itemId) || Rs2Inventory.hasItem(itemId)) + .isPresent(); + } + Rs2Spells rs2Spell = Rs2Magic.getRs2Spell(spellLookupName(transport.getDisplayInfo())); + return rs2Spell != null && Rs2Magic.hasRequiredRunes(rs2Spell); + } + if (isCurrencyBasedTransport(transport.getType()) + && transport.getItemRequirements().isEmpty() + && !transport.getCurrencyName().isEmpty() + && transport.getCurrencyAmount() > 0) + { + int currencyItemId = getCurrencyItemId(transport.getCurrencyName()); + return currencyItemId > 0 + && Rs2Inventory.itemQuantity(currencyItemId) >= transport.getCurrencyAmount(); + } + return transport.getItemRequirements().stream() + .allMatch(requirement -> requirement.isSatisfiedBy( + Rs2WalkerBankingPlanner::carriedItemQuantity)); + } + + private static boolean hasFairyRingAccess() + { + return Rs2Inventory.hasItem(ItemID.DRAMEN_STAFF) + || Rs2Equipment.isWearing(ItemID.DRAMEN_STAFF) + || Rs2Inventory.hasItem(ItemID.LUNAR_MOONCLAN_LIMINAL_STAFF) + || Rs2Equipment.isWearing(ItemID.LUNAR_MOONCLAN_LIMINAL_STAFF) + || Microbot.getVarbitValue(VarbitID.LUMBRIDGE_DIARY_ELITE_COMPLETE) == 1; + } + + private static boolean isBankPlanningTransport(Rs2TransportEdge transport) + { + Rs2TransportType type = transport.getType(); + return type == Rs2TransportType.TELEPORTATION_ITEM + || isSpellTransport(transport) + || type == Rs2TransportType.CANOE + || type == Rs2TransportType.BOAT + || type == Rs2TransportType.CHARTER_SHIP + || type == Rs2TransportType.SHIP + || type == Rs2TransportType.MINECART + || type == Rs2TransportType.MAGIC_CARPET + || planningCoversPlainTransportEdge(transport); + } + + private static boolean isSpellTransport(Rs2TransportEdge transport) + { + return transport.getType() == Rs2TransportType.TELEPORTATION_SPELL; + } + + private static String spellLookupName(String displayInfo) + { + return displayInfo.contains(":") + ? displayInfo.split(":", 2)[0].trim().toLowerCase(Locale.ROOT) + : displayInfo.trim().toLowerCase(Locale.ROOT); + } + public static List getMissingTransports(List transports) { if (transports == null) { return new ArrayList<>(); @@ -149,7 +385,32 @@ public static List getMissingTransports(List transports) { .collect(Collectors.toList()); } + public static List getMissingTransportEdges( + List transports) + { + if (transports == null) + { + return List.of(); + } + return transports.stream() + .filter(transport -> !hasRequiredTransportEdgeItems(transport)) + .collect(Collectors.toUnmodifiableList()); + } + public static Map getMissingTransportItemIdsWithQuantities(List transports) { + return getMissingTransportItemIdsWithQuantities(transports, Rs2Bank::count); + } + + /** + * Pure selection seam for tests and callers that already hold a bank snapshot. + * + *

The public entry point supplies {@link Rs2Bank#count(int)}. Keeping the provider outside the + * selection rules prevents headless tests from waiting on the client thread and lets the AND/OR + * choice policy be verified independently of the bank widget. + */ + static Map getMissingTransportItemIdsWithQuantities( + List transports, + IntUnaryOperator bankQuantityProvider) { if (transports == null) { return new HashMap<>(); } @@ -162,7 +423,7 @@ public static Map getMissingTransportItemIdsWithQuantities(Lis if (!spellRuneRequirements.isEmpty()) { spellRuneRequirements.forEach((runeItemId, requiredQuantity) -> { try { - int bankQuantity = Rs2Bank.count(runeItemId); + int bankQuantity = bankQuantityProvider.applyAsInt(runeItemId); int currentQuantity = itemQuantityMap.getOrDefault(runeItemId, 0); itemQuantityMap.put(runeItemId, currentQuantity + requiredQuantity); log.debug("Added teleportation spell rune requirement: {} (ID: {}) x{} (bank has: {} short={})", @@ -194,22 +455,35 @@ public static Map getMissingTransportItemIdsWithQuantities(Lis return; } - if (transport.getItemIdRequirements() != null) { - for (Set alternativeItems : transport.getItemIdRequirements()) { - int requiredQuantity = (isCurrencyBasedTransport(transport.getType()) && transport.getCurrencyAmount() > 0) - ? transport.getCurrencyAmount() - : 1; + if (transport.getItemRequirements() != null) { + for (TransportItemRequirement requirement : transport.getItemRequirements()) { + if (requirement.isSatisfiedBy(Rs2WalkerBankingPlanner::carriedItemQuantity)) { + continue; + } + Set alternativeItems = requirement.getItemIds(); Integer preferredItemId = null; int preferredBankQuantity = 0; for (Integer itemId : alternativeItems) { + int requiredQuantity = requirement.getRequiredQuantity(itemId); + if (requiredQuantity == 0) { + continue; + } int bankQuantity = 0; try { - bankQuantity = Rs2Bank.count(itemId); + bankQuantity = bankQuantityProvider.applyAsInt(itemId); } catch (Exception e) { log.debug("Could not check bank for item " + itemId + ": " + e.getMessage()); } - if (preferredItemId == null || bankQuantity > preferredBankQuantity) { + int preferredRequired = preferredItemId == null + ? Integer.MAX_VALUE + : requirement.getRequiredQuantity(preferredItemId); + boolean satisfies = bankQuantity >= requiredQuantity; + boolean preferredSatisfies = preferredItemId != null + && preferredBankQuantity >= preferredRequired; + if (preferredItemId == null + || (satisfies && !preferredSatisfies) + || (satisfies == preferredSatisfies && bankQuantity > preferredBankQuantity)) { preferredItemId = itemId; preferredBankQuantity = bankQuantity; } @@ -226,23 +500,22 @@ public static Map getMissingTransportItemIdsWithQuantities(Lis .orElse(null); int currencyItemId = purchasable == null ? -1 : getCurrencyItemId(purchasable.costCurrencyName); if (currencyItemId > 0) { - // One fare per required ITEM. requiredQuantity above is a currency - // amount for currency-based rows, so it must not be used as a count. + int requiredQuantity = requirement.getRequiredQuantity(purchasable.itemId); int itemsNeeded = isCurrencyBasedTransport(transport.getType()) ? 1 : requiredQuantity; int fare = purchasable.costAmount * itemsNeeded; itemQuantityMap.merge(currencyItemId, fare, Integer::sum); log.debug("Transport item {} not banked but purchasable — withdrawing fare {} x{} instead", purchasable.itemId, purchasable.costCurrencyName, fare); - break; + continue; } } if (preferredItemId != null) { + int requiredQuantity = requirement.getRequiredQuantity(preferredItemId); int currentQuantity = itemQuantityMap.getOrDefault(preferredItemId, 0); itemQuantityMap.put(preferredItemId, currentQuantity + requiredQuantity); log.debug("Added transport item requirement: itemId={} x{} (bank has: {} short={})", preferredItemId, requiredQuantity, preferredBankQuantity, preferredBankQuantity < requiredQuantity); } - break; } } }); @@ -250,6 +523,300 @@ public static Map getMissingTransportItemIdsWithQuantities(Lis return itemQuantityMap; } + public static Map getMissingTransportEdgeItemIdsWithQuantities( + List transports) + { + return getMissingTransportEdgeLoadout(transports).getWithdrawals(); + } + + /** + * Build one atomic preparation contract for the exact selected edges. + * + *

Rune quantities include the inventory, rune pouch, equipped providers and combination runes. + * Bank rune quantities are kept separate from raw bank stacks because a semantic contribution + * still has to resolve to a concrete item that can actually be withdrawn.

+ */ + public static Rs2TransportLoadout getMissingTransportEdgeLoadout( + List transports) + { + Map carriedRunes = runeQuantities( + RuneFilter.builder().includeBank(false).build()); + Map bankRunes = runeQuantities(RuneFilter.builder() + .includeInventory(false) + .includeEquipment(false) + .includeRunePouch(false) + .includeBank(true) + .build()); + return getMissingTransportEdgeLoadout( + transports, + Rs2Bank::count, + Rs2WalkerBankingPlanner::carriedItemQuantity, + itemId -> carriedRunes.getOrDefault(itemId, carriedItemQuantity(itemId)), + itemId -> bankRunes.getOrDefault(itemId, safeQuantity(Rs2Bank::count, itemId)), + Rs2Equipment::isWearing); + } + + /** Compatibility view for callers that only consume withdrawals. */ + static Map getMissingTransportEdgeItemIdsWithQuantities( + List transports, + IntUnaryOperator bankQuantityProvider, + IntUnaryOperator carriedQuantityProvider) + { + return getMissingTransportEdgeLoadout( + transports, + bankQuantityProvider, + carriedQuantityProvider, + carriedQuantityProvider, + bankQuantityProvider, + ignored -> false).getWithdrawals(); + } + + /** Pure source-aware selection seam used by banking regressions. */ + static Rs2TransportLoadout getMissingTransportEdgeLoadout( + List transports, + IntUnaryOperator bankQuantityProvider, + IntUnaryOperator carriedQuantityProvider, + IntUnaryOperator carriedRequirementQuantityProvider, + IntUnaryOperator bankRequirementQuantityProvider, + IntPredicate equippedItemProvider) + { + if (transports == null) + { + return Rs2TransportLoadout.empty(); + } + Map withdrawals = new LinkedHashMap<>(); + LinkedHashSet equipmentItemIds = new LinkedHashSet<>(); + for (Rs2TransportEdge transport : transports) + { + if (isSpellTransport(transport) && transport.getItemRequirements().isEmpty()) + { + for (Map.Entry rune : getSpellRuneRequirements(transport).entrySet()) + { + int bankQuantity = safeQuantity(bankQuantityProvider, rune.getKey()); + if (bankQuantity < rune.getValue()) + { + return Rs2TransportLoadout.unavailable(); + } + withdrawals.merge(rune.getKey(), rune.getValue(), Integer::sum); + } + continue; + } + + if (isCurrencyBasedTransport(transport.getType()) + && transport.getCurrencyAmount() > 0 + && transport.getItemRequirements().isEmpty()) + { + int currencyItemId = getCurrencyItemId(transport.getCurrencyName()); + if (currencyItemId > 0) + { + withdrawals.merge( + currencyItemId, transport.getCurrencyAmount(), Integer::sum); + } + continue; + } + + List requirements = transport.getItemRequirements(); + if (transport.getType() == Rs2TransportType.FAIRY_RING + && requirements.isEmpty() + && Microbot.getVarbitValue(VarbitID.LUMBRIDGE_DIARY_ELITE_COMPLETE) != 1) + { + requirements = List.of(new Rs2TransportItemRequirement(Map.of( + ItemID.DRAMEN_STAFF, 1, + ItemID.LUNAR_MOONCLAN_LIMINAL_STAFF, 1))); + } + + Rs2TransportItemRequirement.ProviderSelection providers = + Rs2TransportItemRequirement.selectEquipmentProviders( + requirements, + itemId -> safeSum( + safeQuantity(carriedRequirementQuantityProvider, itemId), + safeQuantity(bankRequirementQuantityProvider, itemId)), + itemId -> safeSum( + safeQuantity(carriedQuantityProvider, itemId), + safeQuantity(bankQuantityProvider, itemId)) > 0, + itemId -> safeSum( + safeQuantity(carriedQuantityProvider, itemId), + safeQuantity(bankQuantityProvider, itemId)) > 0) + .orElse(null); + if (providers == null) + { + return Rs2TransportLoadout.unavailable(); + } + if (!addProviderPreparation( + providers.getStaffItemId(), withdrawals, equipmentItemIds, + bankQuantityProvider, carriedQuantityProvider, equippedItemProvider) + || !addProviderPreparation( + providers.getOffhandItemId(), withdrawals, equipmentItemIds, + bankQuantityProvider, carriedQuantityProvider, equippedItemProvider)) + { + return Rs2TransportLoadout.unavailable(); + } + + for (Rs2TransportItemRequirement requirement : requirements) + { + if (requirement.isSatisfiedBy(carriedRequirementQuantityProvider) + || requirement.getStaffAlternatives().contains(providers.getStaffItemId()) + || requirement.getOffhandAlternatives().contains(providers.getOffhandItemId())) + { + continue; + } + if (!addPreferredRequirement( + withdrawals, + requirement.getAlternatives(), + transport.getType(), + bankQuantityProvider, + carriedRequirementQuantityProvider)) + { + return Rs2TransportLoadout.unavailable(); + } + } + } + if (withdrawals.isEmpty() && equipmentItemIds.isEmpty()) + { + return Rs2TransportLoadout.empty(); + } + return new Rs2TransportLoadout( + withdrawals, new ArrayList<>(equipmentItemIds), true); + } + + private static boolean addProviderPreparation( + int itemId, + Map withdrawals, + Set equipmentItemIds, + IntUnaryOperator bankQuantityProvider, + IntUnaryOperator carriedQuantityProvider, + IntPredicate equippedItemProvider) + { + if (itemId <= 0 || equippedItemProvider.test(itemId)) + { + return true; + } + equipmentItemIds.add(itemId); + if (safeQuantity(carriedQuantityProvider, itemId) > 0) + { + return true; + } + if (safeQuantity(bankQuantityProvider, itemId) <= 0) + { + return false; + } + withdrawals.merge(itemId, 1, Math::max); + return true; + } + + private static boolean addPreferredRequirement( + Map requested, + Map alternatives, + Rs2TransportType transportType, + IntUnaryOperator bankQuantityProvider, + IntUnaryOperator carriedQuantityProvider) + { + Integer preferredItemId = null; + int preferredBankQuantity = 0; + int preferredDeficit = Integer.MAX_VALUE; + for (Map.Entry alternative : alternatives.entrySet()) + { + int itemId = alternative.getKey(); + int requiredQuantity = alternative.getValue(); + if (requiredQuantity == 0) + { + continue; + } + int bankQuantity = safeQuantity(bankQuantityProvider, itemId); + int deficit = Math.max(0, + requiredQuantity - safeQuantity(carriedQuantityProvider, itemId)); + boolean satisfies = bankQuantity >= deficit; + boolean preferredSatisfies = preferredItemId != null + && preferredBankQuantity >= preferredDeficit; + if (preferredItemId == null + || satisfies && !preferredSatisfies + || satisfies == preferredSatisfies && deficit < preferredDeficit + || satisfies == preferredSatisfies && deficit == preferredDeficit + && bankQuantity > preferredBankQuantity) + { + preferredItemId = itemId; + preferredBankQuantity = bankQuantity; + preferredDeficit = deficit; + } + } + + if (preferredItemId == null) + { + return false; + } + if (preferredBankQuantity < preferredDeficit) + { + PurchasableItemCatalog.PurchasableItem purchasable = alternatives.keySet().stream() + .map(PurchasableItemCatalog::byItemId) + .filter(java.util.Objects::nonNull) + .findFirst() + .orElse(null); + int currencyItemId = purchasable == null + ? -1 : getCurrencyItemId(purchasable.costCurrencyName); + if (currencyItemId > 0) + { + int requiredQuantity = alternatives.get(purchasable.itemId); + int itemsNeeded = isCurrencyBasedTransport(transportType) + ? 1 : requiredQuantity; + requested.merge( + currencyItemId, purchasable.costAmount * itemsNeeded, Integer::sum); + return true; + } + return false; + } + if (preferredDeficit > 0) + { + requested.merge(preferredItemId, preferredDeficit, Integer::sum); + } + return true; + } + + private static Map runeQuantities(RuneFilter filter) + { + Map quantities = new HashMap<>(); + Rs2Magic.getRunes(filter).forEach((rune, quantity) -> + quantities.put(rune.getItemId(), quantity)); + return quantities; + } + + private static int safeSum(int first, int second) + { + long sum = (long) Math.max(0, first) + Math.max(0, second); + return sum >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) sum; + } + + private static int safeQuantity(IntUnaryOperator provider, int itemId) + { + try + { + return Math.max(0, provider.applyAsInt(itemId)); + } + catch (Exception exception) + { + log.debug("Could not check bank for item {}: {}", itemId, exception.getMessage()); + return 0; + } + } + + private static int carriedItemQuantity(int itemId) { + int quantity = Rs2Inventory.itemQuantity(itemId); + net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel equipped = Rs2Equipment.get(itemId); + if (equipped != null) { + quantity += Math.max(1, equipped.getQuantity()); + } + return quantity; + } + + private static int carriedRequirementItemQuantity(int itemId) + { + Runes rune = Runes.byItemId(itemId); + if (rune == null) + { + return carriedItemQuantity(itemId); + } + return Rs2Magic.getRunes().getOrDefault(rune, 0); + } + public static List getMissingTransportItemIds(List transports) { return new ArrayList<>(getMissingTransportItemIdsWithQuantities(transports).keySet()); } @@ -273,7 +840,10 @@ public static TransportRouteAnalysis compareRoutes(WorldPoint startPoint, WorldP try { performanceLog.append("\tStart Point: ").append(startPoint).append(", Target: ").append(target).append("\n"); long directPathStartTime = System.nanoTime(); - List directPath = Rs2Walker.getWalkPath(startPoint, target); + Rs2RouteResult directRoute = planRoute( + startPoint, target, false, Rs2RouteRequest.Purpose.BANK_ROUTE_DIRECT); + List directPath = directRoute.getPath(); + List directRouteSteps = directRoute.getSteps(); long directPathEndTime = System.nanoTime(); double directPathTimeMs = (directPathEndTime - directPathStartTime) / 1_000_000.0; @@ -283,47 +853,56 @@ public static TransportRouteAnalysis compareRoutes(WorldPoint startPoint, WorldP BankLocation nearestBank = null; List pathToBank = new ArrayList<>(); + List routeToBankSteps = List.of(); List pathFromBankToTarget = new ArrayList<>(); + List routeFromBankSteps = List.of(); int bankingRouteDistance = -1; try { - boolean originalUseBankItems = Rs2PathApi.getPathfinderConfig().isUseBankItems(); - try { - Rs2PathApi.getPathfinderConfig().setUseBankItems(true); - Rs2PathApi.getPathfinderConfig().refresh(target); - - performanceLog.append("\t-Bank items available: ").append(Rs2Bank.bankItems().size()).append("\n"); + performanceLog.append("\t-Bank items available: ").append(Rs2Bank.bankItems().size()).append("\n"); - long bankSearchStartTime = System.nanoTime(); - nearestBank = Rs2Bank.getNearestBank(startPoint); - long bankSearchEndTime = System.nanoTime(); - double bankSearchTimeMs = (bankSearchEndTime - bankSearchStartTime) / 1_000_000.0; + long bankSearchStartTime = System.nanoTime(); + nearestBank = Rs2Bank.getNearestBank(startPoint); + long bankSearchEndTime = System.nanoTime(); + double bankSearchTimeMs = (bankSearchEndTime - bankSearchStartTime) / 1_000_000.0; - if (nearestBank != null) { + if (nearestBank != null) { WorldPoint bankLocation = nearestBank.getWorldPoint(); performanceLog.append("\t-Nearest bank search: ").append(String.format("%.2f ms", bankSearchTimeMs)); performanceLog.append("\t -> Found: ").append(nearestBank).append(" at ").append(bankLocation).append("\n"); long pathToBankStartTime = System.nanoTime(); - pathToBank = Rs2Walker.getWalkPath(startPoint, bankLocation); + Rs2RouteResult bankRoute = planRoute( + startPoint, bankLocation, false, + Rs2RouteRequest.Purpose.BANK_ROUTE_TO_BANK); + pathToBank = bankRoute.getPath(); + routeToBankSteps = bankRoute.getSteps(); long pathToBankEndTime = System.nanoTime(); double pathToBankTimeMs = (pathToBankEndTime - pathToBankStartTime) / 1_000_000.0; int distanceToBank = Rs2Walker.getTotalTilesFromPath(pathToBank, bankLocation); long pathFromBankStartTime = System.nanoTime(); - pathFromBankToTarget = Rs2Walker.getWalkPath(bankLocation, target); + Rs2RouteResult bankTargetRoute = planRoute( + bankLocation, target, true, + Rs2RouteRequest.Purpose.BANK_ROUTE_FROM_BANK); + pathFromBankToTarget = bankTargetRoute.getPath(); + routeFromBankSteps = bankTargetRoute.getSteps(); long pathFromBankEndTime = System.nanoTime(); double pathFromBankTimeMs = (pathFromBankEndTime - pathFromBankStartTime) / 1_000_000.0; - List bankLegTransports = Rs2Walker.getTransportsForPath( - pathFromBankToTarget, 0, TransportType.TELEPORTATION_SPELL, true); + List bankLegTransports = bankTargetRoute.getTransportSteps().stream() + .map(Rs2RouteStep::getTransport) + .map(transport -> transport.orElseThrow( + () -> new IllegalStateException("transport step has no edge"))) + .collect(Collectors.toList()); long spellCount = bankLegTransports.stream() - .filter(t -> t.getType() == TransportType.TELEPORTATION_SPELL) + .filter(t -> t.getType() == Rs2TransportType.TELEPORTATION_SPELL) .count(); long itemCount = bankLegTransports.stream() - .filter(t -> t.getType() == TransportType.TELEPORTATION_ITEM) + .filter(t -> t.getType() == Rs2TransportType.TELEPORTATION_ITEM) .count(); int distanceFromBankRaw = Rs2Walker.getTotalTilesFromPath(pathFromBankToTarget, target); - int distanceFromBank = effectiveDistanceFromBank(pathFromBankToTarget, distanceFromBankRaw); + int distanceFromBank = effectiveDistanceFromBank( + pathFromBankToTarget, bankTargetRoute.getSteps(), distanceFromBankRaw); performanceLog.append("\t-Path to bank calculation: ").append(String.format("%.2f ms", pathToBankTimeMs)) .append(" (").append(pathToBank.size()).append(" waypoints, ").append(distanceToBank).append(" tiles)\n"); @@ -333,8 +912,8 @@ public static TransportRouteAnalysis compareRoutes(WorldPoint startPoint, WorldP .append(" spells=").append(spellCount) .append(" items=").append(itemCount) .append("\n"); - Transport firstSpellTransport = bankLegTransports.stream() - .filter(t -> t.getType() == TransportType.TELEPORTATION_SPELL) + Rs2TransportEdge firstSpellTransport = bankLegTransports.stream() + .filter(t -> t.getType() == Rs2TransportType.TELEPORTATION_SPELL) .findFirst() .orElse(null); if (firstSpellTransport != null) { @@ -366,13 +945,9 @@ public static TransportRouteAnalysis compareRoutes(WorldPoint startPoint, WorldP bankingRouteDistance = distanceToBank + distanceFromBank; } performanceLog.append("\t-Total banking route distance: ").append(bankingRouteDistance).append(" tiles\n"); - } else { - performanceLog.append("\t-Nearest bank search: ").append(String.format("%.2f ms", bankSearchTimeMs)) - .append("\t -> No accessible bank found\n"); - } - } finally { - Rs2PathApi.getPathfinderConfig().setUseBankItems(originalUseBankItems); - Rs2PathApi.getPathfinderConfig().refresh(); + } else { + performanceLog.append("\t-Nearest bank search: ").append(String.format("%.2f ms", bankSearchTimeMs)) + .append("\t -> No accessible bank found\n"); } } catch (Exception e) { performanceLog.append("Banking route calculation failed: ").append(e.getMessage()).append("\n"); @@ -388,7 +963,8 @@ public static TransportRouteAnalysis compareRoutes(WorldPoint startPoint, WorldP WebWalkLog.compareDetail(performanceLog.toString()); WebWalkLog.compareSummary(totalTimeMs, directDistance, -1, "direct_only_bank_unavailable"); return new TransportRouteAnalysis(directPath, null, null, new ArrayList<>(), new ArrayList<>(), - "Direct route only (banking route unavailable)"); + "Direct route only (banking route unavailable)", directDistance, -1, + directRouteSteps, List.of(), List.of()); } final boolean tie = directDistance == bankingRouteDistance; @@ -418,7 +994,8 @@ public static TransportRouteAnalysis compareRoutes(WorldPoint startPoint, WorldP return new TransportRouteAnalysis(directPath, nearestBank, nearestBank != null ? nearestBank.getWorldPoint() : null, pathToBank, pathFromBankToTarget, recommendation, - directDistance, bankingRouteDistance); + directDistance, bankingRouteDistance, + directRouteSteps, routeToBankSteps, routeFromBankSteps); } catch (Exception e) { long totalEndTime = System.nanoTime(); double totalTimeMs = (totalEndTime - totalStartTime) / 1_000_000.0; @@ -429,6 +1006,19 @@ public static TransportRouteAnalysis compareRoutes(WorldPoint startPoint, WorldP } } + private static Rs2RouteResult planRoute( + WorldPoint start, + WorldPoint target, + boolean useBankItems, + Rs2RouteRequest.Purpose purpose) + { + return Rs2PathApi.plan( + Rs2RouteRequest.to(start, target) + .withRefreshTarget(target) + .withBankItems(useBankItems) + .withPurpose(purpose)); + } + private static Map getSpellRuneRequirements(Transport transport) { Map runeRequirements = new HashMap<>(); if (transport.getType() != TransportType.TELEPORTATION_SPELL || transport.getDisplayInfo() == null) { @@ -465,6 +1055,31 @@ private static Map getSpellRuneRequirements(Transport transpor return runeRequirements; } + private static Map getSpellRuneRequirements(Rs2TransportEdge transport) + { + Map runeRequirements = new HashMap<>(); + if (!isSpellTransport(transport) || transport.getDisplayInfo() == null) + { + return runeRequirements; + } + try + { + Rs2Spells rs2Spell = Rs2Magic.getRs2Spell(spellLookupName(transport.getDisplayInfo())); + if (rs2Spell == null) + { + return runeRequirements; + } + Rs2Magic.getRequiredRunes(rs2Spell, 1, true).forEach((rune, quantity) -> + runeRequirements.put(rune.getItemId(), quantity)); + } + catch (Exception exception) + { + log.warn("Error getting spell rune requirements for transport '{}': {}", + transport.getDisplayInfo(), exception.getMessage()); + } + return runeRequirements; + } + /** Package-private so the planning tests can select the same rows this collector accepts. */ static boolean isCurrencyBasedTransport(TransportType transportType) { return transportType == TransportType.BOAT @@ -475,6 +1090,16 @@ static boolean isCurrencyBasedTransport(TransportType transportType) { || transportType == TransportType.TRANSPORT; } + static boolean isCurrencyBasedTransport(Rs2TransportType transportType) + { + return transportType == Rs2TransportType.BOAT + || transportType == Rs2TransportType.CHARTER_SHIP + || transportType == Rs2TransportType.SHIP + || transportType == Rs2TransportType.MINECART + || transportType == Rs2TransportType.MAGIC_CARPET + || transportType == Rs2TransportType.TRANSPORT; + } + private static int getCurrencyItemId(String currencyName) { if (currencyName == null || currencyName.trim().isEmpty()) { return -1; @@ -497,64 +1122,72 @@ private static int getCurrencyItemId(String currencyName) { * For originless TELEPORTATION_ITEM / TELEPORTATION_SPELL edges, trim pre-teleport walking * from the bank leg metric and keep the post-teleport tail. */ - private static int effectiveDistanceFromBank(List pathFromBankToTarget, int rawDistance) { - if (pathFromBankToTarget == null || pathFromBankToTarget.isEmpty() || rawDistance == Integer.MAX_VALUE) { - return rawDistance; - } + static int effectiveDistanceFromBank( + List pathFromBankToTarget, + List routeSteps, + int rawDistance) { + if (pathFromBankToTarget == null || pathFromBankToTarget.isEmpty() || rawDistance == Integer.MAX_VALUE) { + return rawDistance; + } + if (routeSteps == null || routeSteps.isEmpty()) { + return rawDistance; + } - List transports = Rs2Walker.getTransportsForPath(pathFromBankToTarget, 0, TransportType.TELEPORTATION_SPELL, true); - if (transports.isEmpty()) { - return rawDistance; - } + int firstTransportStep = -1; + Rs2TransportEdge firstTransport = null; + for (int i = 0; i < routeSteps.size(); i++) { + Rs2RouteStep step = routeSteps.get(i); + if (step != null && step.isTransport()) { + firstTransportStep = i; + firstTransport = step.getTransport().orElse(null); + break; + } + } + if (firstTransport == null) { + return rawDistance; + } - // Use first transport that the bank->target path actually consumes and model: - // walk_to_transport + transport_hop + post_transport_tail. - Transport firstTransport = transports.get(0); - int modeledDistance = transportModeledDistance(pathFromBankToTarget, firstTransport, rawDistance); + // Use the exact first transport selected by this route. Endpoint rematching is ambiguous when + // multiple catalog entries share an origin/destination pair and could score the wrong command. + int modeledDistance = transportModeledDistance( + pathFromBankToTarget, firstTransportStep, firstTransport, rawDistance); if (modeledDistance == Integer.MAX_VALUE) { return rawDistance; } return Math.min(rawDistance, modeledDistance); } - private static boolean isImmediateBankTeleport(Transport transport) { - if (transport == null || transport.getOrigin() != null) { - return false; - } - return transport.getType() == TransportType.TELEPORTATION_ITEM - || transport.getType() == TransportType.TELEPORTATION_SPELL; - } + private static boolean isImmediateBankTeleport(Rs2TransportEdge transport) { + if (transport == null || transport.getOrigin() != null) { + return false; + } + return transport.getType() == Rs2TransportType.TELEPORTATION_ITEM + || transport.getType() == Rs2TransportType.TELEPORTATION_SPELL; + } - private static int transportModeledDistance(List pathFromBankToTarget, Transport transport, int fallbackRawDistance) { - if (transport == null || pathFromBankToTarget == null || pathFromBankToTarget.isEmpty()) { - return fallbackRawDistance; - } + private static int transportModeledDistance( + List pathFromBankToTarget, + int transportStepIndex, + Rs2TransportEdge transport, + int fallbackRawDistance) { + if (transport == null || pathFromBankToTarget == null || pathFromBankToTarget.isEmpty() + || transportStepIndex < 0 || transportStepIndex >= pathFromBankToTarget.size() - 1) { + return fallbackRawDistance; + } - WorldPoint destination = transport.getDestination(); - if (destination == null) { - return fallbackRawDistance; - } - int destinationIndex = pathFromBankToTarget.indexOf(destination); - if (destinationIndex < 0) { - return fallbackRawDistance; - } - - int originIndex; - if (isImmediateBankTeleport(transport)) { - originIndex = 0; - } else { - WorldPoint origin = transport.getOrigin(); - originIndex = origin == null ? 0 : pathFromBankToTarget.indexOf(origin); - if (originIndex < 0) { - originIndex = 0; - } - } - - if (destinationIndex < originIndex) { - return fallbackRawDistance; - } + WorldPoint stepOrigin = pathFromBankToTarget.get(transportStepIndex); + WorldPoint stepDestination = pathFromBankToTarget.get(transportStepIndex + 1); + if (!stepDestination.equals(transport.getDestination())) { + return fallbackRawDistance; + } + if (!isImmediateBankTeleport(transport) + && transport.getOrigin() != null + && !stepOrigin.equals(transport.getOrigin())) { + return fallbackRawDistance; + } - int walkToTransport = Math.max(0, originIndex); + int destinationIndex = transportStepIndex + 1; + int walkToTransport = isImmediateBankTeleport(transport) ? 0 : transportStepIndex; int transportHop = 1; int postTransportTail = Math.max(0, pathFromBankToTarget.size() - destinationIndex); return walkToTransport + transportHop + postTransportTail; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/door/Rs2DoorProbe.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/door/Rs2DoorProbe.java index 5191cadb7df..e66aab862dc 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/door/Rs2DoorProbe.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/door/Rs2DoorProbe.java @@ -11,11 +11,11 @@ import net.runelite.api.TileObject; import net.runelite.api.WallObject; import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.shortestpath.Transport; -import net.runelite.client.plugins.microbot.shortestpath.TransportType; import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.walker.Rs2PathApi; +import net.runelite.client.plugins.microbot.util.walker.Rs2TransportEdge; +import net.runelite.client.plugins.microbot.util.walker.Rs2TransportType; /** * Door-probe logic that operates against a {@link DoorProbeContext} (the scan-scoped caches) and @@ -47,7 +47,7 @@ public static ObjectComposition resolveDoorComposition(DoorProbeContext ctx, Til /** * True when this scene object is the interactable listed on a transport catalog row (same - * coordinates and object ids as TSV loaded into {@link Rs2PathApi#getTransports()}), and is not + * coordinates and object ids as TSV loaded into the shortest-path catalog), and is not * itself door-like. Used to avoid treating a catalog transport as a plain door. */ public static boolean isCatalogTransportObject(TileObject object) { @@ -62,18 +62,10 @@ public static boolean isCatalogTransportObject(TileObject object) { if (id <= 0) { return false; } - Map> map = Rs2PathApi.getTransports(); - if (map == null || map.isEmpty()) { - return false; - } - for (int dx = -1; dx <= 1; dx++) { + for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { WorldPoint catalogOrigin = new WorldPoint(loc.getX() + dx, loc.getY() + dy, loc.getPlane()); - Set transports = map.get(catalogOrigin); - if (transports == null || transports.isEmpty()) { - continue; - } - for (Transport t : transports) { + for (Rs2TransportEdge t : Rs2PathApi.getCatalogTransportEdges(catalogOrigin)) { if (t != null && t.getObjectId() == id && !isDoorLikeCatalogTransport(t)) { return true; } @@ -83,11 +75,11 @@ public static boolean isCatalogTransportObject(TileObject object) { return false; } - public static boolean isDoorLikeCatalogTransport(Transport transport) { - if (transport == null || transport.getType() != TransportType.TRANSPORT) { + public static boolean isDoorLikeCatalogTransport(Rs2TransportEdge transport) { + if (transport == null || transport.getType() != Rs2TransportType.TRANSPORT) { return false; } - return Rs2DoorClassifier.isDoorLikeGameObjectName(transport.getName()) + return Rs2DoorClassifier.isDoorLikeGameObjectName(transport.getTarget()) || Rs2DoorClassifier.isDoorLikeGameObjectName(transport.getDisplayInfo()) || isDoorLikeTransportAction(transport.getAction()); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/lifecycle/Rs2WalkerLifecycleRuntime.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/lifecycle/Rs2WalkerLifecycleRuntime.java index 4ecbf8588d3..65bedbdd767 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/lifecycle/Rs2WalkerLifecycleRuntime.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/lifecycle/Rs2WalkerLifecycleRuntime.java @@ -1,22 +1,21 @@ package net.runelite.client.plugins.microbot.util.walker.lifecycle; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; import net.runelite.api.Player; 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.pathfinder.Pathfinder; import net.runelite.client.plugins.microbot.util.walker.Rs2PathApi; +import net.runelite.client.plugins.microbot.util.walker.Rs2PlannerShadowContext; +import net.runelite.client.plugins.microbot.util.walker.Rs2RouteRequest; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import net.runelite.client.ui.overlay.worldmap.WorldMapPoint; import net.runelite.client.ui.overlay.worldmap.WorldMapPointManager; import java.util.Set; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; +import java.util.Objects; @Slf4j public final class Rs2WalkerLifecycleRuntime { @@ -25,6 +24,24 @@ private Rs2WalkerLifecycleRuntime() { } public static void applyWalkerDestination(WorldPoint target) { + applyWalkerDestination(target, false); + } + + /** Apply a destination while retaining whether the request was a recovery/replan. */ + public static void applyWalkerDestination(WorldPoint target, boolean replan) { + applyWalkerDestination(target, replan + ? Rs2PlannerShadowContext.Invocation.ACTIVE_REPLAN + : Rs2PlannerShadowContext.Invocation.ACTIVE_ROUTE); + } + + /** Apply a destination with explicit, evidence-only invocation classification. */ + public static void applyWalkerDestination( + WorldPoint target, + Rs2PlannerShadowContext.Invocation invocation) { + Objects.requireNonNull(invocation, "invocation"); + if (invocation == Rs2PlannerShadowContext.Invocation.SYNCHRONOUS_QUERY) { + throw new IllegalArgumentException("active destination cannot be a synchronous query"); + } if (target == null) { return; } @@ -74,12 +91,12 @@ public static void applyWalkerDestination(WorldPoint target) { } return Rs2Player.getWorldLocation(); }); - final Pathfinder pathfinder = Rs2PathApi.getPathfinder(); - final WorldPoint effectiveStart = (Rs2PathApi.isStartPointSet() && pathfinder != null) - ? pathfinder.getStart() + final WorldPoint effectiveStart = Rs2PathApi.isStartPointSet() + ? Rs2PathApi.getActiveRouteStart().orElse(start) : start; Rs2PathApi.setLastLocation(effectiveStart); - Microbot.getClientThread().runOnSeperateThread(() -> restartPathfinding(effectiveStart, target)); + Microbot.getClientThread().runOnSeperateThread( + () -> restartPathfinding(effectiveStart, Set.of(target), invocation)); } public static boolean restartPathfinding(WorldPoint start, WorldPoint end) { @@ -87,56 +104,25 @@ public static boolean restartPathfinding(WorldPoint start, WorldPoint end) { } public static boolean restartPathfinding(WorldPoint start, Set ends) { - Pathfinder pathfinder = Rs2PathApi.getPathfinder(); - if (pathfinder != null) { - pathfinder.cancel(); - if (Rs2PathApi.getPathfinderFuture() != null) { - Rs2PathApi.getPathfinderFuture().cancel(true); - } - } + return restartPathfinding( + start, ends, Rs2PlannerShadowContext.Invocation.ACTIVE_ROUTE); + } - if (Rs2PathApi.getPathfindingExecutor() == null) { - ThreadFactory shortestPathNaming = new ThreadFactoryBuilder().setNameFormat("shortest-path-%d").build(); - Rs2PathApi.setPathfindingExecutor(Executors.newSingleThreadExecutor(shortestPathNaming)); + private static boolean restartPathfinding( + WorldPoint start, + Set ends, + Rs2PlannerShadowContext.Invocation invocation) { + if (start == null || ends == null || ends.isEmpty()) { + return false; } - WorldPoint refreshTarget = ends != null && !ends.isEmpty() ? ends.iterator().next() : null; - Rs2PathApi.getPathfinderConfig().refresh(refreshTarget); - if (Rs2Player.isInCave()) { - // Cave pathfinding runs synchronously, so no Future represents the pathfinder installed below. - // Clear the cancelled asynchronous handle instead of leaving stale "work in flight" state. - Rs2PathApi.setPathfinderFuture(null); - pathfinder = new Pathfinder(Rs2PathApi.getPathfinderConfig(), start, ends); - pathfinder.run(); - try { - Rs2PathApi.getPathfinderConfig().setIgnoreTeleportAndItems(true); - Pathfinder pathfinderWithoutTeleports = new Pathfinder(Rs2PathApi.getPathfinderConfig(), start, ends); - pathfinderWithoutTeleports.run(); - - boolean noTeleportPathAvailable = !pathfinderWithoutTeleports.getPath().isEmpty(); - boolean basePathAvailable = pathfinder != null && !pathfinder.getPath().isEmpty(); - if (!noTeleportPathAvailable) { - Rs2PathApi.setPathfinder(basePathAvailable ? pathfinder : pathfinderWithoutTeleports); - return true; - } - - WorldPoint lastPath = pathfinderWithoutTeleports.getPath().get(pathfinderWithoutTeleports.getPath().size() - 1); - int reachedDistance = Rs2Walker.config != null ? Rs2Walker.config.reachedDistance() : 10; - boolean pathWithoutTeleportsIsReachable = lastPath.distanceTo(ends.stream().findFirst().orElse(lastPath)) <= reachedDistance; - if (pathWithoutTeleportsIsReachable - && basePathAvailable - && pathfinder.getPath().size() >= pathfinderWithoutTeleports.getPath().size()) { - Rs2PathApi.setPathfinder(pathfinderWithoutTeleports); - } else { - Rs2PathApi.setPathfinder(basePathAvailable ? pathfinder : pathfinderWithoutTeleports); - } - } finally { - Rs2PathApi.getPathfinderConfig().setIgnoreTeleportAndItems(false); - } - } else { - Rs2PathApi.setPathfinder(new Pathfinder(Rs2PathApi.getPathfinderConfig(), start, ends)); - Rs2PathApi.setPathfinderFuture(Rs2PathApi.getPathfindingExecutor().submit(Rs2PathApi.getPathfinder())); - } - return true; + int reachedDistance = Rs2Walker.config != null ? Rs2Walker.config.reachedDistance() : 10; + return Rs2PathApi.restartActiveRoute( + Rs2RouteRequest.toAny(start, ends) + .withRefreshTarget(refreshTarget) + .withRefreshPolicy(Rs2RouteRequest.RefreshPolicy.ALWAYS), + Rs2Player.isInCave(), + reachedDistance, + invocation); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/LiveScene.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/LiveScene.java index 84a2cf4d99e..abf4ce9ebda 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/LiveScene.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/LiveScene.java @@ -2,9 +2,6 @@ import net.runelite.api.TileObject; import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.shortestpath.Transport; - -import java.util.Set; /** * Read-only snapshot of the live world an {@link ObstacleResolver} needs to classify a {@link PlannedEdge}, @@ -21,8 +18,8 @@ public interface LiveScene { /** Whether {@code tile} is walk-reachable from the player right now (live-scene collision BFS). */ boolean isReachable(WorldPoint tile); - /** Transports whose origin is {@code tile} (stairs/ladders/shortcuts/teleports), or empty. */ - Set transportsAt(WorldPoint tile); + /** Whether a transport starts at {@code tile} (stairs/ladders/shortcuts/teleports). */ + boolean hasTransportAt(WorldPoint tile); /** The top interactable object on {@code tile} (door/gate/rockfall/…), or {@code null} if none. */ TileObject objectAt(WorldPoint tile); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/Rs2LiveScene.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/Rs2LiveScene.java index b852876a287..cab98651f4d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/Rs2LiveScene.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/Rs2LiveScene.java @@ -2,13 +2,10 @@ import net.runelite.api.TileObject; import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.shortestpath.Transport; import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; import net.runelite.client.plugins.microbot.util.walker.Rs2PathApi; -import java.util.Collections; import java.util.Map; -import java.util.Set; /** * Live-client implementation of {@link LiveScene} — the read side of the P2 obstacle plumbing @@ -39,14 +36,9 @@ public boolean isReachable(WorldPoint tile) { } @Override - public Set transportsAt(WorldPoint tile) { - final Map> transports = Rs2PathApi.getTransports(); - if (transports == null) { - return Collections.emptySet(); - } - final Set at = transports.get(tile); - return at == null ? Collections.emptySet() : at; - } + public boolean hasTransportAt(WorldPoint tile) { + return Rs2PathApi.hasCatalogTransportOrigin(tile); + } @Override public TileObject objectAt(WorldPoint tile) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/Rs2ObstacleHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/Rs2ObstacleHandler.java index 5da72edd3b1..8a060b96291 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/Rs2ObstacleHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/Rs2ObstacleHandler.java @@ -70,7 +70,7 @@ public static RockfallResult handleRockfall(List path, int index) { if (path == null || path.isEmpty() || index < 0 || index >= path.size()) { return RockfallResult.NOT_APPLICABLE; } - if (Rs2PathApi.getPathfinder() == null) return RockfallResult.NOT_APPLICABLE; + if (!Rs2PathApi.getActiveRouteStatus().isPresent()) return RockfallResult.NOT_APPLICABLE; if (index == path.size() - 1) return RockfallResult.NOT_APPLICABLE; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/TransportResolver.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/TransportResolver.java index 2448abe27c9..c3f7efcf1a6 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/TransportResolver.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/obstacle/TransportResolver.java @@ -1,9 +1,6 @@ package net.runelite.client.plugins.microbot.util.walker.obstacle; import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.shortestpath.Transport; - -import java.util.Set; /** * Resolves a planned edge blocked because its far side is across a transport / agility shortcut (a stepping @@ -23,10 +20,9 @@ public boolean handles(PlannedEdge edge, LiveScene scene) { return false; } final WorldPoint origin = edge.from(); - final Set transports = scene.transportsAt(origin); - if (transports == null || transports.isEmpty()) { - return false; - } + if (!scene.hasTransportAt(origin)) { + return false; + } final WorldPoint player = scene.playerLocation(); // Applies only when the player is off the origin but can still reach it: then stepping onto it lets // the normal loop take the transport. When already on the origin, that loop owns it (not recovery); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/recovery/RouteRecovery.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/recovery/RouteRecovery.java index 7cb37b7275d..7ef59d6952d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/recovery/RouteRecovery.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/recovery/RouteRecovery.java @@ -1,10 +1,8 @@ package net.runelite.client.plugins.microbot.util.walker.recovery; import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.shortestpath.Transport; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.function.Predicate; @@ -213,21 +211,21 @@ public static boolean isLocalRecoveryCandidateOnForwardRoute(List ra * {@code maxEuclidean} tiles of the player), so recovery can walk the player ONTO it and let the normal * transport handler cross next tick. Returns {@code null} when none qualifies. *

- * Pure and fully injected ({@code reachable} tiles and the {@code transports} map are parameters), so it + * Pure and fully injected ({@code reachable} tiles and the transport-origin predicate are parameters), so it * is exercised headlessly by {@code RouteRecoveryTest} rather than requiring a live walk. Rationale: the * far-side fallback otherwise clicks the opposite bank, which the client cannot reach, so the player * loops on the near bank and the transport (which only dispatches while standing on its origin) never * fires. */ - public static WorldPoint findReachableTransportOriginAhead(List rawPath, - int startIndex, - WorldPoint playerLoc, - Set reachable, - Map> transports, - int maxEuclidean, - int forwardScanTiles) { - if (rawPath == null || rawPath.isEmpty() || playerLoc == null || reachable == null - || transports == null || transports.isEmpty() || startIndex < 0 || startIndex >= rawPath.size()) { + public static WorldPoint findReachableTransportOriginAhead(List rawPath, + int startIndex, + WorldPoint playerLoc, + Set reachable, + Predicate hasTransportOrigin, + int maxEuclidean, + int forwardScanTiles) { + if (rawPath == null || rawPath.isEmpty() || playerLoc == null || reachable == null + || hasTransportOrigin == null || startIndex < 0 || startIndex >= rawPath.size()) { return null; } int maxSq = maxEuclidean * maxEuclidean; @@ -243,8 +241,7 @@ public static WorldPoint findReachableTransportOriginAhead(List rawP if (euclideanSq(wp, playerLoc) > maxSq) { continue; // within minimap-click reach } - Set ts = transports.get(wp); - if (ts != null && !ts.isEmpty()) { + if (hasTransportOrigin.test(wp)) { return wp; // nearest reachable transport / shortcut origin ahead } } diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayRenderer.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayRenderer.java index 32f8f659778..a470bb96c59 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayRenderer.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayRenderer.java @@ -42,6 +42,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Objects; import javax.inject.Inject; import javax.inject.Singleton; import javax.swing.SwingUtilities; @@ -343,7 +344,7 @@ else if (preferredLocation != null) graphics.setPaint(paint); graphics.setRenderingHints(renderingHints); graphics.setBackground(background); - if (!graphics.getClip().equals(clip)) + if (!Objects.equals(graphics.getClip(), clip)) { graphics.setClip(clip); } diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/agility_shortcuts.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/agility_shortcuts.tsv index 023b0053d03..7b1095334d7 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/agility_shortcuts.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/agility_shortcuts.tsv @@ -5,14 +5,17 @@ 2556 3074 1 2556 3075 0 Jump;Wall;17048 4 Agility 2936 3355 0 2934 3355 0 Climb-over;Crumbling wall;24222 5 Agility 2934 3355 0 2936 3355 0 Climb-over;Crumbling wall;24222 5 Agility -3246 3179 0 3259 3179 0 Grapple;Broken Raft;17068 8 Agility;37 Ranged;19 Strength -3259 3179 0 3246 3179 0 Grapple;Broken Raft;17068 8 Agility;37 Ranged;19 Strength +3246 3179 0 3259 3179 0 Grapple;Broken Raft;17068 8 Agility;37 Ranged;19 Strength CROSSBOW=1&MITH_GRAPPLE=1 +3259 3179 0 3246 3179 0 Grapple;Broken Raft;17068 8 Agility;37 Ranged;19 Strength CROSSBOW=1&MITH_GRAPPLE=1 2546 2873 0 2546 2871 0 Climb;Rocks;31757 10 Agility 2546 2871 0 2546 2873 0 Climb;Rocks;31757 10 Agility -2766 3665 0 2766 3663 0 Use;Rope -> Boulder;5842 10 Agility 237 10 260>=1 -3033 3390 0 3033 3389 1 Grapple;Wall;17049 11 Agility;19 Ranged;37 Strength 9419 -3032 3388 0 3032 3389 1 Grapple;Wall;17050 11 Agility;19 Ranged;37 Strength 9419 -2575 3107 0 2575 3112 0 Climb-under;Castle wall;16519 16 Agility +2766 3665 0 2766 3663 0 Use;Rope -> Boulder;5842 10 Agility ROPE=1 260>0 10 +3033 3390 0 3033 3389 1 Grapple;Wall;17049 11 Agility;19 Ranged;37 Strength CROSSBOW=1&MITH_GRAPPLE=1 +3032 3388 0 3032 3389 1 Grapple;Wall;17050 11 Agility;19 Ranged;37 Strength CROSSBOW=1&MITH_GRAPPLE=1 +2820 3635 0 2822 3635 0 Climb;Rocks;3748 15 Agility CLIMBING_BOOTS=1 +2857 3611 0 2857 3613 0 Climb;Rocks;3748 15 Agility CLIMBING_BOOTS=1 +2856 3611 0 2856 3613 0 Climb;Rocks;3748 15 Agility CLIMBING_BOOTS=1 +2575 3107 0 2575 3112 0 Climb-under;Castle wall;16519 16 Agility 2575 3112 0 2575 3107 0 Climb-into;Hole;16520 16 Agility 2603 3477 0 2598 3477 0 Walk-across;Log balance;23274 20 Agility 2599 3478 0 2603 3477 0 Walk-across;Log balance;23274 20 Agility @@ -35,17 +38,17 @@ 3153 3363 0 3152 3363 0 Jump-onto;Stepping stone;16533 31 Agility 4 3152 3363 0 3151 3363 0 Jump-onto;Stepping stone;16533 31 Agility 4 3151 3363 0 3150 3363 0 Jump-onto;Stepping stone;16533 31 Agility 4 -2866 3428 0 2869 3428 0 Grapple;Rocks;17042 32 Agility;35 Ranged;35 Strength 9419 +2866 3428 0 2869 3428 0 Grapple;Rocks;17042 32 Agility;35 Ranged;35 Strength CROSSBOW=1&MITH_GRAPPLE=1 2602 3336 0 2598 3336 0 Walk-across;Log balance;16548 33 Agility 2598 3336 0 2602 3336 0 Walk-across;Log balance;16546 33 Agility 2599 3337 0 2602 3336 0 Walk-across;Log balance;16546 33 Agility -2841 3427 0 2841 3433 0 Grapple Crossbow;Tree;17062 36 Agility;39 Ranged;22 Strength 9419 +2841 3427 0 2841 3433 0 Grapple Crossbow;Tree;17062 36 Agility;39 Ranged;22 Strength CROSSBOW=1&MITH_GRAPPLE=1 2486 3515 0 2489 3521 0 Climb;Rocks;16534 37 Agility The Grand Tree 9 2489 3521 0 2486 3515 0 Climb;Rocks;16535 37 Agility The Grand Tree 9 3306 3315 0 3302 3315 0 Climb;Rocks;16549 38 Agility 3302 3315 0 3306 3315 0 Climb;Rocks;16550 38 Agility -2556 3072 0 2556 3073 1 Grapple;Wall;17047 39 Agility;21 Ranged;38 Strength 9419 -2556 3075 0 2556 3074 1 Grapple;Wall;17047 39 Agility;21 Ranged;38 Strength 9419 +2556 3072 0 2556 3073 1 Grapple;Wall;17047 39 Agility;21 Ranged;38 Strength CROSSBOW=1&MITH_GRAPPLE=1 +2556 3075 0 2556 3074 1 Grapple;Wall;17047 39 Agility;21 Ranged;38 Strength CROSSBOW=1&MITH_GRAPPLE=1 2872 3671 0 2869 3671 0 Climb;Rocks;16521 41 Agility 2869 3671 0 2872 3671 0 Climb;Rocks;16521 41 Agility 3070 3260 0 3064 3260 0 Climb-into;Underwall tunnel;19036 42 Agility @@ -86,10 +89,10 @@ 1769 3849 0 1774 3849 0 Climb;Rocks;27988 52 Agility 1774 3849 0 1769 3849 0 Climb;Rocks;27987 52 Agility 2998 3916 0 2998 3931 0 Open;Door;23555 52 Agility -2874 3133 0 2874 3127 0 Grapple;Strong Tree;17074 53 Agility;42 Ranged;21 Strength 9419 -2874 3127 0 2874 3133 0 Grapple;Strong Tree;17074 53 Agility;42 Ranged;21 Strength 9419 -2874 3136 0 2874 3142 0 Grapple;Strong Tree;17074 53 Agility;42 Ranged;21 Strength 9419 -2874 3142 0 2874 3136 0 Grapple;Strong Tree;17074 53 Agility;42 Ranged;21 Strength 9419 +2874 3133 0 2874 3127 0 Grapple;Strong Tree;17074 53 Agility;42 Ranged;21 Strength CROSSBOW=1&MITH_GRAPPLE=1 +2874 3127 0 2874 3133 0 Grapple;Strong Tree;17074 53 Agility;42 Ranged;21 Strength CROSSBOW=1&MITH_GRAPPLE=1 +2874 3136 0 2874 3142 0 Grapple;Strong Tree;17074 53 Agility;42 Ranged;21 Strength CROSSBOW=1&MITH_GRAPPLE=1 +2874 3142 0 2874 3136 0 Grapple;Strong Tree;17074 53 Agility;42 Ranged;21 Strength CROSSBOW=1&MITH_GRAPPLE=1 2573 3859 0 2575 3861 0 Cross;Stepping stone;11768 55 Agility 2575 3861 0 2573 3859 0 Cross;Stepping stone;11768 55 Agility 2688 3697 0 2691 3697 0 Jump;Broken Fence;544 57 Agility @@ -173,9 +176,9 @@ 3713 3830 0 3715 3815 0 Climb through;Hole;31482 70 Agility 3715 3815 0 3713 3830 0 Climb through;Hole;31481 70 Agility 2946 3439 0 2943 3439 0 Climb-up;Climbing rocks;53255 66 Agility -1324 3777 0 1324 3784 0 Climb;Rocks;34397 29 Agility 4 -1324 3787 0 1324 3795 0 Climb;Rocks;34396 62 Agility 4 -1324 3784 0 1324 3777 0 Climb;Rocks;34397 29 Agility 4 +1324 3777 0 1324 3785 0 Climb;Rocks;34397 29 Agility 8 +1324 3787 0 1324 3795 0 Climb;Rocks;34396 62 Agility 8 +1324 3785 0 1324 3777 0 Climb;Rocks;34397 29 Agility 8 1324 3795 0 1324 3787 0 Climb;Rocks;34396 62 Agility 4 2400 4402 0 2400 4404 0 Squeeze-past;Jutting Wall;17002 46 Agility 4 2400 4404 0 2400 4402 0 Squeeze-past;Jutting Wall;17002 46 Agility 4 @@ -191,8 +194,8 @@ 3334 10118 0 3334 10122 0 Squeeze-through;Crevice;53259 77 Agility 4 3371 2958 0 3373 2955 0 Cross;Stepping stone;53241 71 Agility 2 3373 2955 0 3371 2958 0 Cross;Stepping stone;53241 71 Agility 2 -3240 3336 0 3240 3334 0 Jump-over;Fence;16518 13 Agility 3 -3240 3334 0 3240 3336 0 Jump-over;Fence;16518 13 Agility 3 +3240 3335 0 3240 3334 0 Jump-over;Fence;16518 13 Agility 2 +3240 3334 0 3240 3335 0 Jump-over;Fence;16518 13 Agility 2 3222 12441 0 3216 12441 0 Pass;Tight Gap;36692 78 Agility 3 3216 12441 0 3222 12441 0 Pass;Tight Gap;36693 78 Agility 3 3232 12420 0 3242 12420 0 Pass;Tight Gap;36695 84 Agility 3 @@ -284,4 +287,132 @@ 1401 3283 0 1401 3291 0 Walk-across;Log balance;56990 45 Agility 3 1477 3309 0 1477 3305 0 Climb;Rocks;56986 76 Agility 3 1477 3305 0 1477 3309 0 Climb;Rocks;56985 76 Agility 3 -3765 3883 1 3765 3898 0 Teeth-grip;Zip line;57667 76 Agility 6 \ No newline at end of file +3765 3883 1 3765 3898 0 Teeth-grip;Zip line;57667 76 Agility 6 + +# Isafdar Forest shortcuts (reviewed from Skretzo/shortest-path) +2220 3155 0 2220 3152 0 Step-over;Tripwire;3921 1 Agility 8 +2220 3152 0 2220 3155 0 Step-over;Tripwire;3921 1 Agility 8 +2215 3156 0 2215 3153 0 Step-over;Tripwire;3921 1 Agility 8 +2215 3153 0 2215 3156 0 Step-over;Tripwire;3921 1 Agility 8 +2250 3168 0 2253 3168 0 Step-over;Tripwire;3921 1 Agility 8 +2253 3168 0 2250 3168 0 Step-over;Tripwire;3921 1 Agility 8 +2284 3188 0 2287 3188 0 Step-over;Tripwire;3921 1 Agility 8 +2287 3188 0 2284 3188 0 Step-over;Tripwire;3921 1 Agility 8 +2294 3242 0 2294 3245 0 Step-over;Tripwire;3921 1 Agility 8 +2294 3245 0 2294 3242 0 Step-over;Tripwire;3921 1 Agility 8 +2199 3169 0 2202 3169 0 Pass;Sticks;3922 1 Agility 6 +2202 3169 0 2199 3169 0 Pass;Sticks;3922 1 Agility 6 +2234 3181 0 2238 3181 0 Pass;Sticks;3922 1 Agility 6 +2238 3181 0 2234 3181 0 Pass;Sticks;3922 1 Agility 6 +2181 3212 0 2181 3208 0 Pass;Sticks;3922 1 Agility 6 +2181 3208 0 2181 3212 0 Pass;Sticks;3922 1 Agility 6 +2256 3227 0 2260 3227 0 Pass;Sticks;3922 1 Agility 6 +2260 3227 0 2256 3227 0 Pass;Sticks;3922 1 Agility 6 +2274 3163 0 2277 3163 0 Pass;Sticks;3922 1 Agility 6 +2277 3163 0 2274 3163 0 Pass;Sticks;3922 1 Agility 6 +2295 3217 0 2295 3213 0 Pass;Sticks;3922 1 Agility 6 +2295 3213 0 2295 3217 0 Pass;Sticks;3922 1 Agility 6 +2209 3201 0 2209 3205 0 Jump;Leaves;3925 1 Agility 4 +2209 3205 0 2209 3201 0 Jump;Leaves;3925 1 Agility 4 +2275 3262 0 2279 3262 0 Jump;Leaves;3925 1 Agility 4 +2279 3262 0 2275 3262 0 Jump;Leaves;3925 1 Agility 4 +2267 3205 0 2267 3201 0 Jump;Leaves;3925 1 Agility 4 +2267 3201 0 2267 3205 0 Jump;Leaves;3925 1 Agility 4 +2274 3176 0 2274 3172 0 Jump;Leaves;3925 1 Agility 4 +2274 3172 0 2274 3176 0 Jump;Leaves;3925 1 Agility 4 +2202 3237 0 2196 3237 0 Cross;Log balance;3931 45 Agility 8 +2196 3237 0 2202 3237 0 Cross;Log balance;3931 45 Agility 8 +2258 3250 0 2264 3250 0 Cross;Log balance;3932 45 Agility 8 +2264 3250 0 2258 3250 0 Cross;Log balance;3932 45 Agility 8 +2290 3232 0 2290 3239 0 Cross;Log balance;3933 45 Agility 9 +2290 3239 0 2290 3232 0 Cross;Log balance;3933 45 Agility 9 +2188 3162 0 2188 3165 0 Enter;Dense forest;3999 56 Agility 4 +2188 3165 0 2188 3162 0 Enter;Dense forest;3999 56 Agility 4 +2188 3165 0 2188 3168 0 Enter;Dense forest;3939 56 Agility 4 +2188 3168 0 2188 3165 0 Enter;Dense forest;3939 56 Agility 4 +2188 3168 0 2188 3171 0 Enter;Dense forest;3998 56 Agility 4 +2188 3171 0 2188 3168 0 Enter;Dense forest;3998 56 Agility 4 +2217 3169 0 2217 3166 0 Enter;Dense forest;3939 56 Agility 4 +2217 3166 0 2217 3169 0 Enter;Dense forest;3939 56 Agility 4 +2217 3166 0 2217 3163 0 Enter;Dense forest;3938 56 Agility 4 +2217 3163 0 2217 3166 0 Enter;Dense forest;3938 56 Agility 4 +2217 3163 0 2217 3160 0 Enter;Dense forest;3939 56 Agility 4 +2217 3160 0 2217 3163 0 Enter;Dense forest;3939 56 Agility 4 +2231 3149 0 2234 3149 0 Enter;Dense forest;3937 56 Agility 4 +2234 3149 0 2231 3149 0 Enter;Dense forest;3937 56 Agility 4 +2234 3149 0 2237 3149 0 Enter;Dense forest;3938 56 Agility 4 +2237 3149 0 2234 3149 0 Enter;Dense forest;3938 56 Agility 4 +2237 3149 0 2240 3149 0 Enter;Dense forest;3939 56 Agility 4 +2240 3149 0 2237 3149 0 Enter;Dense forest;3939 56 Agility 4 +2226 3219 0 2229 3219 0 Enter;Dense forest;3938 56 Agility 4 +2229 3219 0 2226 3219 0 Enter;Dense forest;3938 56 Agility 4 +2229 3219 0 2232 3219 0 Enter;Dense forest;3939 56 Agility 4 +2232 3219 0 2229 3219 0 Enter;Dense forest;3939 56 Agility 4 +2232 3219 0 2235 3219 0 Enter;Dense forest;3937 56 Agility 4 +2235 3219 0 2232 3219 0 Enter;Dense forest;3937 56 Agility 4 +2235 3219 0 2238 3219 0 Enter;Dense forest;3939 56 Agility 4 +2238 3219 0 2235 3219 0 Enter;Dense forest;3939 56 Agility 4 +2230 3249 0 2233 3249 0 Enter;Dense forest;3938 56 Agility 4 +2233 3249 0 2230 3249 0 Enter;Dense forest;3938 56 Agility 4 +2233 3249 0 2236 3249 0 Enter;Dense forest;3939 56 Agility 4 +2236 3249 0 2233 3249 0 Enter;Dense forest;3939 56 Agility 4 +2236 3249 0 2239 3249 0 Enter;Dense forest;3937 56 Agility 4 +2239 3249 0 2236 3249 0 Enter;Dense forest;3937 56 Agility 4 +2279 3231 0 2279 3228 0 Enter;Dense forest;3939 56 Agility 4 +2279 3228 0 2279 3231 0 Enter;Dense forest;3939 56 Agility 4 +2279 3228 0 2279 3225 0 Enter;Dense forest;3937 56 Agility 4 +2279 3225 0 2279 3228 0 Enter;Dense forest;3937 56 Agility 4 +2279 3225 0 2279 3222 0 Enter;Dense forest;3938 56 Agility 4 +2279 3222 0 2279 3225 0 Enter;Dense forest;3938 56 Agility 4 +2265 3192 0 2268 3192 0 Enter;Dense forest;3938 56 Agility 4 +2268 3192 0 2265 3192 0 Enter;Dense forest;3938 56 Agility 4 +2268 3192 0 2271 3192 0 Enter;Dense forest;3939 56 Agility 4 +2271 3192 0 2268 3192 0 Enter;Dense forest;3939 56 Agility 4 +2271 3192 0 2274 3192 0 Enter;Dense forest;3937 56 Agility 4 +2274 3192 0 2271 3192 0 Enter;Dense forest;3937 56 Agility 4 +2303 3213 0 2303 3216 0 Enter;Dense forest;3937 56 Agility 4 +2303 3216 0 2303 3213 0 Enter;Dense forest;3937 56 Agility 4 +2303 3216 0 2303 3219 0 Enter;Dense forest;3938 56 Agility 4 +2303 3219 0 2303 3216 0 Enter;Dense forest;3938 56 Agility 4 +2303 3219 0 2303 3222 0 Enter;Dense forest;3939 56 Agility 4 +2303 3222 0 2303 3219 0 Enter;Dense forest;3939 56 Agility 4 +2303 3222 0 2303 3225 0 Enter;Dense forest;3938 56 Agility 4 +2303 3225 0 2303 3222 0 Enter;Dense forest;3938 56 Agility 4 + +# Brimhaven Dungeon shortcuts (reviewed from Skretzo/shortest-path) +2698 9492 0 2698 9500 0 Squeeze-through;Pipe;21727 1 Agility 13 +2698 9500 0 2698 9492 0 Squeeze-through;Pipe;21727 1 Agility 13 +2649 9562 0 2647 9557 0 Jump-from;Stepping stone;21738 1 Agility 7 +2647 9557 0 2649 9562 0 Jump-from;Stepping stone;21739 1 Agility 7 +2687 9506 0 2682 9506 0 Walk-across;Log balance;20884 1 Agility 7 +2682 9506 0 2687 9506 0 Walk-across;Log balance;20882 1 Agility 7 + +# Lumbridge cellar shortcut (reviewed from Skretzo/shortest-path) +3219 9618 0 3221 9618 0 Squeeze-through;Hole;6905 13 Agility 532>3 3 +3221 9618 0 3219 9618 0 Squeeze-through;Hole;6905 13 Agility 532>3 3 + +# Karamja rock shortcut (reviewed from Skretzo/shortest-path) +2795 2978 0 2791 2978 0 Climb;Rocks;2231 15 Agility 5 +2795 2979 0 2791 2979 0 Climb;Rocks;2231 15 Agility 5 +2795 2980 0 2791 2980 0 Climb;Rocks;2231 15 Agility 5 +2791 2978 0 2795 2978 0 Climb;Rocks;2231 15 Agility 5 +2791 2979 0 2795 2979 0 Climb;Rocks;2231 15 Agility 5 +2791 2980 0 2795 2980 0 Climb;Rocks;2231 15 Agility 5 + +# Slayer Tower ground-floor shortcut (reviewed from Skretzo/shortest-path) +3421 3550 0 3421 3550 1 Climb-up;Spikey chain;16537 61 Agility +3422 3551 0 3422 3551 1 Climb-up;Spikey chain;16537 61 Agility +3423 3550 0 3423 3550 1 Climb-up;Spikey chain;16537 61 Agility +3422 3549 0 3422 3549 1 Climb-up;Spikey chain;16537 61 Agility +3421 3550 1 3421 3550 0 Climb-down;Spikey chain;16538 61 Agility +3422 3551 1 3422 3551 0 Climb-down;Spikey chain;16538 61 Agility +3422 3549 1 3422 3549 0 Climb-down;Spikey chain;16538 61 Agility +3423 3550 1 3423 3550 0 Climb-down;Spikey chain;16538 61 Agility + +# Darkmeyer wall shortcuts (reviewed from Skretzo/shortest-path) +3667 3375 0 3670 3375 0 Climb;Wall;39542 63 Agility 10449=1 +3670 3375 0 3667 3375 0 Climb;Wall;39542 63 Agility 10449=1 +3670 3375 0 3673 3375 0 Climb;Wall;39541 63 Agility 10450=1 +3673 3375 0 3670 3375 0 Climb;Wall;39541 63 Agility 10450=1 +3672 3376 0 3670 3375 0 Climb;Wall;39541 63 Agility 10450=1 +3672 3374 0 3670 3375 0 Climb;Wall;39541 63 Agility 10450=1 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/blocked_edges.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/blocked_edges.tsv index 9ca08f8dde6..45fae801055 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/blocked_edges.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/blocked_edges.tsv @@ -30,6 +30,9 @@ 2935 3450 0 2935 3449 0 true Taverley wall 2937 3450 0 2937 3449 0 true Taverley wall 2938 3450 0 2938 3449 0 true Taverley wall +# Elemental Workshop odd-looking wall. The collision artifact exposes this cardinal edge; the +# item-gated object transport in transports.tsv is the only valid crossing. +2709 3495 0 2709 3496 0 true Elemental Workshop odd-looking wall 2649 3469 0 2649 3470 0 true Hemenster enclosure north gate - never opens 2650 3469 0 2650 3470 0 true Hemenster enclosure north gate - never opens # Al Kharid mine - block routing through open pit (collision data gap) diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/canoes.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/canoes.tsv index 07a1c276708..3ae62528631 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/canoes.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/canoes.tsv @@ -1,35 +1,67 @@ -# Origin Destination menuOption menuTarget objectID Skills Item IDs Quest Duration Display info -# Edgeville -3132 3510 0 3109 3415 0 Paddle Canoe;Canoe Station;12166 12 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Barbarian Village -3132 3510 0 3199 3344 0 Paddle Canoe;Canoe Station;12166 27 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Champions Guild -3132 3510 0 3240 3242 0 Paddle Canoe;Canoe Station;12166 42 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Lumbridge -3132 3510 0 3154 3638 0 Paddle Canoe;Canoe Station;12166 57 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Ferox Enclave -3132 3510 0 3141 3796 0 Paddle Canoe;Canoe Station;12166 57 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Wilderness Pond - -# Barbarian Village -3112 3411 0 3199 3344 0 Paddle Canoe;Canoe Station;12165 12 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Champions Guild -3112 3411 0 3128 3503 0 Paddle Canoe;Canoe Station;12165 12 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Edgeville -3112 3411 0 3240 3242 0 Paddle Canoe;Canoe Station;12165 27 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Lumbridge -3112 3411 0 3154 3638 0 Paddle Canoe;Canoe Station;12165 57 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Ferox Enclave -3112 3411 0 3141 3796 0 Paddle Canoe;Canoe Station;12165 57 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Wilderness Pond - -# Champions' Guild -3202 3343 0 3240 3242 0 Paddle Canoe;Canoe Station;12164 12 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Lumbridge -3202 3343 0 3109 3415 0 Paddle Canoe;Canoe Station;12164 12 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Barbarian Village -3202 3343 0 3128 3503 0 Paddle Canoe;Canoe Station;12164 27 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Edgeville -3202 3343 0 3154 3638 0 Paddle Canoe;Canoe Station;12164 57 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Ferox Enclave -3202 3343 0 3141 3796 0 Paddle Canoe;Canoe Station;12164 57 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Wilderness Pond - -# Lumbridge -3243 3237 0 3199 3344 0 Paddle Canoe;Canoe Station;12163 12 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Champions Guild -3243 3237 0 3109 3415 0 Paddle Canoe;Canoe Station;12163 27 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Barbarian Village -3243 3237 0 3128 3503 0 Paddle Canoe;Canoe Station;12163 42 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Edgeville -3243 3237 0 3154 3638 0 Paddle Canoe;Canoe Station;12163 57 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Ferox Enclave -3243 3237 0 3141 3796 0 Paddle Canoe;Canoe Station;12163 57 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Wilderness Pond - -# Ferox Enclave -3154 3630 0 3128 3503 0 Paddle Canoe;Canoe Station;39638 12 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Edgeville -3154 3630 0 3109 3415 0 Paddle Canoe;Canoe Station;39638 27 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Barbarian Village -3154 3630 0 3199 3344 0 Paddle Canoe;Canoe Station;39638 42 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Champions Guild -3154 3630 0 3240 3242 0 Paddle Canoe;Canoe Station;39638 57 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Lumbridge -3154 3630 0 3141 3796 0 Paddle Canoe;Canoe Station;39638 57 Woodcutting 1351;1349;1361;1353;1355;1357;1359 12 Wilderness Pond +# Origin Destination menuOption menuTarget objectID Skills Items Duration Display info +# River Lum chain +# Edgeville +3132 3510 0 3109 3415 0 Paddle Canoe;Canoe Station;12166 12 Woodcutting AXE=1 30 Barbarian Village +3132 3510 0 3199 3344 0 Paddle Canoe;Canoe Station;12166 27 Woodcutting AXE=1 30 Champions Guild +3132 3510 0 3240 3242 0 Paddle Canoe;Canoe Station;12166 42 Woodcutting AXE=1 30 Lumbridge +3132 3510 0 3154 3638 0 Paddle Canoe;Canoe Station;12166 57 Woodcutting AXE=1 20 Ferox Enclave +3132 3510 0 3141 3796 0 Paddle Canoe;Canoe Station;12166 57 Woodcutting AXE=1 20 Wilderness Pond + +# Barbarian Village +3112 3411 0 3199 3344 0 Paddle Canoe;Canoe Station;12165 12 Woodcutting AXE=1 30 Champions Guild +3112 3411 0 3128 3503 0 Paddle Canoe;Canoe Station;12165 12 Woodcutting AXE=1 30 Edgeville +3112 3411 0 3240 3242 0 Paddle Canoe;Canoe Station;12165 27 Woodcutting AXE=1 30 Lumbridge +3112 3411 0 3154 3638 0 Paddle Canoe;Canoe Station;12165 57 Woodcutting AXE=1 20 Ferox Enclave +3112 3411 0 3141 3796 0 Paddle Canoe;Canoe Station;12165 57 Woodcutting AXE=1 20 Wilderness Pond + +# Champions' Guild +3202 3343 0 3240 3242 0 Paddle Canoe;Canoe Station;12164 12 Woodcutting AXE=1 30 Lumbridge +3202 3343 0 3109 3415 0 Paddle Canoe;Canoe Station;12164 12 Woodcutting AXE=1 30 Barbarian Village +3202 3343 0 3128 3503 0 Paddle Canoe;Canoe Station;12164 27 Woodcutting AXE=1 30 Edgeville +3202 3343 0 3154 3638 0 Paddle Canoe;Canoe Station;12164 57 Woodcutting AXE=1 20 Ferox Enclave +3202 3343 0 3141 3796 0 Paddle Canoe;Canoe Station;12164 57 Woodcutting AXE=1 20 Wilderness Pond + +# Lumbridge +3243 3237 0 3199 3344 0 Paddle Canoe;Canoe Station;12163 12 Woodcutting AXE=1 30 Champions Guild +3243 3237 0 3109 3415 0 Paddle Canoe;Canoe Station;12163 27 Woodcutting AXE=1 30 Barbarian Village +3243 3237 0 3128 3503 0 Paddle Canoe;Canoe Station;12163 42 Woodcutting AXE=1 30 Edgeville +3243 3237 0 3154 3638 0 Paddle Canoe;Canoe Station;12163 57 Woodcutting AXE=1 20 Ferox Enclave +3243 3237 0 3141 3796 0 Paddle Canoe;Canoe Station;12163 57 Woodcutting AXE=1 20 Wilderness Pond + +# Ferox Enclave +3154 3630 0 3128 3503 0 Paddle Canoe;Canoe Station;39638 12 Woodcutting AXE=1 20 Edgeville +3154 3630 0 3109 3415 0 Paddle Canoe;Canoe Station;39638 27 Woodcutting AXE=1 20 Barbarian Village +3154 3630 0 3199 3344 0 Paddle Canoe;Canoe Station;39638 42 Woodcutting AXE=1 20 Champions Guild +3154 3630 0 3240 3242 0 Paddle Canoe;Canoe Station;39638 57 Woodcutting AXE=1 20 Lumbridge +3154 3630 0 3141 3796 0 Paddle Canoe;Canoe Station;39638 57 Woodcutting AXE=1 20 Wilderness Pond + +# River Dougne chain +# Castle Wars +2439 3135 0 2483 3188 0 Paddle Canoe;Canoe Station;60845 12 Woodcutting AXE=1 30 Tree Gnome Village +2439 3135 0 2577 3261 0 Paddle Canoe;Canoe Station;60845 27 Woodcutting AXE=1 30 The Clock Tower +2439 3135 0 2571 3360 0 Paddle Canoe;Canoe Station;60845 42 Woodcutting AXE=1 30 Chaos Druid Tower +2439 3135 0 2523 3408 0 Paddle Canoe;Canoe Station;60845 57 Woodcutting AXE=1 30 Tree Gnome Stronghold + +# Tree Gnome Village +2485 3192 0 2436 3134 0 Paddle Canoe;Canoe Station;60846 12 Woodcutting AXE=1 30 Castle Wars +2485 3192 0 2577 3261 0 Paddle Canoe;Canoe Station;60846 12 Woodcutting AXE=1 30 The Clock Tower +2485 3192 0 2571 3360 0 Paddle Canoe;Canoe Station;60846 27 Woodcutting AXE=1 30 Chaos Druid Tower +2485 3192 0 2523 3408 0 Paddle Canoe;Canoe Station;60846 42 Woodcutting AXE=1 30 Tree Gnome Stronghold + +# The Clock Tower +2579 3260 0 2436 3134 0 Paddle Canoe;Canoe Station;60847 27 Woodcutting AXE=1 30 Castle Wars +2579 3260 0 2483 3188 0 Paddle Canoe;Canoe Station;60847 12 Woodcutting AXE=1 30 Tree Gnome Village +2579 3260 0 2571 3360 0 Paddle Canoe;Canoe Station;60847 12 Woodcutting AXE=1 30 Chaos Druid Tower +2579 3260 0 2523 3408 0 Paddle Canoe;Canoe Station;60847 27 Woodcutting AXE=1 30 Tree Gnome Stronghold + +# Chaos Druid Tower +2573 3358 0 2436 3134 0 Paddle Canoe;Canoe Station;60848 42 Woodcutting AXE=1 30 Castle Wars +2573 3358 0 2483 3188 0 Paddle Canoe;Canoe Station;60848 27 Woodcutting AXE=1 30 Tree Gnome Village +2573 3358 0 2577 3261 0 Paddle Canoe;Canoe Station;60848 12 Woodcutting AXE=1 30 The Clock Tower +2573 3358 0 2523 3408 0 Paddle Canoe;Canoe Station;60848 12 Woodcutting AXE=1 30 Tree Gnome Stronghold + +# Tree Gnome Stronghold +2525 3408 0 2436 3134 0 Paddle Canoe;Canoe Station;60849 57 Woodcutting AXE=1 30 Castle Wars +2525 3408 0 2483 3188 0 Paddle Canoe;Canoe Station;60849 42 Woodcutting AXE=1 30 Tree Gnome Village +2525 3408 0 2577 3261 0 Paddle Canoe;Canoe Station;60849 27 Woodcutting AXE=1 30 The Clock Tower +2525 3408 0 2571 3360 0 Paddle Canoe;Canoe Station;60849 12 Woodcutting AXE=1 30 Chaos Druid Tower diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/collision-map.zip b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/collision-map.zip index 43d1d412423c5915bbe879954a4bfded9a0d43e8..fbbfbcc5d2b3cdb7ad66cf12c7fc1cc93c51670c 100644 GIT binary patch literal 1200360 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°yf?)% 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 gIHgiFil@lml{f__?0FLhjjOp{^2&H%+ItR(ln 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 z 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%0D<&Lo-OA<9JkFAIfU9L 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 hrJ72A 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

*H4p1 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