Skip to content

feat(sdk): filter findNearbyLoc/interactLoc by loc id (#23)#40

Open
Lawson-Darrow wants to merge 2060 commits into
MaxBittker:mainfrom
Lawson-Darrow:feat/loc-id-filter-23
Open

feat(sdk): filter findNearbyLoc/interactLoc by loc id (#23)#40
Lawson-Darrow wants to merge 2060 commits into
MaxBittker:mainfrom
Lawson-Darrow:feat/loc-id-filter-23

Conversation

@Lawson-Darrow

Copy link
Copy Markdown

Closes #23.

findNearbyLoc and scanFindNearbyLoc only matched on the location name, so same-named objects (all mining rocks are "Rocks", each ore has a distinct id) couldn't be disambiguated, even though NearbyLoc already carries an id.

This adds an optional id filter:

  • New LocFilter { name?, id? } type and a shared locMatcher predicate both finders use.
  • Backward-compatible overloads: findNearbyLoc(/rocks/i) still works; findNearbyLoc(/rocks/i, { id: 2091 }) and findNearbyLoc({ id: 2091 }) are new. Same for scanFindNearbyLoc (the legacy numeric radius arg still works).
  • interactLoc accepts a LocFilter and re-binds to the same variant by id after walking instead of the nearest same-named object.

One behavior note: when interactLoc is given a resolved NearbyLoc, the post-walk re-find now also pins its id (previously name-only). This is more precise and is the intended fix here.

Added unit tests for locMatcher (8 cases). bun test passes; no new type errors in changed files.

MaxBittker and others added 30 commits January 26, 2026 01:16
- Add bot_arcs/**/*.ts and scripts/**/*.ts to tsconfig include
- Fix state.combat -> state.player?.combat (combat on PlayerState)
- Fix shop.items -> shop.shopItems (correct property name)
- Fix player.hp/maxHp -> use Hitpoints skill level/baseLevel
- Fix player.isAnimating -> check animId !== -1
- Fix sendClickItem/sendClickInventory -> sendUseItem
- Fix sendInterfaceButton -> sendClickInterfaceComponent
- Fix bot.eat -> bot.eatFood
- Add null checks for array indexed access (noUncheckedIndexedAccess)
- Fix type-only import for ScriptContext (verbatimModuleSyntax)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MaxBittker and others added 28 commits April 1, 2026 20:36
When the MCP server is launched from a different cwd (e.g. /workspace
in Docker), it can't find bots/{name}/bot.env. Fall back to resolving
relative to the mcp/ directory (one level up = repo root).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SDK clients could override the username field in sdk_action and
sdk_screenshot_request messages to control other users' bots after
authenticating as their own. Pin all post-auth actions to the
session's authenticated targetUsername.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
navigateDialog now logs available options and skips instead of silently
defaulting to index 0. Formatter displays "HP" instead of "Hitpoints".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The engine TCP login normalizes the username via toSafeName before the
DB lookup, but the gateway/SDK auth path forwarded the raw value. A URL
param like ?bot=Bitty would hit the sdk_auth handler with "Bitty" and
fail the case-sensitive account lookup, even when the engine login with
the same credentials succeeded as "bitty".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates all three vendored upstream repos (Engine-TS, Content, Client-TS) to the
274 game snapshot with all rs-sdk modifications re-applied via a 3-way merge
(254 base + local mods, replayed onto upstream 274).

## Engine
- Bump @2004scape/rsbuf 254.1.0 -> 274.1.0 (keep rsmod-pathfinder fork pin)
- Adapt to 274's nested WorldConfig: Environment.ts now exposes flat back-compat
  aliases (WEB_PORT, NODE_ID, ...) plus the rs-sdk-only keys the bot framework
  uses, and createDefaultWorldConfig carries our defaults (xpRate 25, web.port 8888)
- Collision export (web/pages/api.ts) reads 274's in-engine TS routefinder instead
  of the dropped @2004scape/rsmod-pathfinder WASM, which the engine no longer feeds
- Remove dead GameMap.findLongPath (274 routefinder has no long-path variant; the
  SDK does long-distance pathfinding client-side)
- Re-apply security/login mods (sdk_auth, IP login tracking, login-takeover, banned
  usernames, no dev staffmodlevel on non-prod) onto 274's auth refactor
- Add MESSAGE_PUBLIC server packet at the free opcode 255 (254's 81 is now OBJ_ADD)
- NODE_WS_ONDEMAND now defaults on: the 274 web client streams on-demand assets over
  the game WebSocket and otherwise stalls at ~60% on load

## Content
- Full 254->274 content update with our 4 script/config patches re-applied
- worldmap.jag regenerated for /mapview from the 274 maps (via engine build)

## Webclient
- Rebuild standard + bot + viewer bundles; CLIENT_VERSION 274
- ServerProt: keep 274 opcodes, add MESSAGE_PUBLIC=255 with -1 size table entry
- Replace removed Packet.pIsaac() calls with 274's p1Enc()
- Disable terser property mangling (274 enabled it): the bot framework accesses
  client members by name from bot.ejs / the Puppeteer harness

## SDK
- Regenerate sdk/collision-data.json from 274 maps (905k -> 958k tiles)

## start.ts
- Add 274 to the version menu

Verified end-to-end: engine boots and loads 274 maps, content packs, engine
typechecks clean, webclient builds, a headless bot logs in, sdk.getState() returns
a complete snapshot, and bot.walkTo() pathfinds/moves on the regenerated collision
data. Map and hiscores sprites serve correctly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
On 274, accepting the character design left the design interface (player_kit,
3559) open forever, so skipTutorial got stuck before reaching the tutorial dialogs.

The design "Accept" button is a normal button (clientcode=326 / CC_ACCEPT_DESIGN).
A real click sends BOTH the IDK_SAVEDESIGN packet (which only sets the appearance)
AND an IF_BUTTON for the component. The IF_BUTTON is what fires the content trigger
[if_button,player_kit:accept] -> if_close -> [if_close,player_kit] ->
queue(tutorial_designed_character), which closes the interface and advances the
tutorial. The bot's acceptCharacterDesign() only sent IDK_SAVEDESIGN, so the engine
saved the appearance (verified: handler accepts it) but nothing closed the modal.

acceptCharacterDesign() now also sends the IF_BUTTON for the CC_ACCEPT_DESIGN
component (found via IfType.list), matching a real click. Verified: the design
modal closes (interface 3559 -> -1), the tutorial advances to the guide dialog,
and waitForTicks/clickDialog proceed normally.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 254->274 merge dropped the rs-sdk bot-bridge updates to renamed Client
members, leaving them referencing fields that are now undefined:

- StateCollector/BotOverlay: chatComId -> chatModalId. With chatComId
  undefined, `undefined !== -1` made dialog state ALWAYS report isOpen=true
  with zero options, so skipTutorial / navigateDialog / any dialog click hung
  forever (firing RESUME_PAUSEBUTTON into nothing).
- Client.setTab/setCombatStyle: sideOverlayId -> sideIcon, sideTab ->
  activeIcon, redrawSidebar -> redrawSide, redrawSideicons -> redrawIcons.

Verified end-to-end: bot.skipTutorial() returns "Tutorial skipped after 3
dialog clicks", teleports to Lumbridge with the starter kit.

Note: esbuild bundles despite TS errors, so these merge misses shipped
silently -- `bunx tsc --noEmit` in server/webclient surfaces them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- MapView: when the visible area is larger than the map (zoomed out), the
  min/max focus clamps conflicted and pinned focus to a fixed value, freezing
  panning. Center the axis in that case; otherwise clamp to a single edge.
- ItemViewer: correct import casing Jagfile -> JagFile (matches #/io/JagFile.js).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
274 reads nested config only from data/config/world.json and ignores
process.env, so fly.io [env] vars (WEB_PORT, LOGIN_SERVER, NODE_MEMBERS, ...)
were dropped at runtime. Overlay process.env on top of the loaded config in
Environment.ts (env wins; absent keys fall back to world.json/defaults; local
dev with no such vars is a no-op). Export migrateFromLegacyEnv to reuse its
existing flat-env -> nested-config mapping.

Also drop the Dockerfile's `cp .env.example .env` build step: 274 removed
.env.example, and with shell precedence its failure aborted the engine build.
No .env file is needed now that config comes from process.env.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nder)

274's clientRoutefinder mode makes the server expect the *client* to send the
movement path to an interaction target; it no longer pathfinds to locs itself.
The bot's Client.interactLoc() sent only the OPLOC packet with no movement, so
the player never walked and the server replied "I can't reach that!" whenever
it wasn't already adjacent. This broke every bot.* action that walks-then-
interacts with a loc (chopTree, mine, openDoor, openBank-from-afar, etc.).

Mirror the existing useItemOnLoc(): convert world->scene coords and call
tryMove(...type=2) with the loc footprint before writing OPLOC, so the client
routefinder emits the path. Verified locally (clientRoutefinder=true): a bot
4 tiles from a tree now auto-walks adjacent and chops it (+625 wc xp), where
it previously failed with "I can't reach that!".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The webclient bundle (server/webclient/out/) is gitignored and the Dockerfile
did not build it, so `fly deploy` silently shipped whatever was in the local
out/ dir — easy to deploy a stale client. Add a webclient dep-install + build
step (mirroring engine/gateway), and exclude server/webclient/out and
node_modules from the Docker context so the in-image build is authoritative.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…finder)

Same regression class as the interactLoc fix: in 274 clientRoutefinder mode the
server expects the client to send the route to an interaction target. talkToNpc,
interactNpc and interactPlayer wrote only their op packet with no movement, so
the player never walked and the server replied "I can't reach that!" unless
already adjacent. This broke bot.talkTo / interactNpc / attackNpc / pickpocket /
interactPlayer (trade/follow) from any distance.

Add a tryMove(...type=2) to each, targeting the npc/player routeX/routeZ with a
1x1 footprint, mirroring spellOnNpc/useItemOnNpc. Audited all interaction methods
in Client.ts; these three were the only ones still missing the walk-to step.
Verified locally + unit tests pass: a bot now walks to a distant NPC and engages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GameMap.init loads map collision from the packed zip
(data/pack/.cache/maps-server.zip) when present, falling back to the raw
data/pack/server/maps/ dir only if it's absent. The collision export discovered
mapsquares from the raw dir alone — but the zip and dir don't fully overlap: the
zip had ~41 mapsquares the dir lacked (the SW mainland around Yanille, the
Tirannwn band z3584-3775, several high-z instanced areas). Those were loaded and
allocated by the engine but silently dropped from the export, so the SDK's
collision-data.json had no data there and bots got stranded with "no path".

Union the zip and dir mapsquare names for discovery; the existing isZoneAllocated
check still filters to mapsquares actually loaded into the routefinder. Raises
export coverage from 442 to ~483 mapsquares. (collision-data.json regenerated
from this in a follow-up commit.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ENOENT)

The previous commit added zip-only mapsquares to discovery, but the door scanner
still loaded m/l packets via Packet.load() from the raw maps dir, which throws
ENOENT for squares present only in the zip (e.g. m39_71). Add a loadMapPacket()
helper that reads from the dir or falls back to the unzipped entries, and skip
squares with no loc data. Verified the zip carries all 483 m+l entries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Regenerated from the fixed /api/exportCollision (members prod). Adds the 41
mapsquares the old dir-only discovery dropped: the SW mainland around Yanille
(~x2048-2367, z3072-3391), the Tirannwn band (z3584-3775), and several high-z
instanced squares. zones 113k -> 123.6k, tiles 958k -> 1.07M, doors 2550 -> 2603.
Bots in those regions previously got "no path / zone not allocated" and were
stranded; the SDK pathfinder now loads and routes there.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Flip the default so newly-created bots show other players' public chat
(SHOW_CHAT=true) instead of opting in. The controller code (sdk/runner.ts,
mcp/api) already defaulted to true, but the bot.env template wrote
SHOW_CHAT=false into every new bot and the docs said off-by-default; this makes
the default consistent. Template -> SHOW_CHAT=true, create-bot.ts defaults to
chat-on with a --no-chat/--hide-chat opt-out (--show-chat kept as a no-op for
back-compat), sdk default fallback false -> true, README updated. Existing bots
keep their explicit SHOW_CHAT=false until changed individually.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The webclient's levelExperience table used the wrong XP formula
(exponent /7.0, no *10 multiplier), so it computed much higher XP
thresholds per level than the engine. Loaded saves that were level 99
on the server displayed as lower levels in the client. Align the
client table generation with engine/src/engine/entity/Player.ts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
server/engine/public/client/{client.js,ondemandworker.js} were tracked
copies of webclient build artifacts. handlePublicFiles() (web/pages/static.ts)
serves public/<path> BEFORE falling back to webclient/out, so these stale
copies shadowed the freshly-built /client/client.js on every deploy —
which is why the XP-curve fix (0f29234) had no effect on prod.

These are leftovers from the pre-'build-in-image' copy-step deploy model.
The engine now serves the fresh out/standard build directly. Kept the genuine
non-built assets (SCC1_Florestan.sf2 soundfont, tinymidipcm.wasm).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The client was imported as a static ES module URL ('./bot/client.js')
with no version query, and the engine sent no cache headers, so browsers
held the stale cached client indefinitely — even through hard refresh.
After the XP-curve fix, the corrected bundle was live on the origin but
players still saw old skill levels because their browser never refetched.

- Append ?v=<CLIENT_CACHEBUST> (per-process-start token) to the client.js
  import in bot.ejs and client.ejs, so each deploy yields a new module URL.
- Send Cache-Control: no-cache on the HTML pages so the browser always
  revalidates and picks up the new ?v= token.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previous fix (0f29234) correctly switched the client curve to /10 to
match the engine, but also wrongly added the engine's *10 multiplier.
The engine stores XP in x10 'fine' units (its table has *10), but the
server sends the client REAL xp (divided by 10). So the client table must
be in real-xp units WITHOUT *10 (as upstream had it) — only the curve
shape (/10) needed to match.

With the erroneous *10, client base-level thresholds were 10x too large,
so skills whose real xp cleared 99 under the engine curve (e.g. Prayer
1,071,075) showed as baseLevel 66-90. Verified against live getState:
all 19 skills now compute to baseLevel 99 (L99 real-xp threshold 998,458).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
findNearbyLoc and scanFindNearbyLoc matched only on name, so same-named
locations (e.g. ore rocks all named "Rocks") could not be disambiguated even
though NearbyLoc already carries an id. Add an optional id filter via a new
LocFilter type and a shared locMatcher predicate, with backward-compatible
overloads. interactLoc accepts a LocFilter and re-binds to the same variant
after walking. Adds unit tests for the matching logic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bot.interactLoc()` should support filtering by loc ID

7 participants