mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
fix(sdk,content): tradeoff-free fixes from the 2026-09-21 bug-report inbox
- walkTo: honor the caller's tolerance on the final waypoint (was hardcoded 2, so tolerance 0/1 could never be met) and judge distance in tiles (Chebyshev) so a diagonal neighbour counts as adjacent - interactLoc: a same-named fallback loc must offer the requested op, otherwise dispatch the caller's own loc instead of a different id - talkTo: refuse reachable:false targets instead of queueing a talk the server retries forever - eatFood: target optional; no target eats the first item with Eat, not slot 0 - depositItem(pattern, -1): keep going past a per-id failure, report aggregate - closeBank: also close when bank.isOpen outlives interface.isOpen by a frame - openShop: return success when a shop is already open - pickupItem: baseline the inventory count before dispatch (static map spawns never leave the ground mirror, so the count is the only signal) - pickpocketNpc: match Steal-from; craftJewelry: dragonstone slot + reject unknown gems; craftLeather: hard/dragon leathers, named product wins - create-bot --local: write localhost:8888 + GATEWAY_URL; cli hint names the lite runner; getInventory docs say it is compacted - cheat_help: every debugproc listed with its required ::~ prefix Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
3c176cd763
commit
4c2069cc0c
9 files changed
+215
-54
No files matched your search
@@ -77,6 +77,13 @@ async function createBot(username?: string, serverOverride?: string, hideChat?:
|
||||
await replaceInFile(envPath, { 'rs-sdk-demo.fly.dev': serverOverride });
|
||||
console.log(`Server set to: ${serverOverride}`);
|
||||
}
|
||||
if (serverOverride === 'localhost') {
|
||||
// The lite runner builds its cache origin from SERVER, so a bare
|
||||
// 'localhost' dials port 80; and SERVER with a port would then be
|
||||
// mistaken for the gateway. Name both explicitly.
|
||||
await replaceInFile(envPath, { 'SERVER=localhost\n': 'SERVER=localhost:8888\nGATEWAY_URL=ws://localhost:7780\n' });
|
||||
console.log(`Local stack: engine at localhost:8888, gateway at ws://localhost:7780`);
|
||||
}
|
||||
|
||||
// Public chat is shown by default; disable it with --no-chat / --hide-chat
|
||||
if (hideChat) {
|
||||
|
||||
+2
-2
@@ -112,7 +112,7 @@
|
||||
| `async unequipItem(target: InventoryItem \| string \| RegExp): Promise<UnequipResult>` | Unequip an item to inventory. |
|
||||
| `getEquipment(): InventoryItem[]` | Get all currently equipped items. |
|
||||
| `findEquippedItem(pattern: string \| RegExp): InventoryItem \| null` | Find an equipped item by name pattern. |
|
||||
| `async eatFood(target: InventoryItem \| string \| RegExp): Promise<EatResult>` | Eat food to restore hitpoints. |
|
||||
| `async eatFood(target?: InventoryItem \| string \| RegExp): Promise<EatResult>` | Eat food to restore hitpoints. With no target, eats the first inventory item that offers Eat. |
|
||||
| `async attack(target: CombatTarget, timeout: number = 5000): Promise<AttackResult>` | Attack an NPC or another player, walking to the target if needed. Takes either an entity (from `sdk.findNearbyNpc`/`sdk.findNearbyPlayer`) or a name/pattern, matched against NPCs first and players second - so pass the entity when a player shares a name with a monster. ```ts await bot.attack(/^chicken$/i); await bot.attack(sdk.findNearbyPlayer('Zezima')!); ``` PvP attacks are refused outside the wilderness and across too big a level gap; those come back as `reason: 'not_attackable'` with the server's own wording in `message`. |
|
||||
| `async attackPlayer(target: NearbyPlayer \| string \| RegExp, timeout: number = 5000): Promise<AttackResult>` | Attack another player (OPPLAYER2). Prefer {@link attack}, which also takes NPCs. |
|
||||
| `async castSpell(target: CombatTarget, spellComponent: number \| string, timeout: number = 3000): Promise<CastSpellResult>` | Cast a combat spell on an NPC or another player. The two are the same action to the server (OPNPCT vs OPPLAYERT), so this takes either: an entity from `sdk.findNearbyNpc`/`sdk.findNearbyPlayer`, or a name/pattern matched against NPCs first and players second. ```ts await bot.castSpell('goblin', Spells.WIND_STRIKE); await bot.castSpell(sdk.findNearbyPlayer('Zezima')!, Spells.FIRE_STRIKE); ``` Magic XP is the evidence of a cast landing, so a splash still counts as success with `hit: false`. |
|
||||
@@ -179,7 +179,7 @@
|
||||
| `getSkills(): SkillState[]` | Get all skills. |
|
||||
| `getInventoryItem(slot: number): InventoryItem \| null` | Get inventory item by slot number. |
|
||||
| `findInventoryItem(pattern: string \| RegExp): InventoryItem \| null` | Find inventory item by name pattern (shortest matching name wins). |
|
||||
| `getInventory(): InventoryItem[]` | Get all inventory items. |
|
||||
| `getInventory(): InventoryItem[]` | Get all inventory items. The array is COMPACTED (empty slots are omitted), so its index is not the inventory slot. `sendUseItem` / `sendUseItemOnItem` / `sendDropItem` take slot numbers: always pass `item.slot`, never the array index. |
|
||||
| `getEquipmentItem(slot: number): InventoryItem \| null` | Get equipment item by slot number. |
|
||||
| `findEquipmentItem(pattern: string \| RegExp): InventoryItem \| null` | Find equipment item by name pattern (shortest matching name wins). |
|
||||
| `getEquipment(): InventoryItem[]` | Get all equipped items. |
|
||||
|
||||
+8
-12
@@ -311,9 +311,7 @@ export class ActionHelpers {
|
||||
const startZ = startState.player.worldZ;
|
||||
const startTick = startState.tick;
|
||||
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(targetX - startX, 2) + Math.pow(targetZ - startZ, 2)
|
||||
);
|
||||
const distance = this.distance(startX, startZ, targetX, targetZ);
|
||||
const expectedTicks = Math.ceil(distance / TILES_PER_TICK);
|
||||
const maxTicks = Math.max(MIN_TICKS, Math.ceil(expectedTicks * 1.5));
|
||||
|
||||
@@ -336,7 +334,7 @@ export class ActionHelpers {
|
||||
const s = this.sdk.getState()?.player;
|
||||
const fx = s?.worldX ?? lastX;
|
||||
const fz = s?.worldZ ?? lastZ;
|
||||
const fd = Math.sqrt(Math.pow(targetX - fx, 2) + Math.pow(targetZ - fz, 2));
|
||||
const fd = this.distance(fx, fz, targetX, targetZ);
|
||||
done({ arrived: fd <= tolerance, stoppedMoving: true, x: fx, z: fz });
|
||||
}, SAFETY_MS);
|
||||
|
||||
@@ -348,9 +346,7 @@ export class ActionHelpers {
|
||||
const currentTick = state.tick;
|
||||
|
||||
// Check arrival
|
||||
const distToTarget = Math.sqrt(
|
||||
Math.pow(targetX - currentX, 2) + Math.pow(targetZ - currentZ, 2)
|
||||
);
|
||||
const distToTarget = this.distance(currentX, currentZ, targetX, targetZ);
|
||||
if (distToTarget <= tolerance) {
|
||||
done({ arrived: true, stoppedMoving: false, x: currentX, z: currentZ });
|
||||
return;
|
||||
@@ -400,9 +396,7 @@ export class ActionHelpers {
|
||||
const currentPos = { x: pos.worldX, z: pos.worldZ };
|
||||
|
||||
// Check if arrived
|
||||
const distToTarget = Math.sqrt(
|
||||
Math.pow(targetX - currentPos.x, 2) + Math.pow(targetZ - currentPos.z, 2)
|
||||
);
|
||||
const distToTarget = this.distance(currentPos.x, currentPos.z, targetX, targetZ);
|
||||
if (distToTarget <= tolerance) {
|
||||
return { status: 'arrived', pos: currentPos };
|
||||
}
|
||||
@@ -419,10 +413,12 @@ export class ActionHelpers {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance between two points.
|
||||
* Tile distance between two points - Chebyshev, the game's own metric.
|
||||
* Adjacency (including diagonal) is 1, so a tolerance of 1 accepts a
|
||||
* diagonally adjacent tile; the old Euclidean 1.41 rejected it.
|
||||
*/
|
||||
distance(x1: number, z1: number, x2: number, z2: number): number {
|
||||
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(z2 - z1, 2));
|
||||
return Math.max(Math.abs(x2 - x1), Math.abs(z2 - z1));
|
||||
}
|
||||
|
||||
// ============ Specific Door Opening ============
|
||||
|
||||
+120
-24
@@ -874,17 +874,20 @@ export class BotActions {
|
||||
const startTick = this.sdk.getState()?.tick || 0;
|
||||
const msgBaseline = this.helpers.getMessageTick();
|
||||
|
||||
// Success is *gaining* the item, tracked by its exact id. The ground
|
||||
// publication also disappears when another player wins the pickup, so
|
||||
// disappearance alone only stops the wait - see chopTree. Baseline
|
||||
// BEFORE dispatch: the dispatch round-trip can outlast a tick, and a
|
||||
// baseline taken afterwards already contained the item - fatal for
|
||||
// static map spawns, which never leave the ground mirror.
|
||||
const countBefore = countItems(this.sdk.getInventory(), item.id);
|
||||
|
||||
// Now send the pickup command
|
||||
const result = await this.sdk.sendPickup(item.x, item.z, item.id);
|
||||
if (!result.success) {
|
||||
return { success: false, message: result.message };
|
||||
}
|
||||
|
||||
// Success is *gaining* the item, tracked by its exact id. The ground
|
||||
// publication also disappears when another player wins the pickup, so
|
||||
// disappearance alone only stops the wait - see chopTree.
|
||||
const countBefore = countItems(this.sdk.getInventory(), item.id);
|
||||
|
||||
try {
|
||||
const finalState = await this.sdk.waitForCondition(state => {
|
||||
// Check for failure messages
|
||||
@@ -966,6 +969,14 @@ export class BotActions {
|
||||
if (!npcNow) {
|
||||
return { success: false, message: `${npc.name} no longer visible` };
|
||||
}
|
||||
// Talk-to needs adjacency. Dispatching it at an NPC the client cannot
|
||||
// route to (two tiles away behind a house wall) queues an interaction
|
||||
// the server keeps retrying, which blocks every later action. Refuse
|
||||
// up front instead - the name fallback in resolveNpc can pick such a
|
||||
// target when no reachable match exists.
|
||||
if (npcNow.reachable === false) {
|
||||
return { success: false, message: `${npc.name} is not reachable from here (wall or closed door in the way) - walk closer or open the door first` };
|
||||
}
|
||||
|
||||
const startTick = this.sdk.getState()?.tick || 0;
|
||||
const msgBaseline = this.helpers.getMessageTick();
|
||||
@@ -1212,7 +1223,14 @@ export class BotActions {
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.helpers.walkStepToward(wp.x, wp.z, 2, pos);
|
||||
// Intermediate waypoints only need to be passed near (2 tiles);
|
||||
// the final one must honor the caller's tolerance, or a
|
||||
// tolerance of 0/1 can never be met: the stepper reported
|
||||
// "arrived" up to 2 tiles short, the outer check kept failing,
|
||||
// and walkTo ended in "Stuck at" on open ground.
|
||||
const isFinalWp = wp === path.waypoints[path.waypoints.length - 1];
|
||||
const wpTolerance = isFinalWp ? Math.min(2, tolerance) : 2;
|
||||
const result = await this.helpers.walkStepToward(wp.x, wp.z, wpTolerance, pos);
|
||||
const deadMidStep = diedEnRoute();
|
||||
if (deadMidStep) return deadMidStep;
|
||||
if (distTo(result.pos) <= tolerance) return { success: true, message: 'Arrived' };
|
||||
@@ -1285,6 +1303,13 @@ export class BotActions {
|
||||
async openShop(target?: NearbyNpc | string | RegExp): Promise<ActionResult> {
|
||||
await this.dismissBlockingUI();
|
||||
|
||||
// A shop left open by a previous session never fires the open
|
||||
// event this method waits for - it hung until the caller's own timeout.
|
||||
const preState = this.sdk.getState();
|
||||
if (preState?.shop.isOpen) {
|
||||
return { success: true, message: `Shop already open: ${preState.shop.title}` };
|
||||
}
|
||||
|
||||
// Many shops are run by a named NPC (Bob's Brilliant Axes -> 'Bob'),
|
||||
// so with no explicit target, fall back from the /shop keeper/ name to
|
||||
// the nearest NPC that offers a Trade option. An explicit target that
|
||||
@@ -1808,7 +1833,30 @@ export class BotActions {
|
||||
|
||||
/** Close the bank interface. */
|
||||
async closeBank(timeout: number = 5000): Promise<ActionResult> {
|
||||
return this.closeInterface(timeout);
|
||||
// bank.isOpen (the client's component-link check that gates inventory
|
||||
// sends) can read true for a frame after interface.isOpen drops.
|
||||
// closeInterface only looks at the latter, so a retry on that frame
|
||||
// no-op'd with "already closed" while every item send was refused.
|
||||
const state = this.sdk.getState();
|
||||
if (!state?.interface?.isOpen && !state?.bank?.isOpen) {
|
||||
return { success: true, message: 'Bank already closed' };
|
||||
}
|
||||
|
||||
await this.sdk.sendCloseModal();
|
||||
const closed = (s: { interface?: { isOpen: boolean }; bank?: { isOpen: boolean } }) =>
|
||||
!s.interface?.isOpen && !s.bank?.isOpen;
|
||||
try {
|
||||
await this.sdk.waitForCondition(closed, timeout);
|
||||
return { success: true, message: 'Bank closed' };
|
||||
} catch {
|
||||
await this.sdk.sendCloseModal();
|
||||
await this.sdk.waitForTicks(1);
|
||||
const finalState = this.sdk.getState();
|
||||
if (finalState && closed(finalState)) {
|
||||
return { success: true, message: 'Bank closed (second attempt)' };
|
||||
}
|
||||
return { success: false, message: `Bank close timeout - interface.isOpen=${finalState?.interface?.isOpen}, bank.isOpen=${finalState?.bank?.isOpen}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1842,16 +1890,32 @@ export class BotActions {
|
||||
if (matchingIds.length > 1) {
|
||||
let totalDeposited = 0;
|
||||
const names: string[] = [];
|
||||
const failures: string[] = [];
|
||||
let lastFailure: BankDepositResult | null = null;
|
||||
for (const id of matchingIds) {
|
||||
const slotItem = this.sdk.getInventory().find(i => i.id === id);
|
||||
if (!slotItem) continue;
|
||||
const one = await this.depositItem(slotItem, -1);
|
||||
totalDeposited += one.amountDeposited ?? 0;
|
||||
names.push(slotItem.name);
|
||||
// One id failing (item_not_found under state lag, a
|
||||
// full bank slot) used to abort the loop and leave the
|
||||
// remaining matches in the bag. Keep going and report
|
||||
// the aggregate instead.
|
||||
if (!one.success) {
|
||||
return { ...one, amountDeposited: totalDeposited, message: `Deposited ${totalDeposited} across ${names.join(', ')}; stopped: ${one.message}` };
|
||||
failures.push(`${slotItem.name}: ${one.message}`);
|
||||
lastFailure = one;
|
||||
}
|
||||
}
|
||||
if (lastFailure) {
|
||||
return {
|
||||
...lastFailure,
|
||||
success: false,
|
||||
partial: totalDeposited > 0,
|
||||
amountDeposited: totalDeposited,
|
||||
message: `Deposited ${totalDeposited} across ${names.join(', ')}; failed: ${failures.join('; ')}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
message: `Deposited ${totalDeposited} across ${names.join(', ')}`,
|
||||
@@ -2720,13 +2784,18 @@ export class BotActions {
|
||||
return this.sdk.findEquipmentItem(pattern);
|
||||
}
|
||||
|
||||
/** Eat food to restore hitpoints. */
|
||||
async eatFood(target: InventoryItem | string | RegExp): Promise<EatResult> {
|
||||
/** Eat food to restore hitpoints. With no target, eats the first inventory item that offers Eat. */
|
||||
async eatFood(target?: InventoryItem | string | RegExp): Promise<EatResult> {
|
||||
await this.dismissBlockingUI();
|
||||
|
||||
const food = this.helpers.resolveInventoryItem(target, /./);
|
||||
// No target used to fall through to a match-anything pattern and
|
||||
// "eat" slot 0 - reporting "No eat option on Coins" while the bag
|
||||
// held salmon. Default to something that actually publishes Eat.
|
||||
const food = target === undefined
|
||||
? this.sdk.getInventory().find(i => i.optionsWithIndex.some(o => /^eat$/i.test(o.text))) ?? null
|
||||
: this.helpers.resolveInventoryItem(target, /./);
|
||||
if (!food) {
|
||||
return { success: false, hpGained: 0, message: `Food not found: ${target}` };
|
||||
return { success: false, hpGained: 0, message: target === undefined ? 'No edible item in inventory' : `Food not found: ${target}` };
|
||||
}
|
||||
|
||||
// Drinkable healing items (Beer, wine, potions) publish 'Drink', not 'Eat'.
|
||||
@@ -3470,7 +3539,9 @@ export class BotActions {
|
||||
return { success: false, message: 'No needle in inventory', reason: 'no_needle' };
|
||||
}
|
||||
|
||||
const leather = this.sdk.findInventoryItem(/^leather$/i);
|
||||
// Plain, hard, or any dragon leather colour; plain first when several are carried.
|
||||
const leather = this.sdk.findInventoryItem(/^leather$/i)
|
||||
?? this.sdk.findInventoryItem(/^(hard leather|(green|blue|red|black) dragon leather)$/i);
|
||||
if (!leather) {
|
||||
return { success: false, message: 'No leather in inventory', reason: 'no_leather' };
|
||||
}
|
||||
@@ -3563,8 +3634,12 @@ export class BotActions {
|
||||
|
||||
// Handle dialog
|
||||
if (state.dialog.isOpen) {
|
||||
const craftOption = state.dialog.options.find(o =>
|
||||
/glove|make|craft|leather|body|chaps/i.test(o.text)
|
||||
// Dragonhide dialogs list the product by colour ('Black body');
|
||||
// a requested product must win over the generic first match.
|
||||
const productRe = product ? new RegExp(product.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i') : null;
|
||||
const craftOption = (productRe && state.dialog.options.find(o => productRe.test(o.text)))
|
||||
|| state.dialog.options.find(o =>
|
||||
/glove|make|craft|leather|body|chaps|vamb/i.test(o.text)
|
||||
);
|
||||
if (craftOption) {
|
||||
await this.sdk.sendClickDialog(craftOption.index);
|
||||
@@ -3956,10 +4031,24 @@ export class BotActions {
|
||||
// Re-find the location after walking (it may have changed). Keep a RegExp
|
||||
// target as-is so its anchors survive; an entity target re-finds by
|
||||
// identity so same-named neighbours (Varrock cart crates) can't win.
|
||||
const locNow = target instanceof RegExp || typeof target === 'string'
|
||||
? this.helpers.resolveLocation(target, /./, optionFilter)
|
||||
: this.helpers.refindLocation(loc)
|
||||
?? this.helpers.resolveLocation(exactNamePattern(loc.name), /./, optionFilter);
|
||||
let locNow: NearbyLoc | null;
|
||||
if (target instanceof RegExp || typeof target === 'string') {
|
||||
locNow = this.helpers.resolveLocation(target, /./, optionFilter);
|
||||
} else {
|
||||
locNow = this.helpers.refindLocation(loc);
|
||||
if (!locNow) {
|
||||
// The passive nearbyLocs snapshot can lag a runtime loc_change
|
||||
// (kalphite "Tunnel entrance" 3830 vs 3831 with the rope), so a
|
||||
// caller-supplied loc from scanNearbyLocs may not refind. A
|
||||
// same-named neighbour only wins if it actually offers the
|
||||
// requested op; otherwise dispatch the caller's own object -
|
||||
// sending a different loc id and reporting success cost four
|
||||
// live runs to diagnose.
|
||||
const fallback = this.helpers.resolveLocation(exactNamePattern(loc.name), /./, optionFilter);
|
||||
const offersOp = fallback?.optionsWithIndex.some(o => o.opIndex === opIndex) ?? false;
|
||||
locNow = fallback && offersOp ? fallback : loc;
|
||||
}
|
||||
}
|
||||
if (!locNow) {
|
||||
return { success: false, message: `${loc.name} no longer visible`, reason: 'loc_not_found' };
|
||||
}
|
||||
@@ -4204,9 +4293,10 @@ export class BotActions {
|
||||
return { success: false, message: `NPC not found: ${target}`, reason: 'npc_not_found' };
|
||||
}
|
||||
|
||||
const pickOpt = npc.optionsWithIndex.find(o => /pickpocket/i.test(o.text));
|
||||
// Some NPCs publish their steal op as 'Steal-from' (Digsite workmen).
|
||||
const pickOpt = npc.optionsWithIndex.find(o => /pickpocket|steal/i.test(o.text));
|
||||
if (!pickOpt) {
|
||||
return { success: false, message: `No pickpocket option on ${npc.name}`, reason: 'no_pickpocket_option' };
|
||||
return { success: false, message: `No pickpocket/steal option on ${npc.name}`, reason: 'no_pickpocket_option' };
|
||||
}
|
||||
|
||||
const thievingBefore = this.sdk.getSkill('Thieving')?.experience || 0;
|
||||
@@ -4444,8 +4534,8 @@ export class BotActions {
|
||||
/**
|
||||
* Jewelry crafting interface (4161) component mapping.
|
||||
*
|
||||
* Layout: 3 columns (ring, necklace, amulet), each with 5 gem slots:
|
||||
* slot 0 = plain gold, 1 = sapphire, 2 = emerald, 3 = ruby, 4 = diamond
|
||||
* Layout: 3 columns (ring, necklace, amulet), each with 6 gem slots:
|
||||
* slot 0 = plain gold, 1 = sapphire, 2 = emerald, 3 = ruby, 4 = diamond, 5 = dragonstone
|
||||
*/
|
||||
private static readonly JEWELRY_COMPONENTS: Record<string, number> = {
|
||||
'ring': 4233,
|
||||
@@ -4460,6 +4550,7 @@ export class BotActions {
|
||||
'emerald': 2,
|
||||
'ruby': 3,
|
||||
'diamond': 4,
|
||||
'dragonstone': 5,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -4530,7 +4621,12 @@ export class BotActions {
|
||||
gem = gemItem ? gemItem.name.toLowerCase() : 'gold';
|
||||
}
|
||||
|
||||
const gemSlot = BotActions.JEWELRY_GEM_SLOTS[gem] ?? 0;
|
||||
const gemSlot = BotActions.JEWELRY_GEM_SLOTS[gem];
|
||||
if (gemSlot === undefined) {
|
||||
// An unknown gem string used to fall back to slot 0 and silently
|
||||
// craft plain gold.
|
||||
return { success: false, message: `Unknown gem '${gem}'. Use one of: ${Object.keys(BotActions.JEWELRY_GEM_SLOTS).join(', ')}`, reason: 'no_gem' };
|
||||
}
|
||||
|
||||
// Find furnace
|
||||
const furnace = this.sdk.findNearbyLoc(/furnace/i);
|
||||
|
||||
+2
-2
@@ -63,7 +63,7 @@ async function fetchStateViaHttp(
|
||||
console.error(` Nothing is logged into the game as '${username}', so there's no state to report.`);
|
||||
console.error(` This is also what you see after a server restart or when an idle client goes stale.`);
|
||||
console.error(` Fix: open the web client and log in as '${username}', then re-run.`);
|
||||
console.error(` (Scripts via sdk/runner.ts launch the client for you.)`);
|
||||
console.error(` Headless: cd server/webclient && bun src/lite/runner.ts '${username}' (scripts via sdk/runner.ts do NOT launch a client).`);
|
||||
if (body?.error && res.status !== 404) console.error(` ${body.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -106,7 +106,7 @@ async function connectAndAwaitState(sdk: BotSDK, gatewayUrl: string, username: s
|
||||
console.error(` Nothing is logged into the game as '${username}', so there's no state to report.`);
|
||||
console.error(` This is also what you see after a server restart or when an idle client goes stale.`);
|
||||
console.error(` Fix: open the web client and log in as '${username}', then re-run.`);
|
||||
console.error(` (Scripts via sdk/runner.ts launch the client for you.)`);
|
||||
console.error(` Headless: cd server/webclient && bun src/lite/runner.ts '${username}' (scripts via sdk/runner.ts do NOT launch a client).`);
|
||||
} else {
|
||||
console.error(`Error: No state received for '${username}'`);
|
||||
console.error(` Lost the gateway connection before any state arrived.`);
|
||||
|
||||
+10
-2
@@ -823,7 +823,13 @@ export class BotSDK {
|
||||
return shortestNameMatch(this.state.inventory, pattern);
|
||||
}
|
||||
|
||||
/** Get all inventory items. */
|
||||
/**
|
||||
* Get all inventory items.
|
||||
*
|
||||
* The array is COMPACTED (empty slots are omitted), so its index is not
|
||||
* the inventory slot. `sendUseItem` / `sendUseItemOnItem` / `sendDropItem`
|
||||
* take slot numbers: always pass `item.slot`, never the array index.
|
||||
*/
|
||||
getInventory(): InventoryItem[] {
|
||||
return this.state?.inventory || [];
|
||||
}
|
||||
@@ -1909,7 +1915,9 @@ export class BotSDK {
|
||||
if (message.type === 'sdk_state' && message.state) {
|
||||
// Filter out player chat messages unless showChat is enabled. Player
|
||||
// chat = public (types 1/2) and private (3/6/7); system/game messages
|
||||
// (level-ups, combat, examines) always pass through.
|
||||
// (level-ups, combat, examines) always pass through. Note this also
|
||||
// hides DMs from getNewChat/waitForChat - keep showChat on (the
|
||||
// default) for any bot that talks to other bots.
|
||||
if (this.config.showChat === false && message.state.gameMessages) {
|
||||
message.state.gameMessages = message.state.gameMessages.filter(
|
||||
msg => !isPlayerChat(msg.type)
|
||||
|
||||
@@ -187,9 +187,13 @@ describe('BotSDK chat surface (via handleMessage)', () => {
|
||||
expect(sdk.getChatFrom('').every(m => m.sender !== '')).toBe(true);
|
||||
});
|
||||
|
||||
test('showChat:false excludes player chat from history', () => {
|
||||
test('showChat:false excludes player chat (public and private) from history', () => {
|
||||
const sdk = new BotSDK({ botUsername: 'test', showChat: false });
|
||||
pushState(sdk, [msg(10, 'hello'), msg(11, 'sys', { type: 0, sender: '' })]);
|
||||
pushState(sdk, [
|
||||
msg(10, 'hello'),
|
||||
msg(11, 'sys', { type: 0, sender: '' }),
|
||||
msg(12, 'psst', { type: 3, sender: 'friend' }),
|
||||
]);
|
||||
|
||||
const retained = sdk.getChat({ limit: 0, types: [0, 1, 2, 3, 6, 7] });
|
||||
expect(retained.every(m => m.type === 0)).toBe(true);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* walkTo must honor a caller tolerance below 2 on the final waypoint, and
|
||||
* judge tolerance in tiles (Chebyshev), not Euclidean distance.
|
||||
* Bug-report harvest 2026-09-21 (openGE/openBank/staircase "Stuck at" family).
|
||||
*/
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { BotSDK } from '../index';
|
||||
import { BotActions } from '../actions';
|
||||
|
||||
function makeBot(start: { x: number; z: number }) {
|
||||
const sdk = new BotSDK({ botUsername: 'test' });
|
||||
let pos = { ...start };
|
||||
const state = () => ({
|
||||
player: { worldX: pos.x, worldZ: pos.z, x: pos.x, z: pos.z, level: 0, lifeId: 1, isDead: false },
|
||||
tick: 1, gameMessages: [], nearbyLocs: [], nearbyNpcs: [], inventory: [],
|
||||
dialog: { isOpen: false, options: [], isWaiting: false }, interface: { isOpen: false },
|
||||
});
|
||||
(sdk as any).getState = state;
|
||||
const bot = new BotActions(sdk);
|
||||
(bot as any).dismissBlockingUI = async () => {};
|
||||
const steps: number[] = [];
|
||||
(bot as any).helpers.walkStepToward = async (tx: number, tz: number, tolerance: number) => {
|
||||
steps.push(tolerance);
|
||||
// The stepper stops as soon as it is within its tolerance, like the real one.
|
||||
const dx = Math.sign(tx - pos.x), dz = Math.sign(tz - pos.z);
|
||||
while (Math.max(Math.abs(tx - pos.x), Math.abs(tz - pos.z)) > tolerance) pos = { x: pos.x + dx, z: pos.z + dz };
|
||||
return { status: 'arrived' as const, pos: { ...pos } };
|
||||
};
|
||||
return { sdk, bot, steps, pos: () => pos };
|
||||
}
|
||||
|
||||
describe('walkTo tolerance', () => {
|
||||
test('tolerance 0 reaches the exact tile', async () => {
|
||||
const { sdk, bot, steps, pos } = makeBot({ x: 3200, z: 3200 });
|
||||
(sdk as any).sendFindPath = async () => ({ success: true, reachedDestination: true, waypoints: [{ x: 3205, z: 3200 }, { x: 3210, z: 3200 }] });
|
||||
const r = await bot.walkTo(3210, 3200, 0);
|
||||
expect(r.success).toBe(true);
|
||||
expect(pos()).toEqual({ x: 3210, z: 3200 });
|
||||
// Intermediate waypoint keeps the loose 2-tile pass; the final one uses the caller's 0.
|
||||
expect(steps).toEqual([2, 0]);
|
||||
});
|
||||
|
||||
test('tolerance 1 accepts a diagonally adjacent tile', async () => {
|
||||
const { sdk, bot } = makeBot({ x: 3201, z: 3201 });
|
||||
(sdk as any).sendFindPath = async () => { throw new Error('should not path: already within 1 tile (diagonal)'); };
|
||||
const r = await bot.walkTo(3200, 3200, 1);
|
||||
expect(r.success).toBe(true);
|
||||
expect(r.message).toBe('Already at destination');
|
||||
});
|
||||
});
|
||||
@@ -20,18 +20,18 @@ if (p_finduid(uid) = true) {
|
||||
~mesbox("@blu@Engine commands|@whi@::seq@bla@/@whi@::anim@bla@ - Play a seq on your player|@whi@::spotanim@bla@ - Play a spotanim on your player|@whi@::loc@bla@ - Spawn a loc under your player|@whi@::npc@bla@ - Spawn an NPC under your player");
|
||||
|
||||
[label,account_commands]
|
||||
~mesbox("@blu@Account commands|@whi@::reset@bla@ - Reset your progress|@whi@::minme@bla@ - Set all your stats to level 1|@whi@::maxme@bla@ - Set all your stats to level 99|@whi@::setstat@bla@ - Set a stat to a specific level");
|
||||
~mesbox("@blu@Account commands|@whi@::addxp@bla@ - Add XP to a skill|@whi@::setxp@bla@ - Set your XP in a skill");
|
||||
~mesbox("@blu@Account commands|@whi@::1hp@bla@ - Set your health to 1|@whi@::foodbank@bla@ - Fill your bank with food|@whi@::energy@bla@ - Restore your run energy");
|
||||
~mesbox("@blu@Account commands|@whi@::~reset@bla@ - Reset your progress|@whi@::minme@bla@ - Set all your stats to level 1|@whi@::~maxme@bla@ - Set all your stats to level 99|@whi@::setstat@bla@ - Set a stat to a specific level");
|
||||
~mesbox("@blu@Account commands|@whi@::~addxp@bla@ - Add XP to a skill|@whi@::setxp@bla@ - Set your XP in a skill");
|
||||
~mesbox("@blu@Account commands|@whi@::~1hp@bla@ - Set your health to 1|@whi@::~foodbank@bla@ - Fill your bank with food|@whi@::~energy@bla@ - Restore your run energy");
|
||||
|
||||
[label,item_commands]
|
||||
~mesbox("@blu@Item commands|@whi@::bank@bla@ - Open your bank anywhere|@whi@::bank_preset@bla@ - Fill your bank with common items|@whi@::clearinv@bla@ - Clear a named inventory|@whi@::give@bla@ - Give yourself any item");
|
||||
~mesbox("@blu@Item commands|@whi@::fmtest@bla@ - Give yourself items to test firemaking|@whi@::fmbank@bla@ - Fill your bank with firemaking items");
|
||||
~mesbox("@blu@Item commands|@whi@::~bank@bla@ - Open your bank anywhere|@whi@::~bank_preset@bla@ - Fill your bank with common items|@whi@::~clearinv@bla@ - Clear a named inventory|@whi@::give@bla@ - Give yourself any item");
|
||||
~mesbox("@blu@Item commands|@whi@::fmtest@bla@ - Give yourself items to test firemaking|@whi@::~fmbank@bla@ - Fill your bank with firemaking items");
|
||||
~mesbox("@blu@Item commands|@whi@::fishtest@bla@ - Give yourself items to test fishing");
|
||||
~mesbox("@blu@Item commands|@whi@::giverunes@bla@ - Give yourself items to test magic|@whi@::magicbank@bla@ - Fill your bank with magic items");
|
||||
~mesbox("@blu@Item commands|@whi@::~giverunes@bla@ - Give yourself items to test magic|@whi@::~magicbank@bla@ - Fill your bank with magic items");
|
||||
|
||||
[label,teleport_commands]
|
||||
~mesbox("@blu@Teleport commands|@whi@::tele@bla@ - Teleport to specific coordinates|@whi@::tele up@bla@ - Teleport up a level|@whi@::tele down@bla@ - Teleport down a level|@whi@::north@bla@/@whi@::east@bla@/@whi@::south@bla@/@whi@::west@bla@ - Teleport 1 tile in a direction");
|
||||
~mesbox("@blu@Teleport commands|@whi@::home@bla@ - Teleport to Lumbridge|@whi@::varrock@bla@ - Teleport to Varrock|@whi@::falador@bla@ - Teleport to Falador|@whi@::draynor@bla@ - Teleport to Draynor Village");
|
||||
~mesbox("@blu@Teleport commands|@whi@::portsarim@bla@ - Teleport to Port Sarim|@whi@::rimmington@bla@ - Teleport to Rimmington|@whi@::alkharid@bla@ - Teleport to Al Kharid|@whi@::seers@bla@ - Teleport to Seers Village");
|
||||
~mesbox("@blu@Teleport commands|@whi@::giants@bla@ - Teleport to Giants in Edgeville Dungeon|@whi@::brimhaven@bla@ - Teleport to Brimhaven|@whi@::entrana@bla@ - Teleport to Entrana");
|
||||
~mesbox("@blu@Teleport commands|@whi@::tele@bla@ - Teleport to specific coordinates|@whi@::tele up@bla@ - Teleport up a level|@whi@::tele down@bla@ - Teleport down a level|@whi@::~north@bla@/@whi@::~east@bla@/@whi@::~south@bla@/@whi@::~west@bla@ - Teleport 1 tile in a direction");
|
||||
~mesbox("@blu@Teleport commands|@whi@::~home@bla@ - Teleport to Lumbridge|@whi@::~varrock@bla@ - Teleport to Varrock|@whi@::~falador@bla@ - Teleport to Falador|@whi@::~draynor@bla@ - Teleport to Draynor Village");
|
||||
~mesbox("@blu@Teleport commands|@whi@::~portsarim@bla@ - Teleport to Port Sarim|@whi@::~rimmington@bla@ - Teleport to Rimmington|@whi@::~alkharid@bla@ - Teleport to Al Kharid|@whi@::~seers@bla@ - Teleport to Seers Village");
|
||||
~mesbox("@blu@Teleport commands|@whi@::~giants@bla@ - Teleport to Giants in Edgeville Dungeon|@whi@::~brimhaven@bla@ - Teleport to Brimhaven|@whi@::~entrana@bla@ - Teleport to Entrana");
|
||||
Reference in new issue
Block a user