feat(sdk): publish plane on NearbyLoc, refuse stale cross-plane loc refs

Scans are already scoped to the player's plane, but a NearbyLoc had no
level field, so a reference carried across a climb/descend clicked
through to whatever occupies those coordinates on the wrong floor
(Black Knights' Fortress ladder report). Publish the observation plane
on every scanned loc, and make resolveLocation re-resolve a passed-in
loc whose plane no longer matches the player - failing loudly with
loc_not_found instead of interacting with the wrong floor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
max bittkerandClaude Fable 5 committed 2026-08-12 12:43:05 -04:00
1 parent 931ebe0514
commit 035c64a9ad
8 files changed
+33 -3

No files matched your search

+1 -1
View File
@@ -221,7 +221,7 @@
| Signature | Description |
|---|---|
| `async scanNearbyLocs(radius?: number): Promise<NearbyLoc[]>` | Scan for nearby locations with custom radius. |
| `async scanNearbyLocs(radius?: number): Promise<NearbyLoc[]>` | Scan for nearby locations with custom radius. Results are scoped to the player's current plane (each carries `level`); re-scan after climbing or descending rather than reusing old references. |
| `async scanGroundItems(radius?: number): Promise<GroundItem[]>` | Scan for ground items on-demand. This is more efficient than constantly pushing this data in state updates. |
| `async scanFindNearbyLoc(pattern: string \| RegExp, radius?: number): Promise<NearbyLoc \| null>` | Find a nearby location by name pattern (on-demand scan). |
| `async scanFindGroundItem(pattern: string \| RegExp, radius?: number): Promise<GroundItem \| null>` | Find a ground item by name pattern (on-demand scan). |
+11 -1
View File
@@ -505,7 +505,17 @@ export class ActionHelpers {
withOption?: string | RegExp
): NearbyLoc | null {
if (typeof target === 'object' && target !== null && 'x' in target) {
return target;
// A loc reference carried across a climb/descend is stale: the
// click would resolve against whatever occupies these coordinates
// on the wrong floor. Re-resolve from current state instead, and
// fail loudly (null -> loc_not_found) if it isn't on this plane.
const playerLevel = this.sdk.getState()?.player?.level;
if (playerLevel === undefined || target.level === undefined || target.level === playerLevel) {
return target;
}
return this.sdk.getState()?.nearbyLocs.find(
l => l.id === target.id && l.x === target.x && l.z === target.z
) ?? null;
}
const pattern = target ?? defaultPattern;
if (withOption !== undefined) {
+3 -1
View File
@@ -944,7 +944,9 @@ export class BotSDK {
// Use these for expensive scans of nearby locations and ground items
/**
* Scan for nearby locations with custom radius.
* Scan for nearby locations with custom radius. Results are scoped to the
* player's current plane (each carries `level`); re-scan after climbing or
* descending rather than reusing old references.
* @param radius - Scan radius in tiles (default 15)
* @returns Array of nearby locations sorted by distance
*/
+1
View File
@@ -4,6 +4,7 @@ import type { NearbyNpc } from '../types';
const target: NearbyNpc = {
kind: 'npc',
id: 9,
index: 42,
name: 'Guard',
combatLevel: 21,
+1
View File
@@ -4,6 +4,7 @@ import type { NearbyNpc, NearbyPlayer } from '../types';
const goblin: NearbyNpc = {
kind: 'npc',
id: 100,
index: 4,
name: 'Goblin',
combatLevel: 2,
+6
View File
@@ -174,6 +174,12 @@ export interface NearbyLoc {
name: string;
x: number;
z: number;
/**
* Plane this loc was observed on - always the player's plane at scan
* time. A loc carried across a climb/descend is stale: re-scan instead
* of interacting with it, or the click resolves against the wrong floor.
*/
level: number;
distance: number;
optionsWithIndex: LocOption[];
/** Convenience array of option text strings */
@@ -1095,6 +1095,7 @@ export class BotStateCollector implements ScanProvider {
name,
x: baseX + tileX,
z: baseZ + tileZ,
level: currentLevel,
distance,
optionsWithIndex,
options: optionsWithIndex.map(o => o.text),
@@ -1129,6 +1130,7 @@ export class BotStateCollector implements ScanProvider {
name: locType.name,
x: baseX + tileX,
z: baseZ + tileZ,
level: currentLevel,
distance,
optionsWithIndex,
options: optionsWithIndex.map(o => o.text),
@@ -1163,6 +1165,7 @@ export class BotStateCollector implements ScanProvider {
name: locType.name,
x: baseX + tileX,
z: baseZ + tileZ,
level: currentLevel,
distance,
optionsWithIndex,
options: optionsWithIndex.map(o => o.text),
@@ -1197,6 +1200,7 @@ export class BotStateCollector implements ScanProvider {
name: locType.name,
x: baseX + tileX,
z: baseZ + tileZ,
level: currentLevel,
distance,
optionsWithIndex,
options: optionsWithIndex.map(o => o.text),
+6
View File
@@ -144,6 +144,12 @@ export interface NearbyLoc {
name: string;
x: number;
z: number;
/**
* Plane this loc was observed on - always the player's plane at scan
* time. A loc carried across a climb/descend is stale: re-scan instead
* of interacting with it, or the click resolves against the wrong floor.
*/
level: number;
distance: number;
optionsWithIndex: LocOption[]; // Options with op index (use .map(o => o.text) for display)
/** Convenience array of option text strings */