Close-type components ("Close Window") have no server if_button trigger -
the real client handles them locally. clickComponent sent IF_BUTTON anyway,
so the server answered "No trigger for [if_button,shop_template:com_77]" and
the modal stayed open. Route close-type buttons to closeModal().
An open shop or bank replaces the inventory tab, so isComponentVisible()
rejects inventory packets sent underneath with no game message at all.
sendUseItemOnItem now fails up front with reason 'modal_open' naming the
close method, instead of appearing to do nothing.
fletchLogs searched dialog.options for buttons reading "Ok". skill_multi3
has none - its twelve buttons are Make X / Make 10 / Make 5 / <product> per
product - so that branch never fired and every call fell through to an
index guess: arrow shafts clicked Make X and hung on the count prompt,
'short' on regular logs made ten arrow shafts. Product labels also arrive
padded with layout newlines ("\n\n\n\n15 Arrow Shafts"), defeating string
matching.
Flatten component labels at the client, and resolve the product by name
via resolveSkillDialogProduct - only the make-1 button carries a name, so
the named options are the products in order. On no match, report the
available labels rather than fletching something else.
Also document closeInterface/closeShop/sendCloseModal and the dialog
option ordering in CLAUDE.md, which is where they were missing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
10 KiB
RS-Agent Bot Guide
You're here to play the mmo game through the progressive development of botting scripts, starting small then adapting to your desires and ideas. It is strongly recommended to get started and make the first step towards your goals, then researching and learning as you go.
First Time Setup
Create a new bot using the setup script:
Ask the user for a bot name (max 12 chars, alphanumeric). If they skip, use the command without a username to auto-generate a random 9-character name.
# With custom username
bun bots/create-bot.ts {username}
# Auto-generate random username
bun bots/create-bot.ts
# Use local server (sets SERVER=localhost in bot.env)
bun bots/create-bot.ts {username} --local
# Use a custom server
bun bots/create-bot.ts {username} --server=myserver.example.com
This automatically creates:
bots/{username}/bot.env- Credentials with auto-generated passwordbots/{username}/lab_log.md- Session notes templatebots/{username}/script.ts- Ready-to-run starter script
Quick Start
- Install dependencies:
bun install(from project root) - Open project in your AI coding tool — the MCP server will be available automatically
- Control your bot with suggestions.
Tools
| Tool | Description |
|---|---|
execute_code(bot_name, code) |
Run code on a bot. Auto-connects on first use. |
list_bots() |
List connected bots |
disconnect_bot(name) |
Disconnect a bot |
Example
// Just execute - auto-connects on first use
execute_code({
bot_name: "mybot",
code: `
const state = sdk.getState();
console.log('Position:', state.player.worldX, state.player.worldZ);
// Chop trees for 1 minute
const endTime = Date.now() + 60_000;
while (Date.now() < endTime) {
const tree = sdk.findNearbyLoc(/^tree$/i);
if (tree) await bot.chopTree(tree);
}
return sdk.getInventory();
`
})
See mcp/README.md for detailed API reference.
Code runs in an async context with bot (BotActions) and sdk (BotSDK) available as globals.
Session Workflow
This is a persistent character - you don't restart fresh each time. The workflow is:
1. Check World State First
Before writing any script, check where the bot is and what it has:
bun sdk/cli.ts {username}
This shows: position, inventory, skills, nearby NPCs/objects, and more.
Exception: Skip this if you just created the character and know it's at spawn.
Tutorial Check: If the character is in the tutorial area, call await bot.skipTutorial() before running any other scripts. The tutorial blocks normal gameplay.
2. Write Your Script
Edit bots/{username}/script_name.ts with your goal. Keep scripts focused on one task. you may write multiple scripts for different tasks and switch between them.
3. Run the Script
bun bots/{username}/script_name.ts
4. Observe and Iterate
Watch the output. After the script finishes (or fails), check state again:
bun sdk/cli.ts {username}
Record observations in lab_log.md, then improve the script.
Script Duration Guidelines
Start short, extend as you gain confidence:
| Duration | Use When |
|---|---|
| 10s | New script, single actions, untested logic, debugging |
| 30s-1 min | Validated approach, building confidence |
| 5+ min | Proven strategy, grinding runs. USE SPARINGLY |
A failed 5-minute run wastes more time than five 30 second diagnostic runs. Fail fast and start simple.
Look out for "I can't reach" messages - the solution is often to open closed gates or that the item isn't accessible.
Read and grep in the learnings/ and wiki/ folder for tips, skill guides, item and npc locations, and shop information.
SDK API Reference
For the complete method reference, see sdk/API.md (auto-generated from source).
Quick overview:
bot.*- High-level actions that attempt to observe method-specific evidence (chopTree, walkTo, attackNpc, etc.). Check a result when the method returns one.sdk.*- State queries and low-level browser-client dispatches. A successfulsend*result does not prove the game server applied the effect.
bot.* Quick Reference
| Method | What it does |
|---|---|
walkTo(x, z, tolerance?) |
Pathfind to coords, opens doors along the way |
talkTo(target) |
Walk to NPC, start dialog |
interactNpc(target, option?) |
Walk to NPC, interact with any option (e.g. 'trade', 'fish') |
interactLoc(target, option?) |
Walk to loc, interact with any option (e.g. 'mine', 'smelt') |
attackNpc(target) |
Walk to NPC, start combat |
pickpocketNpc(target) |
Pickpocket NPC, detects XP gain vs stun |
castSpellOnNpc(target, spell) |
Cast combat spell on NPC |
chopTree(target?) |
Chop tree, wait for logs |
pickupItem(target) |
Pick up ground item |
openDoor(target?) |
Open a door or gate |
openBank() |
Open nearest bank |
depositItem(target, amount?) |
Deposit item to bank |
withdrawItem(slot, amount?) |
Withdraw item from bank |
closeInterface() |
Close any open modal (shop, bank, book, quest scroll) |
closeShop() / closeBank() |
Close the shop / bank interface |
openShop(target?) |
Open shop via shopkeeper NPC |
buyFromShop(target, amount?) |
Buy item from open shop |
sellToShop(target, amount?) |
Sell item to open shop |
equipItem(target) |
Equip from inventory |
unequipItem(target) |
Unequip to inventory |
eatFood(target) |
Eat food, returns HP gained |
useItemOnLoc(item, loc) |
Use inventory item on loc (e.g. fish on range) |
useItemOnNpc(item, npc) |
Use inventory item on NPC |
burnLogs(target?) |
Light logs with tinderbox |
fletchLogs(product?) |
Fletch logs with knife |
craftLeather(product?) |
Craft leather with needle |
smithAtAnvil(product) |
Smith bars at anvil |
dismissBlockingUI() |
Dismiss level-up dialogs (called automatically by all actions) |
navigateDialog(choices) |
Auto-click through dialog options |
skipTutorial() |
Skip the tutorial island |
sdk.* Commonly Used Directly
| Method | What it does |
|---|---|
getState() |
Full world state snapshot |
getSkill(name) / getSkillXp(name) |
Skill info |
getInventory() / findInventoryItem(pattern) |
Inventory queries |
findNearbyNpc(pattern) / findNearbyLoc(pattern) |
Find nearby entities |
findGroundItem(pattern) |
Find ground items |
getDialog() |
Current dialog state |
sendClickDialog(option) |
Click dialog option by its published index |
clickDialogByText(pattern) |
Click dialog option by label — prefer this |
sendClickComponent(id) |
Click interface button |
sendCloseModal() / sendCloseShop() |
Close the open modal / shop |
sendDropItem(slot) |
Drop inventory item |
sendUseItem(slot) |
Use inventory item (bury, etc.) |
sendUseItemOnItem(src, dst) |
Combine two items |
say(text) |
Send chat, auto-chunked past the length cap |
getChat(opts?) / getNewChat(opts?) |
Read chat history (500-deep) / only unseen messages |
waitForChat(opts?) |
Wait for a message ({from, matching, timeout}) |
waitForCondition(pred) |
Wait for state predicate |
waitForTicks(n) |
Wait n game ticks |
scanNearbyLocs(radius?) |
Async extended-range loc scan; must be awaited |
See sdk/API.md for exact async signatures, parameter defaults, and return
types.
Code environments
MCP execute_code exposes bot and sdk directly. Standalone
bots/{username}/*.ts files must import runScript and receive
{ bot, sdk } in its callback. Do not paste runner-context expressions into
execute_code.
Only one control connection should own a bot. Use observe mode for read-only
monitoring, avoid overlapping action programs on the same bot, and inspect
sdk.getStateAge() before acting after a disconnect or long pause.
Modals Hide the Inventory
A shop or bank modal replaces the inventory tab, so the server rejects inventory packets sent underneath it as "component not visible" — with no game message. Close the modal before doing anything else:
await bot.closeInterface(); // any modal
await bot.closeShop(); // shop specifically
await sdk.sendCloseModal(); // low-level
Don't click the interface's own "Close Window" component id. It's a client-side close button with no server trigger; the SDK now routes those to a close for you, but the named methods above are what to reach for.
Dialog Options Are Not In Product Order
Skill dialogs (fletching, crafting) publish four buttons per product — Make X, Make 10, Make 5, then one labelled with the product name. Arrow shafts are option 4 of 12, not option 1. Never assume ordering:
const dialog = sdk.getDialog();
console.log(dialog?.options.map(o => `${o.index}: ${o.text}`));
await sdk.clickDialogByText(/arrow shaft/i); // or bot.fletchLogs('arrow shaft')
Dismiss Level-Up Dialogs
All BotActions methods automatically dismiss blocking UI (level-up dialogs, etc.) before executing. You don't need to call this manually in loops.
// Only needed if using low-level sdk methods directly:
await bot.dismissBlockingUI();
// Or manually check
if (sdk.getState()?.dialog.isOpen) {
await sdk.sendClickDialog(0);
}
Error Handling
const result = await bot.chopTree();
if (!result.success) {
console.log(`Failed: ${result.message}`);
// Handle failure - maybe walk somewhere else
}
Project Structure
bots/
└── {username}/
├── bot.env # Credentials (BOT_USERNAME, PASSWORD, SERVER)
├── lab_log.md # Session notes and observations
└── script.ts # Current script
sdk/
├── index.ts # BotSDK (low-level)
├── actions.ts # BotActions (high-level)
├── cli.ts # CLI for checking state
└── types.ts # Type definitions
learnings/
├── banking.md
└── ...etc
wiki/
├── npcs/
├── items/
├── skills/
└── shops/
Troubleshooting
"No state received" - Bot isn't connected to game. Open browser first or use autoLaunchBrowser: true.
Script stalls - Check for open dialogs (state.dialog.isOpen). Level-ups block everything.
"Can't reach" - Path is blocked. Try walking closer first, or find a different target.
Wrong target - Use more specific regex patterns: /^tree$/i not /tree/i (which matches "tree stump").
Start small and build up!