mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
feat(sdk): option-aware loc resolution fixes the duplicate-furnace trap
Overlapping duplicate locs are real content: the Lumbridge furnace pairs an
inert, option-less 'Furnace' loc with the one offering Smelt, and plain
name matching returns whichever is nearer - so interactLoc(/furnace/i,
'smelt') dead-ended with 'No matching option on Furnace' while a usable
furnace stood one tile away (bug reports 2026-07-27 and 2026-08-08; the
latter's fork-only bot.smelt helper broke on exactly this).
findNearbyLoc now takes {withOption} to consider only locs publishing a
matching interaction option, and interactLoc resolves its target through
that filter whenever the option is named (falling back to plain matching so
the no-matching-option error still names what it found).
Verified live at the Lumbridge furnace: both duplicate locs confirmed
( (3225,3256) with no options, (3226,3255) with Smelt ), withOption picks
the smeltable one, and interactLoc(/furnace/i, 'smelt') opens the smelting
interface.
The same reports' bot.mine default-target complaint targets a helper that
only exists in a fork, not in this repo - nothing to fix here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b3c19c5abf
commit
d447f76077
5 files changed
+98
-9
No files matched your search
+14
-6
@@ -501,15 +501,23 @@ export class ActionHelpers {
|
||||
|
||||
resolveLocation(
|
||||
target: NearbyLoc | string | RegExp | undefined,
|
||||
defaultPattern: RegExp
|
||||
defaultPattern: RegExp,
|
||||
withOption?: string | RegExp
|
||||
): NearbyLoc | null {
|
||||
if (!target) {
|
||||
return this.sdk.findNearbyLoc(defaultPattern);
|
||||
}
|
||||
if (typeof target === 'object' && 'x' in target) {
|
||||
if (typeof target === 'object' && target !== null && 'x' in target) {
|
||||
return target;
|
||||
}
|
||||
return this.sdk.findNearbyLoc(target);
|
||||
const pattern = target ?? defaultPattern;
|
||||
if (withOption !== undefined) {
|
||||
// Prefer a loc that actually offers the requested option -
|
||||
// overlapping option-less duplicates (Lumbridge furnace) otherwise
|
||||
// win on distance and dead-end the interaction. Fall back to plain
|
||||
// name matching so the "no matching option" error can still name
|
||||
// the loc it found.
|
||||
const withOptionMatch = this.sdk.findNearbyLoc(pattern, { withOption });
|
||||
if (withOptionMatch) return withOptionMatch;
|
||||
}
|
||||
return this.sdk.findNearbyLoc(pattern);
|
||||
}
|
||||
|
||||
resolveInventoryItem(
|
||||
|
||||
+4
-2
@@ -3428,7 +3428,9 @@ export class BotActions {
|
||||
): Promise<InteractLocResult> {
|
||||
await this.dismissBlockingUI();
|
||||
|
||||
const loc = this.helpers.resolveLocation(target, /./);
|
||||
// A named option narrows resolution to locs that actually offer it.
|
||||
const optionFilter = typeof option === 'number' ? undefined : option;
|
||||
const loc = this.helpers.resolveLocation(target, /./, optionFilter);
|
||||
if (!loc) {
|
||||
return { success: false, message: `Location not found: ${target}`, reason: 'loc_not_found' };
|
||||
}
|
||||
@@ -3459,7 +3461,7 @@ export class BotActions {
|
||||
const locPattern = target instanceof RegExp || typeof target === 'string'
|
||||
? target
|
||||
: exactNamePattern(loc.name);
|
||||
const locNow = this.helpers.resolveLocation(locPattern, /./);
|
||||
const locNow = this.helpers.resolveLocation(locPattern, /./, optionFilter);
|
||||
if (!locNow) {
|
||||
return { success: false, message: `${loc.name} no longer visible`, reason: 'loc_not_found' };
|
||||
}
|
||||
|
||||
+10
-1
@@ -906,7 +906,16 @@ export class BotSDK {
|
||||
/** Find location by name pattern (shortest matching name wins, then nearest; reachable preferred). */
|
||||
findNearbyLoc(pattern: string | RegExp, options?: FindOptions): NearbyLoc | null {
|
||||
if (!this.state) return null;
|
||||
return applyFindOptions(shortestNameMatch(this.state.nearbyLocs, pattern), options);
|
||||
let candidates = this.state.nearbyLocs;
|
||||
if (options?.withOption !== undefined) {
|
||||
const optionPattern = options.withOption;
|
||||
const regex = typeof optionPattern === 'string' ? new RegExp(optionPattern, 'i') : optionPattern;
|
||||
candidates = candidates.filter(loc => loc.optionsWithIndex?.some(o => {
|
||||
regex.lastIndex = 0;
|
||||
return regex.test(o.text);
|
||||
}));
|
||||
}
|
||||
return applyFindOptions(shortestNameMatch(candidates, pattern), options);
|
||||
}
|
||||
|
||||
/** Get all nearby locations (trees, rocks, etc). */
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Logic test for findNearbyLoc's withOption filter - no bot required.
|
||||
*
|
||||
* Overlapping duplicate locs are real: the Lumbridge furnace pairs an inert,
|
||||
* option-less "Furnace" with the one offering Smelt, and the inert one often
|
||||
* wins plain name matching on distance - dead-ending interactLoc with
|
||||
* "No matching option on Furnace" while a usable furnace stands 1 tile away.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { BotSDK } from '../index';
|
||||
import type { BotWorldState, NearbyLoc } from '../types';
|
||||
|
||||
function makeLoc(partial: Partial<NearbyLoc>): NearbyLoc {
|
||||
return {
|
||||
id: 2781,
|
||||
name: 'Furnace',
|
||||
x: 0,
|
||||
z: 0,
|
||||
distance: 0,
|
||||
optionsWithIndex: [],
|
||||
...partial,
|
||||
} as NearbyLoc;
|
||||
}
|
||||
|
||||
function mountSdk(locs: NearbyLoc[]): BotSDK {
|
||||
const sdk = new BotSDK({ botUsername: 'test' });
|
||||
(sdk as any).state = { nearbyLocs: locs } as unknown as BotWorldState;
|
||||
return sdk;
|
||||
}
|
||||
|
||||
describe('findNearbyLoc({ withOption })', () => {
|
||||
// Nearest-first list order, inert duplicate first - the reported trap.
|
||||
const inert = makeLoc({ x: 3225, z: 3256, distance: 1, optionsWithIndex: [] });
|
||||
const smeltable = makeLoc({ x: 3226, z: 3255, distance: 2, optionsWithIndex: [{ text: 'Smelt', opIndex: 2 }] });
|
||||
|
||||
test('plain matching still returns the nearer, inert duplicate', () => {
|
||||
const sdk = mountSdk([inert, smeltable]);
|
||||
expect(sdk.findNearbyLoc(/furnace/i)).toBe(inert);
|
||||
});
|
||||
|
||||
test('withOption skips duplicates that do not offer the option', () => {
|
||||
const sdk = mountSdk([inert, smeltable]);
|
||||
expect(sdk.findNearbyLoc(/furnace/i, { withOption: /smelt/i })).toBe(smeltable);
|
||||
});
|
||||
|
||||
test('string option filters case-insensitively', () => {
|
||||
const sdk = mountSdk([inert, smeltable]);
|
||||
expect(sdk.findNearbyLoc(/furnace/i, { withOption: 'SMELT' })).toBe(smeltable);
|
||||
});
|
||||
|
||||
test('returns null when nothing offers the option', () => {
|
||||
const sdk = mountSdk([inert]);
|
||||
expect(sdk.findNearbyLoc(/furnace/i, { withOption: /smelt/i })).toBeNull();
|
||||
});
|
||||
|
||||
test('a stateful /g regex cannot alternate matches across calls', () => {
|
||||
const sdk = mountSdk([inert, smeltable]);
|
||||
const sticky = /smelt/gi;
|
||||
expect(sdk.findNearbyLoc(/furnace/i, { withOption: sticky })).toBe(smeltable);
|
||||
expect(sdk.findNearbyLoc(/furnace/i, { withOption: sticky })).toBe(smeltable);
|
||||
});
|
||||
});
|
||||
@@ -150,6 +150,13 @@ export interface FindOptions {
|
||||
* one. Targets whose reachability is unknown always qualify.
|
||||
*/
|
||||
reachable?: boolean;
|
||||
/**
|
||||
* Loc lookups only: consider only locs that publish a matching interaction
|
||||
* option. Overlapping duplicates are real - the Lumbridge furnace pairs an
|
||||
* inert, option-less "Furnace" with the one that offers Smelt, and plain
|
||||
* name matching happily returns the inert one.
|
||||
*/
|
||||
withOption?: string | RegExp;
|
||||
}
|
||||
|
||||
export interface LocOption {
|
||||
|
||||
Reference in new issue
Block a user