pull insteadof push neerby locs and stuff

This commit is contained in:
max bittker committed 2026-01-28 19:29:20 -05:00
1 parent 9c16e927ed
commit 347ea327aa
9 files changed
+159 -98

No files matched your search

+24 -13
View File
@@ -176,7 +176,7 @@ export class BotActions {
// ============ Porcelain: Smart Actions ============
async openDoor(target?: NearbyLoc | string | RegExp): Promise<OpenDoorResult> {
const door = this.resolveLocation(target, /door|gate/i);
const door = await this.resolveLocation(target, /door|gate/i);
if (!door) {
return { success: false, message: 'No door found nearby', reason: 'door_not_found' };
}
@@ -187,7 +187,8 @@ export class BotActions {
if (closeOpt) {
return { success: true, message: `${door.name} is already open`, reason: 'already_open', door };
}
return { success: false, message: `${door.name} has no Open option (options: ${door.options.join(', ')})`, reason: 'no_open_option', door };
const optTexts = door.optionsWithIndex.map(o => o.text);
return { success: false, message: `${door.name} has no Open option (options: ${optTexts.join(', ')})`, reason: 'no_open_option', door };
}
if (door.distance > 2) {
@@ -196,7 +197,9 @@ export class BotActions {
return { success: false, message: `Could not walk to ${door.name}: ${walkResult.message}`, reason: 'walk_failed', door };
}
const doorsNow = this.sdk.getNearbyLocs().filter(l =>
// Re-scan after walking
const locsNow = await this.sdk.scanNearbyLocs();
const doorsNow = locsNow.filter(l =>
l.x === door.x && l.z === door.z && /door|gate/i.test(l.name)
);
const refreshedDoor = doorsNow[0];
@@ -278,7 +281,7 @@ export class BotActions {
async chopTree(target?: NearbyLoc | string | RegExp): Promise<ChopTreeResult> {
await this.dismissBlockingUI();
const tree = this.resolveLocation(target, /^tree$/i);
const tree = await this.resolveLocation(target, /^tree$/i);
if (!tree) {
return { success: false, message: 'No tree found' };
}
@@ -368,7 +371,7 @@ export class BotActions {
}
async pickupItem(target: GroundItem | string | RegExp): Promise<PickupResult> {
const item = this.resolveGroundItem(target);
const item = await this.resolveGroundItem(target);
if (!item) {
return { success: false, message: 'Item not found on ground', reason: 'item_not_found' };
}
@@ -801,7 +804,7 @@ export class BotActions {
await this.dismissBlockingUI();
const banker = this.sdk.findNearbyNpc(/banker/i);
const bankBooth = this.sdk.findNearbyLoc(/bank booth|bank chest/i);
const bankBooth = await this.sdk.scanFindNearbyLoc(/bank booth|bank chest/i);
let interactSuccess = false;
@@ -1691,7 +1694,7 @@ export class BotActions {
}
// Find anvil
const anvil = this.sdk.findNearbyLoc(/anvil/i);
const anvil = await this.sdk.scanFindNearbyLoc(/anvil/i);
if (!anvil) {
return { success: false, message: 'No anvil nearby', reason: 'no_anvil' };
}
@@ -1816,17 +1819,21 @@ export class BotActions {
// ============ Resolution Helpers ============
private resolveLocation(
/**
* Resolve a location target by scanning if needed.
* Now async because location scanning is on-demand.
*/
private async resolveLocation(
target: NearbyLoc | string | RegExp | undefined,
defaultPattern: RegExp
): NearbyLoc | null {
): Promise<NearbyLoc | null> {
if (!target) {
return this.sdk.findNearbyLoc(defaultPattern);
return this.sdk.scanFindNearbyLoc(defaultPattern);
}
if (typeof target === 'object' && 'x' in target) {
return target;
}
return this.sdk.findNearbyLoc(target);
return this.sdk.scanFindNearbyLoc(target);
}
private resolveInventoryItem(
@@ -1842,11 +1849,15 @@ export class BotActions {
return this.sdk.findInventoryItem(target);
}
private resolveGroundItem(target: GroundItem | string | RegExp): GroundItem | null {
/**
* Resolve a ground item target by scanning if needed.
* Now async because ground item scanning is on-demand.
*/
private async resolveGroundItem(target: GroundItem | string | RegExp): Promise<GroundItem | null> {
if (typeof target === 'object' && 'x' in target) {
return target;
}
return this.sdk.findGroundItem(target);
return this.sdk.scanFindGroundItem(target);
}
private resolveNpc(target: NearbyNpc | string | RegExp): NearbyNpc | null {
+60
View File
@@ -360,6 +360,66 @@ export class BotSDK {
return this.state?.dialog || null;
}
// ============ On-Demand Scanning ============
// These methods scan the environment on-demand rather than relying on pushed state
// Use these for expensive scans of nearby locations and ground items
/**
* Scan for nearby locations (trees, rocks, doors, etc.) on-demand.
* This is more efficient than constantly pushing this data in state updates.
* @param radius - Scan radius in tiles (default 15)
* @returns Array of nearby locations sorted by distance
*/
async scanNearbyLocs(radius?: number): Promise<NearbyLoc[]> {
const result = await this.sendAction({ type: 'scanNearbyLocs', radius, reason: 'SDK' });
if (result.success && result.data) {
return result.data as NearbyLoc[];
}
return [];
}
/**
* Scan for ground items on-demand.
* This is more efficient than constantly pushing this data in state updates.
* @param radius - Scan radius in tiles (default 15)
* @returns Array of ground items sorted by distance
*/
async scanGroundItems(radius?: number): Promise<GroundItem[]> {
const result = await this.sendAction({ type: 'scanGroundItems', radius, reason: 'SDK' });
if (result.success && result.data) {
return result.data as GroundItem[];
}
return [];
}
/**
* Find a nearby location by name pattern (on-demand scan).
* @param pattern - String or RegExp to match location name
* @param radius - Scan radius in tiles (default 15)
* @returns First matching location or null
*/
async scanFindNearbyLoc(pattern: string | RegExp, radius?: number): Promise<NearbyLoc | null> {
const locs = await this.scanNearbyLocs(radius);
const regex = typeof pattern === 'string'
? new RegExp(pattern, 'i')
: pattern;
return locs.find(l => regex.test(l.name)) || null;
}
/**
* Find a ground item by name pattern (on-demand scan).
* @param pattern - String or RegExp to match item name
* @param radius - Scan radius in tiles (default 15)
* @returns First matching item or null
*/
async scanFindGroundItem(pattern: string | RegExp, radius?: number): Promise<GroundItem | null> {
const items = await this.scanGroundItems(radius);
const regex = typeof pattern === 'string'
? new RegExp(pattern, 'i')
: pattern;
return items.find(i => regex.test(i.name)) || null;
}
// ============ State Subscriptions ============
onStateUpdate(listener: (state: BotWorldState) => void): () => void {
+6 -4
View File
@@ -49,7 +49,6 @@ export interface InventoryItem {
id: number;
name: string;
count: number;
options: string[];
optionsWithIndex: InventoryItemOption[];
}
@@ -73,7 +72,6 @@ export interface NearbyNpc {
combatCycle: number;
animId: number;
spotanimId: number;
options: string[];
optionsWithIndex: NpcOption[];
}
@@ -106,7 +104,6 @@ export interface NearbyLoc {
x: number;
z: number;
distance: number;
options: string[];
optionsWithIndex: LocOption[];
}
@@ -249,11 +246,16 @@ export type BotAction =
| { type: 'spellOnItem'; slot: number; spellComponent: number; reason: string }
| { type: 'setTab'; tabIndex: number; reason: string }
| { type: 'bankDeposit'; slot: number; amount: number; reason: string }
| { type: 'bankWithdraw'; slot: number; amount: number; reason: string };
| { type: 'bankWithdraw'; slot: number; amount: number; reason: string }
// On-demand scanning (returns data in action result)
| { type: 'scanNearbyLocs'; radius?: number; reason: string }
| { type: 'scanGroundItems'; radius?: number; reason: string };
export interface ActionResult {
success: boolean;
message: string;
/** Optional data payload (used by scan actions to return results) */
data?: any;
}
// ============ SDK Config ============
+33 -1
View File
@@ -2,22 +2,34 @@
// Maps BotAction types to actual game client operations
import type { Client } from '#/client/Client.js';
import type { BotAction } from './types.js';
import type { BotAction, NearbyLoc, GroundItem } from './types.js';
export interface ActionResult {
success: boolean;
message: string;
data?: any; // Optional data payload for scan results
}
export type ActionResultOrPromise = ActionResult | Promise<ActionResult>;
// Interface for on-demand scanning (provided by StateCollector)
export interface ScanProvider {
scanNearbyLocs(radius?: number): NearbyLoc[];
scanGroundItems(radius?: number): GroundItem[];
}
export class ActionExecutor {
private client: Client;
private scanProvider: ScanProvider | null = null;
constructor(client: Client) {
this.client = client;
}
setScanProvider(provider: ScanProvider): void {
this.scanProvider = provider;
}
execute(action: BotAction): ActionResultOrPromise {
try {
switch (action.type) {
@@ -214,6 +226,26 @@ export class ActionExecutor {
'Failed to send message'
);
case 'scanNearbyLocs':
if (!this.scanProvider) {
return { success: false, message: 'No scan provider available' };
}
return {
success: true,
message: `Scanned nearby locations`,
data: this.scanProvider.scanNearbyLocs(action.radius)
};
case 'scanGroundItems':
if (!this.scanProvider) {
return { success: false, message: 'No scan provider available' };
}
return {
success: true,
message: `Scanned ground items`,
data: this.scanProvider.scanGroundItems(action.radius)
};
default:
return { success: false, message: `Unknown action type: ${(action as any).type}` };
}
+4 -1
View File
@@ -41,6 +41,9 @@ export class BotOverlay implements GatewayMessageHandler {
onPacketLogToggle: () => {}
});
// Wire scan provider so ActionExecutor can trigger on-demand scans
this.executor.setScanProvider(this.collector);
// Connect to gateway
this.gateway.connect();
@@ -205,7 +208,7 @@ export class BotOverlay implements GatewayMessageHandler {
this.gateway.sendState(state, formattedState);
}
private sendActionResult(result: { success: boolean; message: string }): void {
private sendActionResult(result: { success: boolean; message: string; data?: any }): void {
if (this.currentActionId) {
this.gateway.sendActionResult(this.currentActionId, result);
this.ui.logAction(result.success ? 'success' : 'failed', result.message);
+1 -1
View File
@@ -111,7 +111,7 @@ export class GatewayConnection {
});
}
sendActionResult(actionId: string, result: { success: boolean; message: string }): void {
sendActionResult(actionId: string, result: { success: boolean; message: string; data?: any }): void {
this.send({
type: 'actionResult',
actionId,
+12 -27
View File
@@ -39,8 +39,9 @@ import {
type DialogState,
type InterfaceState
} from './types.js';
import type { ScanProvider } from './ActionExecutor.js';
export class BotStateCollector {
export class BotStateCollector implements ScanProvider {
private client: Client;
// Combat event tracking
private combatEvents: CombatEvent[] = [];
@@ -69,8 +70,8 @@ export class BotStateCollector {
combatStyle: this.collectCombatStyle(),
nearbyNpcs: this.collectNearbyNpcs(),
nearbyPlayers: this.collectNearbyPlayers(),
nearbyLocs: this.collectNearbyLocs(),
groundItems: this.collectGroundItems(),
nearbyLocs: [], // On-demand via scanNearbyLocs()
groundItems: [], // On-demand via scanGroundItems()
gameMessages: this.collectGameMessages(),
menuActions: this.collectMenuActions(),
shop: this.collectShopState(),
@@ -398,7 +399,6 @@ export class BotStateCollector {
if (objId > 0) {
let name = 'Unknown';
const options: string[] = [];
const optionsWithIndex: InventoryItemOption[] = [];
try {
@@ -410,7 +410,6 @@ export class BotStateCollector {
for (let opIdx = 0; opIdx < obj.iop.length; opIdx++) {
const op = obj.iop[opIdx];
if (op) {
options.push(op);
optionsWithIndex.push({ text: op, opIndex: opIdx + 1 }); // opIndex is 1-based
}
}
@@ -422,7 +421,6 @@ export class BotStateCollector {
id: objId - 1,
name,
count,
options,
optionsWithIndex
});
}
@@ -454,13 +452,11 @@ export class BotStateCollector {
const dz = (npc.z || 0) - (player.z || 0);
const distance = Math.max(Math.abs(dx), Math.abs(dz)) >> 7;
const options: string[] = [];
const optionsWithIndex: NpcOption[] = [];
if (npcType.op) {
for (let opIdx = 0; opIdx < npcType.op.length; opIdx++) {
const op = npcType.op[opIdx];
if (op) {
options.push(op);
optionsWithIndex.push({ text: op, opIndex: opIdx + 1 }); // opIndex is 1-based
}
}
@@ -492,7 +488,6 @@ export class BotStateCollector {
combatCycle,
animId: npc.primarySeqId ?? -1,
spotanimId: npc.spotanimId ?? -1,
options,
optionsWithIndex
});
}
@@ -538,7 +533,8 @@ export class BotStateCollector {
return players;
}
private collectNearbyLocs(): NearbyLoc[] {
// On-demand scanning for nearby locations (implements ScanProvider)
scanNearbyLocs(radius?: number): NearbyLoc[] {
const c = this.client as any;
const locs: NearbyLoc[] = [];
const player = c.localPlayer;
@@ -555,8 +551,8 @@ export class BotStateCollector {
// Track seen locations to avoid duplicates (key = `${id}_${x}_${z}`)
const seen = new Set<string>();
// Scan nearby tiles
const scanRadius = 15;
// Scan nearby tiles (default 15 tile radius)
const scanRadius = radius ?? 15;
for (let dx = -scanRadius; dx <= scanRadius; dx++) {
for (let dz = -scanRadius; dz <= scanRadius; dz++) {
const tileX = playerTileX + dx;
@@ -577,13 +573,11 @@ export class BotStateCollector {
const locType = LocType.get(locId);
// Include locations that have a name (skip unnamed scenery)
if (locType.name) {
const options: string[] = [];
const optionsWithIndex: LocOption[] = [];
if (locType.op) {
for (let i = 0; i < locType.op.length; i++) {
const op = locType.op[i];
if (op) {
options.push(op);
optionsWithIndex.push({ text: op, opIndex: i + 1 }); // opIndex is 1-based
}
}
@@ -594,7 +588,6 @@ export class BotStateCollector {
x: baseX + tileX,
z: baseZ + tileZ,
distance,
options,
optionsWithIndex
});
}
@@ -612,13 +605,11 @@ export class BotStateCollector {
try {
const locType = LocType.get(wallId);
if (locType.name) {
const options: string[] = [];
const optionsWithIndex: LocOption[] = [];
if (locType.op) {
for (let i = 0; i < locType.op.length; i++) {
const op = locType.op[i];
if (op) {
options.push(op);
optionsWithIndex.push({ text: op, opIndex: i + 1 });
}
}
@@ -629,7 +620,6 @@ export class BotStateCollector {
x: baseX + tileX,
z: baseZ + tileZ,
distance,
options,
optionsWithIndex
});
}
@@ -647,13 +637,11 @@ export class BotStateCollector {
try {
const locType = LocType.get(decorId);
if (locType.name) {
const options: string[] = [];
const optionsWithIndex: LocOption[] = [];
if (locType.op) {
for (let i = 0; i < locType.op.length; i++) {
const op = locType.op[i];
if (op) {
options.push(op);
optionsWithIndex.push({ text: op, opIndex: i + 1 });
}
}
@@ -664,7 +652,6 @@ export class BotStateCollector {
x: baseX + tileX,
z: baseZ + tileZ,
distance,
options,
optionsWithIndex
});
}
@@ -682,13 +669,11 @@ export class BotStateCollector {
try {
const locType = LocType.get(groundDecorId);
if (locType.name) {
const options: string[] = [];
const optionsWithIndex: LocOption[] = [];
if (locType.op) {
for (let i = 0; i < locType.op.length; i++) {
const op = locType.op[i];
if (op) {
options.push(op);
optionsWithIndex.push({ text: op, opIndex: i + 1 });
}
}
@@ -699,7 +684,6 @@ export class BotStateCollector {
x: baseX + tileX,
z: baseZ + tileZ,
distance,
options,
optionsWithIndex
});
}
@@ -714,7 +698,8 @@ export class BotStateCollector {
return locs;
}
private collectGroundItems(): GroundItem[] {
// On-demand scanning for ground items (implements ScanProvider)
scanGroundItems(radius?: number): GroundItem[] {
const c = this.client as any;
const items: GroundItem[] = [];
const player = c.localPlayer;
@@ -730,8 +715,8 @@ export class BotStateCollector {
const baseX = c.sceneBaseTileX || 0;
const baseZ = c.sceneBaseTileZ || 0;
// Scan nearby tiles (within reasonable range)
const scanRadius = 15;
// Scan nearby tiles (default 15 tile radius)
const scanRadius = radius ?? 15;
for (let dx = -scanRadius; dx <= scanRadius; dx++) {
for (let dz = -scanRadius; dz <= scanRadius; dz++) {
const tileX = playerTileX + dx;
+12 -44
View File
@@ -57,7 +57,8 @@ export function formatBotState(state: BotState): string {
const npc = state.nearbyNpcs[i];
const lvlStr = npc.combatLevel > 0 ? ` (Lvl ${npc.combatLevel})` : '';
const hpStr = npc.maxHp > 0 ? ` HP: ${npc.hp}/${npc.maxHp}` : '';
const opStr = npc.options.length > 0 ? ` [${npc.options.join(', ')}]` : '';
const opts = npc.optionsWithIndex.map(o => o.text);
const opStr = opts.length > 0 ? ` [${opts.join(', ')}]` : '';
lines.push(`${npc.name}${lvlStr}${hpStr} - ${npc.distance} tiles${opStr}`);
}
if (state.nearbyNpcs.length > 5) {
@@ -81,35 +82,14 @@ export function formatBotState(state: BotState): string {
}
lines.push('');
// Nearby Locations (interactable objects)
// Nearby Locations (interactable objects) - now on-demand via SDK scan methods
lines.push('--- NEARBY OBJECTS ---');
if (state.nearbyLocs.length === 0) {
lines.push('None');
} else {
for (let i = 0; i < Math.min(8, state.nearbyLocs.length); i++) {
const loc = state.nearbyLocs[i];
const opStr = loc.options.length > 0 ? ` [${loc.options.join(', ')}]` : '';
lines.push(`${loc.name} at (${loc.x}, ${loc.z}) - ${loc.distance} tiles${opStr}`);
}
if (state.nearbyLocs.length > 8) {
lines.push(`... and ${state.nearbyLocs.length - 8} more`);
}
}
lines.push('(Use sdk.scanNearbyLocs() for on-demand scan)');
lines.push('');
// Ground items
// Ground items - now on-demand via SDK scan methods
lines.push('--- GROUND ITEMS ---');
if (state.groundItems.length === 0) {
lines.push('None');
} else {
for (let i = 0; i < Math.min(5, state.groundItems.length); i++) {
const item = state.groundItems[i];
lines.push(`${item.name} x${item.count} - ${item.distance} tiles`);
}
if (state.groundItems.length > 5) {
lines.push(`... and ${state.groundItems.length - 5} more`);
}
}
lines.push('(Use sdk.scanGroundItems() for on-demand scan)');
lines.push('');
// Recent game messages
@@ -265,20 +245,14 @@ export function formatWorldStateForAgent(state: BotWorldState, goal: string): st
for (const npc of state.nearbyNpcs.slice(0, 8)) {
const lvl = npc.combatLevel > 0 ? ` (Lvl ${npc.combatLevel})` : '';
const hp = npc.maxHp > 0 ? ` HP: ${npc.hp}/${npc.maxHp}` : '';
const opts = npc.options.length > 0 ? ` [${npc.options.join(', ')}]` : '';
const optTexts = npc.optionsWithIndex.map(o => o.text);
const opts = optTexts.length > 0 ? ` [${optTexts.join(', ')}]` : '';
lines.push(`- ${npc.name}${lvl}${hp} - ${npc.distance} tiles away, index: ${npc.index}${opts}`);
}
}
// Nearby Objects (trees, rocks, doors, etc.)
if (state.nearbyLocs && state.nearbyLocs.length > 0) {
lines.push('');
lines.push('### Nearby Objects');
for (const loc of state.nearbyLocs.slice(0, 10)) {
const opts = loc.options.length > 0 ? ` [${loc.options.join(', ')}]` : '';
lines.push(`- ${loc.name} at (${loc.x}, ${loc.z}) - ${loc.distance} tiles, id: ${loc.id}${opts}`);
}
}
// Nearby Objects note - scanning is now on-demand
// (Removed auto-display since nearbyLocs is empty until scanned)
// Inventory summary
if (state.inventory.length > 0) {
@@ -293,14 +267,8 @@ export function formatWorldStateForAgent(state: BotWorldState, goal: string): st
}
}
// Ground items
if (state.groundItems.length > 0) {
lines.push('');
lines.push('### Ground Items Nearby');
for (const item of state.groundItems.slice(0, 5)) {
lines.push(`- ${item.name} x${item.count} at (${item.x}, ${item.z}), ${item.distance} tiles`);
}
}
// Ground items note - scanning is now on-demand
// (Removed auto-display since groundItems is empty until scanned)
// Recent game messages
if (state.gameMessages && state.gameMessages.length > 0) {
+7 -7
View File
@@ -37,8 +37,7 @@ export interface InventoryItem {
id: number;
name: string;
count: number;
options: string[]; // Display-only list of option texts
optionsWithIndex: InventoryItemOption[]; // Options with their actual op index
optionsWithIndex: InventoryItemOption[]; // Options with op index (use .map(o => o.text) for display)
}
export interface NpcOption {
@@ -69,8 +68,7 @@ export interface NearbyNpc {
animId: number;
/** Current spot animation ID (-1 = none) */
spotanimId: number;
options: string[]; // Display-only list of option texts
optionsWithIndex: NpcOption[]; // Options with their actual op index
optionsWithIndex: NpcOption[]; // Options with op index (use .map(o => o.text) for display)
}
export interface NearbyPlayer {
@@ -102,8 +100,7 @@ export interface NearbyLoc {
x: number;
z: number;
distance: number;
options: string[]; // Display-only list of option texts
optionsWithIndex: LocOption[]; // Options with their actual op index
optionsWithIndex: LocOption[]; // Options with op index (use .map(o => o.text) for display)
}
export interface MenuAction {
@@ -295,4 +292,7 @@ export type BotAction =
| { type: 'setTab'; tabIndex: number; reason: string }
| { type: 'say'; message: string; reason: string }
| { type: 'bankDeposit'; slot: number; amount: number; reason: string }
| { type: 'bankWithdraw'; slot: number; amount: number; reason: string };
| { type: 'bankWithdraw'; slot: number; amount: number; reason: string }
// On-demand scanning (returns data in action result)
| { type: 'scanNearbyLocs'; radius?: number; reason: string }
| { type: 'scanGroundItems'; radius?: number; reason: string };