Skip to content

Commit e47447d

Browse files
authored
Merge pull request #1826 from chsami/development
Release Microbot 2.6.16
2 parents 4136e9e + d42f6b3 commit e47447d

75 files changed

Lines changed: 12066 additions & 1545 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/entity-guides/items.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,31 @@ Do not make call sites compensate by assuming `index == 0` when the action array
152152
**Where this applies:** `Rs2Reflection.getGroundItemActions`, `Rs2GroundItem.interact`, `Rs2TileItemModel.click`, and any future ground-item interaction helper that derives a `MenuAction` from item actions.
153153

154154
**Defensive check:** Live smoke with ExampleScript's "Drop and loot item" check after any RuneLite or injected-client version bump.
155+
156+
---
157+
158+
## 9. Dispatch ground-item actions through the synthetic target menu
159+
160+
Resolve the ground item's action and `MenuAction` first, then dispatch it with `Microbot.doInvoke(NewMenuEntry, bounds)`. Do not call `Rs2Reflection.invokeMenu` directly for ground-item interactions.
161+
162+
**Why this matters:** A raw canvas click can select a door or NPC that visually overlaps the ground item's tile. The reflected client-menu call can loot successfully but still emit `Unable to find clicked menu op` engine messages because it bypasses the normal clicked-menu correlation. `Microbot.doInvoke` sets `Microbot.targetMenu` before clicking, allowing `MicrobotPlugin` to replace the generated scene menu with the intended ground-item entry regardless of what is under the cursor.
163+
164+
**Pattern to follow:**
165+
166+
```java
167+
Microbot.doInvoke(new NewMenuEntry()
168+
.option(action)
169+
.target(target)
170+
.identifier(itemId)
171+
.opcode(menuAction.getId())
172+
.param0(sceneX)
173+
.param1(sceneY)
174+
.itemId(-1)
175+
.worldViewId(worldViewId), bounds);
176+
```
177+
178+
Keep action discovery and dispatch separate: `Rs2Reflection.getGroundItemActions` retains the third-slot `Take` fallback described above, while `Microbot.doInvoke` owns the interaction.
179+
180+
**Where this applies:** `Rs2GroundItem.interact`, `Rs2TileItemModel.click`, and future ground-item interaction helpers.
181+
182+
**Defensive check:** Drop loot on a tile visually overlapped by an NPC and beside an openable door. Verify the intended item is taken from multiple camera angles and no `Unable to find clicked menu op` engine message appears.

docs/walker-audit.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Walker Audit & Roadmap
2+
3+
_Audit date: 2026-07-25. Scope: `shortestpath/` (pathfinding) and `util/walker/` (runtime execution)._
4+
5+
## Verdict
6+
7+
**Path *generation* is healthy. The runtime *executor* is the problem, and collision-map
8+
fidelity is the root trigger.** The common failure mode (player stuck far from the goal,
9+
unable to route back, eventually idle-logged-out) is a runtime-recovery pathology, not a
10+
pathfinding one.
11+
12+
## Healthy — leave alone
13+
14+
- **`Pathfinder` (809 lines)** — bidirectional A*, admissible network-landmark heuristics
15+
(fairy rings / spirit trees / gliders / quetzals folded in as landmarks),
16+
underground-aware distance, per-node random tiebreaker to kill the "identical route every
17+
trip" fingerprint. Mature and well-commented.
18+
- **`PathSmoother` (118)** — LOS run-collapsing with correct invariants (transport anchors
19+
and collision walls preserved; segment cap keeps `isNearPath` intersecting long corridors).
20+
- **`CollisionMap.canStep` (451)** — correct N/E edge model; the live-overlay pin
21+
(`beginSearch` / `pinnedLive`) prevents mixing two scenes into one path.
22+
- **Live-collision store (Phase 2)** — already built, disk-backed, self-filling, 22 tests
23+
green. Gated behind `useLiveCollision`, which **defaults OFF**.
24+
25+
## Problems (by severity)
26+
27+
1. **`Rs2Walker` is a ~12,000-line, 336-method god-class.** ~40+ methods are door/recovery
28+
heuristics accreted over time (`handleDoors`×2, `handleDoorsWithTimeout`×3,
29+
`tryResolveNearbyDoorBlocker`, `tryResolveDoorBlockerLineOfSight`,
30+
`tryResolvePathAdjacentBlocker`, `handleDoorsInRawSegment`, `handleRockfallInRawSegment`,
31+
`findReachableRejoinRawPathPoint`, `recentlyOpenedStationaryDoorOnSegment`, …).
32+
Unmaintainable; this is where stalls/loops live.
33+
2. **Recovery does expensive per-tick scene scans.** Field log:
34+
`slow raw scene scan: doorProbe=1265ms doorWait=1564ms`~3s stalls, repeated, plus
35+
`cancel:processWalk:after-stuck-check`. Overlapping "stuck" recovery paths can cancel each
36+
other and thrash instead of converging.
37+
3. **Collision fidelity is the *trigger*.** The pathfinder plans on the static map, which
38+
(a) mis-derives some edges (dumper heuristics ≠ runtime `CollisionData`) and (b) cannot
39+
model dynamic obstacles (doors, rockfalls). So the plan routes through tiles the live
40+
scene blocks → every discrepancy invokes the recovery machinery in #1/#2.
41+
4. **Decomposition started but ~90% unfinished.** `util/walker/{door,stall,transport,lifecycle}/`
42+
packages exist with a few classes; the monolith still holds the bulk.
43+
44+
## Root-cause chain
45+
46+
static-map edge/obstacle gaps → plan crosses a live-blocked tile → player stalls → 12k-line
47+
recovery pile does ~3s scene scans, sometimes loops/cancels → no progress → caller gives up →
48+
idle logout.
49+
50+
## Roadmap
51+
52+
### P0 — Stabilize (low risk, high payoff)
53+
- **Turn on + harden the live-collision store.** It's built and tested; it directly attacks
54+
root cause #3 (learns real blocked edges as the bot travels, overriding bad static). Add a
55+
**code-version stamp** to the disk store so stale pre-fix captures auto-invalidate (removes
56+
the manual "Reset learned collision" dependency). Verify self-heal + persistence in-client,
57+
then decide whether `useLiveCollision` flips default-ON.
58+
- **Bound recovery cost.** Cache door-probe results per segment per tick and cap the
59+
door-scan budget so one blocked edge can't cause repeated 3s stalls or the cancel-loop.
60+
61+
### P1 — Decompose `Rs2Walker` (maintainability)
62+
Extract into the already-scaffolded packages, leaving `Rs2Walker` a thin facade:
63+
- `WalkExecutor` — the `processWalk` loop + clicking.
64+
- `RouteRecovery` — rejoin / off-path / blocker logic.
65+
- `DoorService` — consolidate the ~20 door methods into `util/walker/door/`.
66+
- `ObstacleService` — rockfall + future dynamic obstacles, handled uniformly.
67+
68+
### P2 — Unify the recovery model (the real fix)
69+
Replace the pile of special cases with one principle: **when the live scene contradicts a
70+
planned edge, write that edge to the live store and recalc.** Doors/rockfalls become
71+
uniformly-handled obstacle transports, not bespoke handlers — this lets you *delete*
72+
thousands of lines rather than reorganize them.
73+
74+
### P3 — Observability & safety net
75+
`WebWalkLog` is already rich; add a compact per-tick decision trace and lean on the existing
76+
walker test harness so P1/P2 refactors are regression-checked.
77+
78+
## Sequencing
79+
80+
P0 is independent and gives the biggest immediate relief for the stuck-far-away symptom — do
81+
it first. P1 unblocks P2. (AIOHunting's v1.9.3 travel-retry is the correct band-aid until P0
82+
lands.)

docs/walker-p2-unification.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Walker P2 — Unify the Obstacle Model
2+
3+
_Plan date: 2026-07-26. Depends on P1 (docs/walker-audit.md): `state/WalkerRouteState`,
4+
`recovery/RouteRecovery` + harness, `geometry/WalkerPathGeometry`, and the pre-existing
5+
`door/` + `obstacle/` packages are in place._
6+
7+
## Status / outcome (2026-07-26)
8+
9+
The plan below was executed **partially and deliberately** — the parts that pay off were taken, the parts
10+
that would trade a working system for architectural tidiness were **not**. What shipped:
11+
12+
-**Abstractions + registry** (`obstacle/`: `PlannedEdge`, `ObstacleResolution`, `ObstacleResolver`,
13+
`ObstacleRegistry`, `LiveScene`, `WalkerActions`) — headless-tested.
14+
-**`MineableResolver` + `TransportResolver`** — decision logic pure, headless-tested.
15+
-**Dispatch cutover in the recovery block** (`Rs2Walker.resolveRecoveryObstacle`) — one
16+
`ObstacleResolution` switch replacing the inline rockfall mine + the stepping-stone override.
17+
**Live-verified** (user walked door route + MLM rockfall + River Lum stones, "works fine").
18+
-**Rockfall fully migrated + legacy deleted** — all three rockfall sites route through
19+
`MineableResolver` (`resolveRockfallOnSegment`/`resolveRockfallOnEdge`); `applyRockfall` and
20+
`handleRockfallInRawSegment` are gone. The complete adapter→cutover→delete lifecycle on one obstacle type.
21+
-**Door decision cores harnessed** (`Rs2DoorClassifierTest`, `Rs2DoorGeometryTest`) — the "tests first"
22+
net, without touching the cascade.
23+
24+
**Deliberately NOT done (and why):** the **door cascade** and the **transport interaction** (`handleTransports`,
25+
charter ships / stairs / precomputed continuations) were left on their existing paths. Unlike rockfall (an
26+
isolated, per-edge obstacle that fit the model cleanly), these are **stateful, order-dependent cascades**
27+
whose complexity is largely *essential* (real scenario diversity), not the accidental sprawl this plan
28+
assumed. Forcing them into per-edge `resolve()` would **reorder** working recovery logic for limited gain —
29+
a poor risk/reward on the walker's subtlest, working subsystem. The `door/` package is already well-
30+
decomposed and now tested at the decision level, which is the right end state for it. Revisit only if a
31+
specific symptom demands it — targeted, harness-pinned — not as a wholesale rewrite. `Rs2LiveScene` (the
32+
live read adapter) is retained for a future per-edge dispatch model but is currently unused in production
33+
(the shipped dispatch never needed the scene view).
34+
35+
The plan as originally written follows, for context.
36+
37+
## Problem being solved
38+
39+
The walker plans on a static/overlay collision map, then discovers at runtime that a planned edge is
40+
blocked or gated by something the map can't model: a closed **door**/gate, a **rockfall**/rockslide, an
41+
agility **shortcut** (stepping stone, grapple, pipe), or a **transport** (stairs, ladder, cave, teleport,
42+
boat, fairy ring). Today each of these has its **own bespoke handler and its own recovery path**:
43+
44+
- doors: `handleDoors`×2, `handleDoorsInRawSegment`, `handleUnresolvedDoorNearRawPath`,
45+
`tryResolvePathAdjacentBlocker`, `tryResolveNearbyDoorBlocker`, the whole `door/` package, plus
46+
door-attempt bookkeeping (`lastDoorAttempt*`, `nextDoorInteractionAllowedAtMs`, `rawScanFocusedDoor*`).
47+
- rockfalls: `obstacle/Rs2ObstacleHandler` (already returns a `RockfallResult`).
48+
- shortcuts/transports: the ~760-line `handleTransports`, `handleTransportsInRawSegment`,
49+
`finishHandledTransport`, `RouteRecovery.findReachableTransportOriginAhead` (the stepping-stone fix).
50+
51+
They are gated differently, fire at different points in `processWalk`, and interact — which is why a fix
52+
in one (rockfall passable → smoother walks through → recovery clicks the far bank) breaks another. That is
53+
the whack-a-mole. **P2 collapses all of it into one path so a fix lands in one place, and thousands of
54+
lines of special-casing get deleted rather than reorganised.**
55+
56+
## The one principle
57+
58+
> When the live scene contradicts a planned edge, identify what blocks it and resolve it uniformly:
59+
> walk to the interaction tile if needed, perform the one right interaction, wait for it to clear, then
60+
> continue the route. If nothing can resolve it, record the real blocked edge and recalculate.
61+
62+
Every door/rockfall/shortcut/transport is an instance of this. There is exactly one dispatch.
63+
64+
## Core abstractions (new, in `util/walker/obstacle/`)
65+
66+
```
67+
PlannedEdge { WorldPoint from, to; boolean adjacent; } // the route step in question
68+
LiveScene // read-only injected view: reachable set, transports map, tile objects/actions,
69+
// player tile. NO Rs2* statics inside resolvers -> harness-testable.
70+
ObstacleResolution enum { CROSSED, INTERACTED, WALK_TO_ORIGIN(WorldPoint), WAITING, ABORT(reason),
71+
NOT_APPLICABLE }
72+
ObstacleResolver interface {
73+
boolean handles(PlannedEdge edge, LiveScene scene); // pure, cheap classification
74+
ObstacleResolution resolve(PlannedEdge edge, LiveScene scene, WalkerActions io); // may act
75+
}
76+
ObstacleRegistry // ordered list of resolvers; first that handles() wins.
77+
```
78+
79+
`WalkerActions` is the thin imperative shell (interact-with-object, click-tile, sleepUntil) — the only
80+
part that touches the live client. Resolvers keep their **decision** logic pure (classification +
81+
what-to-do), so each is exercised headlessly by the harness exactly like `RouteRecoveryTest` does now.
82+
83+
## Resolvers (each replaces a pile)
84+
85+
| Resolver | handles() when the edge is blocked by… | resolve() |
86+
|----------|----------------------------------------|-----------|
87+
| `DoorResolver` | an openable door/gate on the edge (uses `door/Rs2DoorDetection`/`Rs2DoorClassifier`) | open it, wait for the edge to open |
88+
| `MineableResolver` | a rockfall/rockslide on/adjacent (folds in `Rs2ObstacleHandler`) | mine it, wait for it to clear |
89+
| `TransportResolver` | the edge is a transport/shortcut origin (stepping stone, ladder, cave, teleport) | `WALK_TO_ORIGIN` if not standing on it (the stepping-stone fix), else take it |
90+
91+
Adding a new dynamic obstacle later = one new resolver, registered — never another branch in `processWalk`.
92+
93+
## Resolution flow (replaces the cascade)
94+
95+
`processWalk`'s "stuck at unreachable tile" recovery and the post-transport segment handlers both become:
96+
97+
```
98+
PlannedEdge edge = nextBlockedPlannedEdge(path, player, scene); // the frontier we can't cross
99+
if (edge != null) {
100+
switch (registry.resolve(edge, scene, io)) {
101+
case WALK_TO_ORIGIN(t): clickToward(t); return; // e.g. step onto the stone
102+
case INTERACTED / WAITING: return; // door opening / rock mining
103+
case CROSSED: continue;
104+
case ABORT(r): liveStore.markBlocked(edge); recalculate(); return; // learn + replan
105+
case NOT_APPLICABLE: fall through to the plain minimap-recovery click.
106+
}
107+
}
108+
```
109+
110+
One place. The ~40 handlers become 3 resolvers behind `resolve()`.
111+
112+
## Migration — strangler, ordered (each step compile+harness green, live-test the behavior ones)
113+
114+
1. **Define the abstractions** above + a `LiveScene`/`WalkerActions` adapter over the current
115+
`Rs2Player`/`Rs2Tile`/`Rs2GameObject`/`Rs2PathApi` calls. No behavior change.
116+
2. **Adapter resolvers first (no rewrite):** wrap the *existing* `Rs2DoorHandler`, `Rs2ObstacleHandler`,
117+
and `handleTransports` logic as `DoorResolver`/`MineableResolver`/`TransportResolver` that delegate to
118+
today's code. Register them. Still no behavior change — just reachable through one interface.
119+
3. **Cut over the dispatch:** replace the recovery/segment-handler cascade in `processWalk` with the
120+
single `registry.resolve(edge)` call above. This is the first behavior-affecting step → **live-test**
121+
(door route, MLM rockfall, stepping stone). Because the resolvers still wrap the old code, behavior
122+
should be identical; the cutover just proves the single path.
123+
4. **Rewrite resolvers into pure decision + thin action, one at a time**, deleting the wrapped legacy
124+
methods as each is replaced. Start with `TransportResolver` (already partly pure via
125+
`findReachableTransportOriginAhead`), then `MineableResolver` (already clean), then `DoorResolver`
126+
(the biggest deletion). Each rewrite is harness-tested headlessly + one live walk.
127+
5. **Delete** the now-dead special-cases and their bookkeeping state.
128+
129+
## Deletion targets (the payoff)
130+
131+
- The ~20 door methods + `door/`'s overlap with `processWalk`, and door-attempt state fields.
132+
- The special-case branches inside the ~760-line `handleTransports` that duplicate segment/recovery logic.
133+
- The duplicated recovery paths (`route-fold-continuation`, per-obstacle `*InRawSegment` scans, the
134+
far-tile fallback's obstacle guesses). Net: target four-figure line reduction, not reorganisation.
135+
136+
## Testing
137+
138+
Every resolver's `handles()`/`resolve()` decision is pure and headless-tested in
139+
`recovery`/`obstacle` test classes using in-memory `LiveScene` fixtures (the pattern `RouteRecoveryTest`
140+
already establishes). `WalkerActions` is mocked. The only thing needing a live walk is the **dispatch
141+
cutover (step 3)** and each **resolver rewrite (step 4)** — a handful of walks, not one per fix.
142+
143+
## Where the cataloged "weirdness" gets fixed
144+
145+
Each symptom the user is cataloging (far-side clicks, oscillation, mis-timed door probes, transports not
146+
taken) is a property of **one** resolver or the single dispatch — fix it there, once, with a harness test
147+
that pins it. No more "fix here, break there," because there is no longer a "there."
148+
149+
## Risks & mitigation
150+
151+
- **Behavior drift during cutover** → steps 2–3 keep the *old* logic behind the interface, so the cutover
152+
is a dispatch change, not a logic change; live-tested.
153+
- **Un-unit-tested `processWalk`** → the strangler keeps `processWalk` mostly intact; only the recovery
154+
dispatch is swapped, and each resolver rewrite is guarded by harness + one live walk.
155+
- **Scope creep** → resolvers are added/rewritten one at a time; the branch is always shippable.

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ project.build.version=1.12.33
3232
runelite.injected-client.version=1.12.33
3333

3434
glslang.path=
35-
microbot.version=2.6.15
35+
microbot.version=2.6.16
3636
microbot.commit.sha=nogit
3737
microbot.repo.url=http://138.201.81.246:8081/repository/microbot-snapshot/
3838
microbot.repo.username=

runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/models/Rs2TileItemModel.java

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import net.runelite.client.plugins.microbot.Microbot;
99
import net.runelite.client.plugins.microbot.api.IEntity;
1010
import net.runelite.client.plugins.microbot.util.camera.Rs2Camera;
11+
import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry;
1112
import net.runelite.client.plugins.microbot.util.player.Rs2Player;
1213
import net.runelite.client.plugins.microbot.util.reflection.Rs2Reflection;
1314

@@ -264,33 +265,17 @@ public boolean click(String action) {
264265
Rectangle bounds = canvas == null
265266
? new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())
266267
: canvas.getBounds();
267-
MenuAction selectedMenuAction = menuAction;
268-
String selectedAction = action;
269268
int worldViewId = localPoint1.getWorldView();
270-
Microbot.getClientThread().runOnClientThreadOptional(() -> {
271-
MenuEntry entry = Microbot.getClient().getMenu().createMenuEntry(-1)
272-
.setOption(selectedAction)
273-
.setTarget(target)
274-
.setIdentifier(identifier)
275-
.setType(selectedMenuAction)
276-
.setParam0(param0)
277-
.setParam1(param1)
278-
.setItemId(-1)
279-
.setWorldViewId(worldViewId);
280-
Microbot.getClient().setMenuEntries(new MenuEntry[]{entry});
281-
return true;
282-
});
283-
Rs2Reflection.invokeMenu(
284-
param0,
285-
param1,
286-
menuAction.getId(),
287-
identifier,
288-
-1,
289-
worldViewId,
290-
action,
291-
target,
292-
(int) bounds.getCenterX(),
293-
(int) bounds.getCenterY());
269+
Microbot.doInvoke(new NewMenuEntry()
270+
.option(action)
271+
.target(target)
272+
.identifier(identifier)
273+
.opcode(menuAction.getId())
274+
.param0(param0)
275+
.param1(param1)
276+
.itemId(-1)
277+
.worldViewId(worldViewId),
278+
bounds);
294279
return true;
295280
} catch (Exception ex) {
296281
Microbot.logStackTrace("Rs2TileItemModel", ex);

0 commit comments

Comments
 (0)