Summary
Rs2Walker.walkWithStateInternal returns ARRIVED whenever the target tile is unwalkable and the player is within distance of it — without checking whether the player can actually interact from their current tile. When a solid wall sits between player and target, this produces an infinite loop in QuestScript (and any caller that re-issues walkTo after a failed interaction).
Reproduction
Observed in ironman-guide worktree, multiple QuestScript-N threads running concurrently:
- Player at
(3305,3501,0) (Falador / Port Sarim area).
- Quest target at
(3305,3500,0) — unwalkable tile (counter / NPC stance / similar).
- Solid wall directly between the two tiles. There is no door — the actual route is around the wall.
- Player has no line of sight to the target object/NPC from their current tile.
Result: 5+ concurrent quest scripts hot-loop indefinitely, each burning ~1s of CPU per tick. The player never moves.
Trace from the logs
[Pathfinder] run() started: src=(3305,3501,0), dst=(3305,3500,0), cutoff=3000ms
[Pathfinder] Loop exited. cancelled=false, boundaryEmpty=true, pendingEmpty=true, bestLastNode=(3305,3501,0)
[Pathfinder] run() completed. done=true, cancelled=false, stats=PathfinderStats(nodes=1037532,transports=1454,time=359ms)
[Walker] ARRIVED early. arrivalCheck=0ms, distToTarget=1
Sequence per quest tick (per script):
QuestScript.applyObjectStep calls Rs2Walker.canReach(target) (line 1311 / 1371).
canReach runs PathfinderConfig.refresh() (~560ms) → new Pathfinder(...).run() synchronously (~360ms, exhausts the world because target is unreachable on the static collision map) → another refresh() in finally (~560ms). ~1.5s per call.
canReach returns based on path-endpoint-vs-target proximity intersection — true here because endpoint = src and target is 1 tile away.
- Quest helper falls to
Rs2Walker.walkTo(target, 1).
walkWithStateInternal arrival check:
walkableCheck = Rs2Tile.isWalkable(localTarget) → false (target tile has BLOCK_MOVEMENT_FULL).
distToTarget=1, distance=1.
- Branch
(!walkableCheck && distToTarget <= distance) → true → returns ARRIVED.
- Quest helper attempts to interact with the object/NPC at the target → fails because LOS is blocked by the wall.
- Quest helper iterates next tick, repeat from step 1.
Multiple quest scripts hitting this at the same time (QuestScript-2, -3, -7, -8, -10) compound the cost since they share ShortestPathPlugin.pathfinder and each setTarget cancels/restarts it.
Root cause
Rs2Walker.walkWithStateInternal (Rs2Walker.java:139):
if (reachableTileCheck || (!walkableCheck && distToTarget <= distance)) {
log.info("[Walker] ARRIVED early. arrivalCheck={}ms, distToTarget={}", arrivalCheckMs, distToTarget);
return WalkerState.ARRIVED;
}
The (!walkableCheck && distToTarget <= distance) branch assumes "target unwalkable + within proximity = arrived". That's correct for an NPC standing in an open room, but wrong when a wall separates the player from the target. There is no check that the player has a viable interaction angle (line of sight, or being on the correct side of the obstacle).
Proposed fix
When the target tile is unwalkable and distToTarget ≤ distance, additionally verify that the player can plausibly interact. Cheapest test: line-of-sight from player tile to target. If LOS is blocked, search nearby walkable tiles around the target for one that has LOS, walk to that tile instead of returning ARRIVED.
Sketch:
if (!walkableCheck && distToTarget <= distance) {
if (Rs2Player.hasLineOfSight(target)) {
return WalkerState.ARRIVED; // genuinely close enough
}
WorldPoint losTile = Rs2Tile.getWalkableTilesAroundTile(target, 3).stream()
.filter(t -> Rs2Tile.hasLineOfSightToTile(t, target))
.min(Comparator.comparingInt(t -> t.distanceTo(Rs2Player.getWorldLocation())))
.orElse(null);
if (losTile != null && !losTile.equals(Rs2Player.getWorldLocation())) {
// walk to losTile, then re-check
}
// else: genuinely can't reach with LOS; consider returning UNREACHABLE
// so the caller stops looping
}
Secondary issue: canReach is wasteful
Independent of the LOS bug, Rs2Walker.canReach(WorldPoint) (Rs2Walker.java:721) does refresh + new Pathfinder.run() (synchronous, full BFS) + refresh per call — ~1.5s including a world-exhausting BFS when the target is unreachable. Quest helper calls this every tick, often from multiple concurrent scripts. Even after the LOS fix, this is costly.
Possible improvements:
- Cache the result for a few hundred ms keyed on
(playerTile, target).
- Skip the leading/trailing
refresh() when called repeatedly within a refresh window.
- Fall back to
Rs2Tile-based reachability for short distances before invoking the full pathfinder.
Workaround in quest helper
Until the walker fix lands, QuestScript.applyObjectStep could detect "called walkTo and got ARRIVED but interaction still fails twice in a row" and re-target a tile with LOS via Rs2Tile.getWalkableTilesAroundTile(target, n) filtered by LOS.
Notes
Summary
Rs2Walker.walkWithStateInternalreturnsARRIVEDwhenever the target tile is unwalkable and the player is withindistanceof it — without checking whether the player can actually interact from their current tile. When a solid wall sits between player and target, this produces an infinite loop in QuestScript (and any caller that re-issueswalkToafter a failed interaction).Reproduction
Observed in
ironman-guideworktree, multipleQuestScript-Nthreads running concurrently:(3305,3501,0)(Falador / Port Sarim area).(3305,3500,0)— unwalkable tile (counter / NPC stance / similar).Result: 5+ concurrent quest scripts hot-loop indefinitely, each burning ~1s of CPU per tick. The player never moves.
Trace from the logs
Sequence per quest tick (per script):
QuestScript.applyObjectStepcallsRs2Walker.canReach(target)(line 1311 / 1371).canReachrunsPathfinderConfig.refresh()(~560ms) →new Pathfinder(...).run()synchronously (~360ms, exhausts the world because target is unreachable on the static collision map) → anotherrefresh()infinally(~560ms). ~1.5s per call.canReachreturns based on path-endpoint-vs-target proximity intersection — true here because endpoint = src and target is 1 tile away.Rs2Walker.walkTo(target, 1).walkWithStateInternalarrival check:walkableCheck = Rs2Tile.isWalkable(localTarget)→ false (target tile hasBLOCK_MOVEMENT_FULL).distToTarget=1,distance=1.(!walkableCheck && distToTarget <= distance)→ true → returnsARRIVED.Multiple quest scripts hitting this at the same time (
QuestScript-2, -3, -7, -8, -10) compound the cost since they shareShortestPathPlugin.pathfinderand eachsetTargetcancels/restarts it.Root cause
Rs2Walker.walkWithStateInternal(Rs2Walker.java:139):The
(!walkableCheck && distToTarget <= distance)branch assumes "target unwalkable + within proximity = arrived". That's correct for an NPC standing in an open room, but wrong when a wall separates the player from the target. There is no check that the player has a viable interaction angle (line of sight, or being on the correct side of the obstacle).Proposed fix
When the target tile is unwalkable and
distToTarget ≤ distance, additionally verify that the player can plausibly interact. Cheapest test: line-of-sight from player tile to target. If LOS is blocked, search nearby walkable tiles around the target for one that has LOS, walk to that tile instead of returning ARRIVED.Sketch:
Secondary issue: canReach is wasteful
Independent of the LOS bug,
Rs2Walker.canReach(WorldPoint)(Rs2Walker.java:721) doesrefresh + new Pathfinder.run() (synchronous, full BFS) + refreshper call — ~1.5s including a world-exhausting BFS when the target is unreachable. Quest helper calls this every tick, often from multiple concurrent scripts. Even after the LOS fix, this is costly.Possible improvements:
(playerTile, target).refresh()when called repeatedly within a refresh window.Rs2Tile-based reachability for short distances before invoking the full pathfinder.Workaround in quest helper
Until the walker fix lands,
QuestScript.applyObjectStepcould detect "called walkTo and got ARRIVED but interaction still fails twice in a row" and re-target a tile with LOS viaRs2Tile.getWalkableTilesAroundTile(target, n)filtered by LOS.Notes
rs2walker-fixPR (fix(rs2walker): unblock long indoor paths — filtered-edge guard, exhaustion bail, scene doors chsami/Microbot#1756) — that branch addresses BFS exhaustion, scene-discovered doors, filtered transport edges, off-path door opens, and door pre-scan. None of those code paths touch the "unwalkable target + LOS-blocked" arrival case.ironman-guideworktree at241922bbfb, no rs2walker-fix patches loaded.