kill-switch-mcp 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kill-switch-mcp.js +5 -5
- package/dist/server.js +4947 -0
- package/package.json +4 -2
- package/src/api/index.ts +0 -188
- package/src/sdk/actions-helpers.ts +0 -450
- package/src/sdk/actions.ts +0 -3208
- package/src/sdk/formatter.ts +0 -263
- package/src/sdk/index.ts +0 -1313
- package/src/sdk/pathfinding.ts +0 -57
- package/src/sdk/types.ts +0 -584
- package/src/server.ts +0 -1088
- package/src/wallet/index.ts +0 -369
package/src/sdk/index.ts
DELETED
|
@@ -1,1313 +0,0 @@
|
|
|
1
|
-
// Bot SDK - Standalone client for remote bot control
|
|
2
|
-
// Low-level WebSocket API that maps 1:1 to the action protocol
|
|
3
|
-
// Actions resolve when game ACKNOWLEDGES them (not when effects complete)
|
|
4
|
-
|
|
5
|
-
import type {
|
|
6
|
-
BotWorldState,
|
|
7
|
-
BotAction,
|
|
8
|
-
ActionResult,
|
|
9
|
-
SkillState,
|
|
10
|
-
InventoryItem,
|
|
11
|
-
NearbyNpc,
|
|
12
|
-
NearbyPlayer,
|
|
13
|
-
NearbyLoc,
|
|
14
|
-
GroundItem,
|
|
15
|
-
DialogState,
|
|
16
|
-
BankItem,
|
|
17
|
-
SDKConfig,
|
|
18
|
-
ConnectionState,
|
|
19
|
-
SDKConnectionMode,
|
|
20
|
-
BotStatus,
|
|
21
|
-
PrayerState,
|
|
22
|
-
PrayerName
|
|
23
|
-
} from './types';
|
|
24
|
-
import { PRAYER_INDICES, PRAYER_NAMES } from './types';
|
|
25
|
-
import * as pathfinding from './pathfinding';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Derive the gateway WebSocket URL from a SERVER env value.
|
|
29
|
-
* - undefined/empty → ws://localhost:7780 (local default)
|
|
30
|
-
* - Full URL (ws:// or wss://) → used as-is
|
|
31
|
-
* - "localhost" or "localhost:PORT" → ws://localhost:PORT (plain WS)
|
|
32
|
-
* - anything else → wss://HOST/gateway (TLS, remote gateway path)
|
|
33
|
-
*/
|
|
34
|
-
export function deriveGatewayUrl(server?: string): string {
|
|
35
|
-
if (!server) return 'ws://localhost:7780';
|
|
36
|
-
if (server.startsWith('ws://') || server.startsWith('wss://')) return server;
|
|
37
|
-
const isLocal = server === 'localhost' || server.startsWith('localhost:');
|
|
38
|
-
if (isLocal) {
|
|
39
|
-
return `ws://${server.includes(':') ? server : server + ':7780'}`;
|
|
40
|
-
}
|
|
41
|
-
return `wss://${server}/gateway`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface SyncToSDKMessage {
|
|
45
|
-
type: 'sdk_connected' | 'sdk_state' | 'sdk_action_result' | 'sdk_error' | 'sdk_screenshot_response';
|
|
46
|
-
success?: boolean;
|
|
47
|
-
state?: BotWorldState;
|
|
48
|
-
stateReceivedAt?: number; // Timestamp when gateway received state from bot
|
|
49
|
-
actionId?: string;
|
|
50
|
-
result?: ActionResult;
|
|
51
|
-
error?: string;
|
|
52
|
-
screenshotId?: string;
|
|
53
|
-
dataUrl?: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface PendingAction {
|
|
57
|
-
resolve: (result: ActionResult) => void;
|
|
58
|
-
reject: (error: Error) => void;
|
|
59
|
-
timeout: ReturnType<typeof setTimeout>;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface PendingScreenshot {
|
|
63
|
-
resolve: (dataUrl: string) => void;
|
|
64
|
-
reject: (error: Error) => void;
|
|
65
|
-
timeout: ReturnType<typeof setTimeout>;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export class BotSDK {
|
|
69
|
-
readonly config: Required<SDKConfig>;
|
|
70
|
-
private ws: WebSocket | null = null;
|
|
71
|
-
private state: BotWorldState | null = null;
|
|
72
|
-
private stateReceivedAt: number = 0;
|
|
73
|
-
private pendingActions = new Map<string, PendingAction>();
|
|
74
|
-
private pendingScreenshots = new Map<string, PendingScreenshot>();
|
|
75
|
-
private stateListeners = new Set<(state: BotWorldState) => void>();
|
|
76
|
-
private connectionListeners = new Set<(state: ConnectionState, attempt?: number) => void>();
|
|
77
|
-
private connectPromise: Promise<void> | null = null;
|
|
78
|
-
private sdkClientId: string;
|
|
79
|
-
|
|
80
|
-
// Reconnection state
|
|
81
|
-
private connectionState: ConnectionState = 'disconnected';
|
|
82
|
-
private reconnectAttempt = 0;
|
|
83
|
-
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
84
|
-
private intentionalDisconnect = false;
|
|
85
|
-
private browserLaunched = false;
|
|
86
|
-
|
|
87
|
-
constructor(config: SDKConfig) {
|
|
88
|
-
this.config = {
|
|
89
|
-
botUsername: config.botUsername,
|
|
90
|
-
password: config.password || '',
|
|
91
|
-
gatewayUrl: config.gatewayUrl || '',
|
|
92
|
-
host: config.host || 'localhost',
|
|
93
|
-
port: config.port || 7780,
|
|
94
|
-
connectionMode: config.connectionMode || 'control',
|
|
95
|
-
autoLaunchBrowser: config.autoLaunchBrowser ?? 'auto',
|
|
96
|
-
freshDataThreshold: config.freshDataThreshold ?? 3000,
|
|
97
|
-
browserLaunchUrl: config.browserLaunchUrl || '',
|
|
98
|
-
browserLaunchTimeout: config.browserLaunchTimeout || 10000,
|
|
99
|
-
actionTimeout: config.actionTimeout || 60000,
|
|
100
|
-
autoReconnect: config.autoReconnect ?? true,
|
|
101
|
-
reconnectMaxRetries: config.reconnectMaxRetries ?? Infinity,
|
|
102
|
-
reconnectBaseDelay: config.reconnectBaseDelay ?? 1000,
|
|
103
|
-
reconnectMaxDelay: config.reconnectMaxDelay ?? 30000,
|
|
104
|
-
showChat: config.showChat ?? false
|
|
105
|
-
};
|
|
106
|
-
this.sdkClientId = `sdk-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ============ Connection ============
|
|
110
|
-
|
|
111
|
-
/** Connect to the gateway WebSocket. */
|
|
112
|
-
async connect(): Promise<void> {
|
|
113
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (this.connectPromise) {
|
|
118
|
-
return this.connectPromise;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
this.intentionalDisconnect = false;
|
|
122
|
-
|
|
123
|
-
const isReconnect = this.connectionState === 'reconnecting';
|
|
124
|
-
if (!isReconnect) {
|
|
125
|
-
this.setConnectionState('connecting');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Auto-launch browser based on config
|
|
129
|
-
if (this.config.autoLaunchBrowser && !isReconnect) {
|
|
130
|
-
try {
|
|
131
|
-
const status = await this.checkBotStatus();
|
|
132
|
-
const shouldLaunch = this.shouldLaunchBrowser(status);
|
|
133
|
-
|
|
134
|
-
if (shouldLaunch) {
|
|
135
|
-
console.log(`[BotSDK] Launching browser...`);
|
|
136
|
-
this.browserLaunched = true;
|
|
137
|
-
await this.launchBrowser();
|
|
138
|
-
await this.waitForBotConnection();
|
|
139
|
-
}
|
|
140
|
-
} catch (error) {
|
|
141
|
-
console.error(`[BotSDK] Auto-launch failed:`, error);
|
|
142
|
-
// Continue anyway - maybe gateway is local and status endpoint doesn't work yet
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
this.connectPromise = new Promise((resolve, reject) => {
|
|
147
|
-
const url = this.config.gatewayUrl || `ws://${this.config.host}:${this.config.port}`;
|
|
148
|
-
this.ws = new WebSocket(url);
|
|
149
|
-
|
|
150
|
-
const timeout = setTimeout(() => {
|
|
151
|
-
reject(new Error('Connection timeout'));
|
|
152
|
-
this.ws?.close();
|
|
153
|
-
}, 10000);
|
|
154
|
-
|
|
155
|
-
this.ws.onopen = () => {
|
|
156
|
-
clearTimeout(timeout);
|
|
157
|
-
this.send({
|
|
158
|
-
type: 'sdk_connect',
|
|
159
|
-
username: this.config.botUsername,
|
|
160
|
-
password: this.config.password,
|
|
161
|
-
clientId: this.sdkClientId,
|
|
162
|
-
mode: this.config.connectionMode
|
|
163
|
-
});
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
this.ws.onmessage = (event) => {
|
|
167
|
-
this.handleMessage(event.data);
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
this.ws.onclose = () => {
|
|
171
|
-
console.warn(`[LOGOUT DEBUG] SDK WebSocket closed - autoReconnect=${this.config.autoReconnect}, intentionalDisconnect=${this.intentionalDisconnect}`);
|
|
172
|
-
this.connectPromise = null;
|
|
173
|
-
this.ws = null;
|
|
174
|
-
|
|
175
|
-
for (const [actionId, pending] of this.pendingActions) {
|
|
176
|
-
clearTimeout(pending.timeout);
|
|
177
|
-
pending.reject(new Error('Connection closed'));
|
|
178
|
-
}
|
|
179
|
-
this.pendingActions.clear();
|
|
180
|
-
|
|
181
|
-
if (this.config.autoReconnect && !this.intentionalDisconnect) {
|
|
182
|
-
console.warn('[LOGOUT DEBUG] SDK scheduling auto-reconnect');
|
|
183
|
-
this.scheduleReconnect();
|
|
184
|
-
} else {
|
|
185
|
-
this.setConnectionState('disconnected');
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
this.ws.onerror = (error) => {
|
|
190
|
-
console.warn('[LOGOUT DEBUG] SDK WebSocket error event');
|
|
191
|
-
clearTimeout(timeout);
|
|
192
|
-
reject(new Error('WebSocket error'));
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const checkConnected = (event: MessageEvent) => {
|
|
196
|
-
try {
|
|
197
|
-
const msg = JSON.parse(event.data);
|
|
198
|
-
if (msg.type === 'sdk_connected') {
|
|
199
|
-
this.ws?.removeEventListener('message', checkConnected);
|
|
200
|
-
this.reconnectAttempt = 0;
|
|
201
|
-
this.setConnectionState('connected');
|
|
202
|
-
|
|
203
|
-
// Send login credentials through gateway if we launched a browser
|
|
204
|
-
if (this.browserLaunched) {
|
|
205
|
-
this.sendLogin();
|
|
206
|
-
this.browserLaunched = false;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Automatically wait for game state to be ready
|
|
210
|
-
this.waitForReady(15000)
|
|
211
|
-
.then(() => {
|
|
212
|
-
console.log('[BotSDK] Connected and game state ready');
|
|
213
|
-
resolve();
|
|
214
|
-
})
|
|
215
|
-
.catch((error) => {
|
|
216
|
-
console.warn('[BotSDK] Connected but game state not ready:', error.message);
|
|
217
|
-
console.warn('[BotSDK] Continuing anyway - state may load later');
|
|
218
|
-
resolve(); // Still resolve - allow usage even if state isn't fully ready
|
|
219
|
-
});
|
|
220
|
-
} else if (msg.type === 'sdk_error') {
|
|
221
|
-
// Handle authentication errors during connection
|
|
222
|
-
clearTimeout(timeout);
|
|
223
|
-
this.ws?.removeEventListener('message', checkConnected);
|
|
224
|
-
const errorMessage = msg.error || 'Authentication failed';
|
|
225
|
-
console.error(`[BotSDK] Connection error: ${errorMessage}`);
|
|
226
|
-
// Disable auto-reconnect for auth failures - they won't succeed on retry
|
|
227
|
-
this.intentionalDisconnect = true;
|
|
228
|
-
reject(new Error(errorMessage));
|
|
229
|
-
this.ws?.close();
|
|
230
|
-
}
|
|
231
|
-
} catch {}
|
|
232
|
-
};
|
|
233
|
-
this.ws.addEventListener('message', checkConnected);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
return this.connectPromise;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
private setConnectionState(state: ConnectionState, attempt?: number) {
|
|
240
|
-
this.connectionState = state;
|
|
241
|
-
for (const listener of this.connectionListeners) {
|
|
242
|
-
try {
|
|
243
|
-
listener(state, attempt);
|
|
244
|
-
} catch (e) {
|
|
245
|
-
console.error('Connection listener error:', e);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
private scheduleReconnect() {
|
|
251
|
-
if (this.reconnectAttempt >= this.config.reconnectMaxRetries) {
|
|
252
|
-
console.log(`[BotSDK] Max reconnection attempts (${this.config.reconnectMaxRetries}) reached, giving up`);
|
|
253
|
-
this.setConnectionState('disconnected');
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
this.reconnectAttempt++;
|
|
258
|
-
this.setConnectionState('reconnecting', this.reconnectAttempt);
|
|
259
|
-
|
|
260
|
-
const delay = Math.min(
|
|
261
|
-
this.config.reconnectBaseDelay * Math.pow(2, this.reconnectAttempt - 1),
|
|
262
|
-
this.config.reconnectMaxDelay
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
console.log(`[BotSDK] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})`);
|
|
266
|
-
|
|
267
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
268
|
-
this.reconnectTimer = null;
|
|
269
|
-
try {
|
|
270
|
-
await this.connect();
|
|
271
|
-
console.log(`[BotSDK] Reconnected successfully after ${this.reconnectAttempt} attempt(s)`);
|
|
272
|
-
} catch (e) {
|
|
273
|
-
console.log(`[BotSDK] Reconnection attempt ${this.reconnectAttempt} failed`);
|
|
274
|
-
}
|
|
275
|
-
}, delay);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/** Disconnect from the gateway. */
|
|
279
|
-
async disconnect(): Promise<void> {
|
|
280
|
-
this.intentionalDisconnect = true;
|
|
281
|
-
|
|
282
|
-
if (this.reconnectTimer) {
|
|
283
|
-
clearTimeout(this.reconnectTimer);
|
|
284
|
-
this.reconnectTimer = null;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (this.ws) {
|
|
288
|
-
// Wait for websocket to actually close
|
|
289
|
-
await new Promise<void>((resolve) => {
|
|
290
|
-
if (this.ws!.readyState === WebSocket.CLOSED) {
|
|
291
|
-
resolve();
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
this.ws!.addEventListener('close', () => resolve(), { once: true });
|
|
295
|
-
this.ws!.close();
|
|
296
|
-
});
|
|
297
|
-
this.ws = null;
|
|
298
|
-
}
|
|
299
|
-
this.connectPromise = null;
|
|
300
|
-
this.reconnectAttempt = 0;
|
|
301
|
-
this.setConnectionState('disconnected');
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/** Check if WebSocket is connected. */
|
|
305
|
-
isConnected(): boolean {
|
|
306
|
-
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/** Get current connection state (connecting, connected, reconnecting, disconnected). */
|
|
310
|
-
getConnectionState(): ConnectionState {
|
|
311
|
-
return this.connectionState;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** Get current reconnection attempt number. */
|
|
315
|
-
getReconnectAttempt(): number {
|
|
316
|
-
return this.reconnectAttempt;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
onConnectionStateChange(listener: (state: ConnectionState, attempt?: number) => void): () => void {
|
|
320
|
-
this.connectionListeners.add(listener);
|
|
321
|
-
return () => this.connectionListeners.delete(listener);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/** Get connection mode (control or observe). */
|
|
325
|
-
getConnectionMode(): SDKConnectionMode {
|
|
326
|
-
return this.config.connectionMode;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ============ Bot Status & Auto-Launch ============
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Check bot status via gateway HTTP endpoint.
|
|
333
|
-
* Returns info about whether bot is connected and who else is controlling/observing.
|
|
334
|
-
*/
|
|
335
|
-
async checkBotStatus(): Promise<BotStatus> {
|
|
336
|
-
const statusUrl = this.getStatusUrl();
|
|
337
|
-
try {
|
|
338
|
-
console.log(`[BotSDK] Checking bot status via URL: ${statusUrl}`);
|
|
339
|
-
const response = await fetch(statusUrl);
|
|
340
|
-
if (!response.ok) {
|
|
341
|
-
console.log(`[BotSDK] Status check HTTP error: ${response.status} ${response.statusText} (URL: ${statusUrl})`);
|
|
342
|
-
throw new Error(`Status check failed: ${response.status}`);
|
|
343
|
-
}
|
|
344
|
-
const data = await response.json();
|
|
345
|
-
return data;
|
|
346
|
-
} catch (error) {
|
|
347
|
-
// If endpoint doesn't exist or bot not found, return disconnected status
|
|
348
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
349
|
-
console.log(`[BotSDK] Status check failed: ${errorMsg} (URL: ${statusUrl})`);
|
|
350
|
-
return {
|
|
351
|
-
status: 'dead',
|
|
352
|
-
inGame: false,
|
|
353
|
-
stateAge: null,
|
|
354
|
-
controllers: [],
|
|
355
|
-
observers: [],
|
|
356
|
-
player: null,
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Check if bot is currently connected to gateway.
|
|
363
|
-
*/
|
|
364
|
-
async isBotConnected(): Promise<boolean> {
|
|
365
|
-
const status = await this.checkBotStatus();
|
|
366
|
-
return status.status !== 'dead';
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Determine if browser should be launched based on config and current status.
|
|
371
|
-
* - 'auto': Launch only if session is dead (no WebSocket)
|
|
372
|
-
* - true: Launch if bot not connected (dead)
|
|
373
|
-
* - false: Never launch
|
|
374
|
-
*/
|
|
375
|
-
private shouldLaunchBrowser(status: BotStatus): boolean {
|
|
376
|
-
if (this.config.autoLaunchBrowser === false) {
|
|
377
|
-
return false;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (status.status === 'dead') {
|
|
381
|
-
console.log(`[BotSDK] Bot not connected`);
|
|
382
|
-
return true;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
console.log(`[BotSDK] Active client detected, skipping browser launch`);
|
|
386
|
-
return false;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Launch native browser to client URL.
|
|
391
|
-
* Uses platform-specific open command (open on macOS, start on Windows, xdg-open on Linux).
|
|
392
|
-
*/
|
|
393
|
-
async launchBrowser(): Promise<void> {
|
|
394
|
-
const url = this.buildClientUrl();
|
|
395
|
-
console.log(`[BotSDK] Opening browser: ${url}`);
|
|
396
|
-
|
|
397
|
-
const { exec } = await import('child_process');
|
|
398
|
-
|
|
399
|
-
const command = process.platform === 'darwin'
|
|
400
|
-
? `open "${url}"`
|
|
401
|
-
: process.platform === 'win32'
|
|
402
|
-
? `start "" "${url}"`
|
|
403
|
-
: `xdg-open "${url}"`;
|
|
404
|
-
|
|
405
|
-
return new Promise((resolve, reject) => {
|
|
406
|
-
exec(command, (error) => {
|
|
407
|
-
if (error) {
|
|
408
|
-
reject(new Error(`Failed to open browser: ${error.message}`));
|
|
409
|
-
} else {
|
|
410
|
-
resolve();
|
|
411
|
-
}
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Wait for bot to connect to gateway after browser launch.
|
|
418
|
-
*/
|
|
419
|
-
async waitForBotConnection(timeout?: number): Promise<void> {
|
|
420
|
-
const timeoutMs = timeout || this.config.browserLaunchTimeout;
|
|
421
|
-
const startTime = Date.now();
|
|
422
|
-
const pollInterval = 500;
|
|
423
|
-
let attemptCount = 0;
|
|
424
|
-
|
|
425
|
-
console.log(`[BotSDK] Waiting for bot to connect and load game (timeout: ${timeoutMs}ms)...`);
|
|
426
|
-
|
|
427
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
428
|
-
attemptCount++;
|
|
429
|
-
const elapsed = Date.now() - startTime;
|
|
430
|
-
const status = await this.checkBotStatus();
|
|
431
|
-
|
|
432
|
-
console.log(`[BotSDK] Poll attempt ${attemptCount} (${elapsed}ms): status="${status.status}", inGame=${status.inGame}, controllers=${status.controllers.length}, observers=${status.observers.length}`);
|
|
433
|
-
|
|
434
|
-
if (status.status !== 'dead' && status.inGame) {
|
|
435
|
-
console.log(`[BotSDK] Bot connected and in-game!`);
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
throw new Error(`Bot did not fully load within ${timeoutMs}ms`);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
private getStatusUrl(): string {
|
|
445
|
-
const gatewayUrl = this.config.gatewayUrl || `http://${this.config.host}:${this.config.port}`;
|
|
446
|
-
// Convert ws:// to http:// and wss:// to https://
|
|
447
|
-
const httpUrl = gatewayUrl
|
|
448
|
-
.replace(/^ws:/, 'http:')
|
|
449
|
-
.replace(/^wss:/, 'https:')
|
|
450
|
-
.replace(/\/gateway$/, ''); // Remove /gateway suffix if present
|
|
451
|
-
|
|
452
|
-
return `${httpUrl}/status/${encodeURIComponent(this.config.botUsername)}`;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
private buildClientUrl(): string {
|
|
456
|
-
if (this.config.browserLaunchUrl) {
|
|
457
|
-
const url = new URL(this.config.browserLaunchUrl);
|
|
458
|
-
url.searchParams.set('bot', this.config.botUsername);
|
|
459
|
-
return url.toString();
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Derive from gateway URL
|
|
463
|
-
const gatewayUrl = this.config.gatewayUrl || `ws://${this.config.host}:${this.config.port}`;
|
|
464
|
-
|
|
465
|
-
if (gatewayUrl.includes('localhost') || gatewayUrl.includes('127.0.0.1')) {
|
|
466
|
-
return `http://localhost:8888/play?bot=${encodeURIComponent(this.config.botUsername)}`;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Remote: assume same host with /play path
|
|
470
|
-
const httpUrl = gatewayUrl
|
|
471
|
-
.replace(/^ws:/, 'http:')
|
|
472
|
-
.replace(/^wss:/, 'https:')
|
|
473
|
-
.replace(/\/gateway$/, '');
|
|
474
|
-
|
|
475
|
-
return `${httpUrl}/play?bot=${encodeURIComponent(this.config.botUsername)}`;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/** Send login credentials to the browser client through the gateway */
|
|
479
|
-
sendLogin(): void {
|
|
480
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
481
|
-
console.error('[BotSDK] Cannot send login - not connected to gateway');
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
this.ws.send(JSON.stringify({
|
|
485
|
-
type: 'sdk_login',
|
|
486
|
-
username: this.config.botUsername,
|
|
487
|
-
password: this.config.password
|
|
488
|
-
}));
|
|
489
|
-
console.log(`[BotSDK] Sent login command for ${this.config.botUsername} through gateway`);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/** Wait for WebSocket connection to be established. */
|
|
493
|
-
async waitForConnection(timeout: number = 60000): Promise<void> {
|
|
494
|
-
if (this.isConnected()) {
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
return new Promise((resolve, reject) => {
|
|
499
|
-
const timeoutId = setTimeout(() => {
|
|
500
|
-
unsubscribe();
|
|
501
|
-
reject(new Error('waitForConnection timed out'));
|
|
502
|
-
}, timeout);
|
|
503
|
-
|
|
504
|
-
const unsubscribe = this.onConnectionStateChange((state) => {
|
|
505
|
-
if (state === 'connected') {
|
|
506
|
-
clearTimeout(timeoutId);
|
|
507
|
-
unsubscribe();
|
|
508
|
-
resolve();
|
|
509
|
-
} else if (state === 'disconnected') {
|
|
510
|
-
clearTimeout(timeoutId);
|
|
511
|
-
unsubscribe();
|
|
512
|
-
reject(new Error('Connection failed'));
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// ============ State Access (Synchronous) ============
|
|
519
|
-
|
|
520
|
-
/** Get current game state snapshot. */
|
|
521
|
-
getState(): BotWorldState | null {
|
|
522
|
-
return this.state;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/** Get timestamp when state was last received (ms since epoch) */
|
|
526
|
-
getStateReceivedAt(): number {
|
|
527
|
-
return this.stateReceivedAt;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/** Get age of current state in milliseconds */
|
|
531
|
-
getStateAge(): number {
|
|
532
|
-
if (this.stateReceivedAt === 0) return 0;
|
|
533
|
-
return Date.now() - this.stateReceivedAt;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/** Get a skill by name (case-insensitive). */
|
|
537
|
-
getSkill(name: string): SkillState | null {
|
|
538
|
-
if (!this.state) return null;
|
|
539
|
-
return this.state.skills.find(s =>
|
|
540
|
-
s.name.toLowerCase() === name.toLowerCase()
|
|
541
|
-
) || null;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/** Get XP for a skill by name. */
|
|
545
|
-
getSkillXp(name: string): number | null {
|
|
546
|
-
const skill = this.getSkill(name);
|
|
547
|
-
return skill?.experience ?? null;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/** Get all skills. */
|
|
551
|
-
getSkills(): SkillState[] {
|
|
552
|
-
return this.state?.skills || [];
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/** Get inventory item by slot number. */
|
|
556
|
-
getInventoryItem(slot: number): InventoryItem | null {
|
|
557
|
-
if (!this.state) return null;
|
|
558
|
-
return this.state.inventory.find(i => i.slot === slot) || null;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/** Find inventory item by name pattern. */
|
|
562
|
-
findInventoryItem(pattern: string | RegExp): InventoryItem | null {
|
|
563
|
-
if (!this.state) return null;
|
|
564
|
-
const regex = typeof pattern === 'string'
|
|
565
|
-
? new RegExp(pattern, 'i')
|
|
566
|
-
: pattern;
|
|
567
|
-
return this.state.inventory.find(i => regex.test(i.name)) || null;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/** Get all inventory items. */
|
|
571
|
-
getInventory(): InventoryItem[] {
|
|
572
|
-
return this.state?.inventory || [];
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/** Get equipment item by slot number. */
|
|
576
|
-
getEquipmentItem(slot: number): InventoryItem | null {
|
|
577
|
-
if (!this.state) return null;
|
|
578
|
-
return this.state.equipment.find(i => i.slot === slot) || null;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/** Find equipment item by name pattern. */
|
|
582
|
-
findEquipmentItem(pattern: string | RegExp): InventoryItem | null {
|
|
583
|
-
if (!this.state) return null;
|
|
584
|
-
const regex = typeof pattern === 'string'
|
|
585
|
-
? new RegExp(pattern, 'i')
|
|
586
|
-
: pattern;
|
|
587
|
-
return this.state.equipment.find(i => regex.test(i.name)) || null;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/** Get all equipped items. */
|
|
591
|
-
getEquipment(): InventoryItem[] {
|
|
592
|
-
return this.state?.equipment || [];
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
/** Get bank item by slot number (bank must be open). */
|
|
596
|
-
getBankItem(slot: number): BankItem | null {
|
|
597
|
-
if (!this.state?.bank.isOpen) return null;
|
|
598
|
-
return this.state.bank.items.find(i => i.slot === slot) || null;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
/** Find bank item by name pattern (bank must be open). */
|
|
602
|
-
findBankItem(pattern: string | RegExp): BankItem | null {
|
|
603
|
-
if (!this.state?.bank.isOpen) return null;
|
|
604
|
-
const regex = typeof pattern === 'string'
|
|
605
|
-
? new RegExp(pattern, 'i')
|
|
606
|
-
: pattern;
|
|
607
|
-
return this.state.bank.items.find(i => regex.test(i.name)) || null;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/** Get all bank items (bank must be open). */
|
|
611
|
-
getBankItems(): BankItem[] {
|
|
612
|
-
return this.state?.bank.items || [];
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
/** Check if bank interface is open. */
|
|
616
|
-
isBankOpen(): boolean {
|
|
617
|
-
return this.state?.bank.isOpen || false;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/** Get NPC by index. */
|
|
621
|
-
getNearbyNpc(index: number): NearbyNpc | null {
|
|
622
|
-
if (!this.state) return null;
|
|
623
|
-
return this.state.nearbyNpcs.find(n => n.index === index) || null;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/** Find NPC by name pattern. */
|
|
627
|
-
findNearbyNpc(pattern: string | RegExp): NearbyNpc | null {
|
|
628
|
-
if (!this.state) return null;
|
|
629
|
-
const regex = typeof pattern === 'string'
|
|
630
|
-
? new RegExp(pattern, 'i')
|
|
631
|
-
: pattern;
|
|
632
|
-
return this.state.nearbyNpcs.find(n => regex.test(n.name)) || null;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
/** Get all nearby NPCs. */
|
|
636
|
-
getNearbyNpcs(): NearbyNpc[] {
|
|
637
|
-
return this.state?.nearbyNpcs || [];
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/** Find nearby player by name pattern. */
|
|
641
|
-
findNearbyPlayer(pattern: string | RegExp): NearbyPlayer | null {
|
|
642
|
-
if (!this.state) return null;
|
|
643
|
-
const regex = typeof pattern === 'string'
|
|
644
|
-
? new RegExp(pattern, 'i')
|
|
645
|
-
: pattern;
|
|
646
|
-
return this.state.nearbyPlayers.find(p => regex.test(p.name)) || null;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
/** Get all nearby players. */
|
|
650
|
-
getNearbyPlayers(): NearbyPlayer[] {
|
|
651
|
-
return this.state?.nearbyPlayers || [];
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/** Get location (object) by coordinates and ID. */
|
|
655
|
-
getNearbyLoc(x: number, z: number, id: number): NearbyLoc | null {
|
|
656
|
-
if (!this.state) return null;
|
|
657
|
-
return this.state.nearbyLocs.find(l =>
|
|
658
|
-
l.x === x && l.z === z && l.id === id
|
|
659
|
-
) || null;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/** Find location by name pattern. */
|
|
663
|
-
findNearbyLoc(pattern: string | RegExp): NearbyLoc | null {
|
|
664
|
-
if (!this.state) return null;
|
|
665
|
-
const regex = typeof pattern === 'string'
|
|
666
|
-
? new RegExp(pattern, 'i')
|
|
667
|
-
: pattern;
|
|
668
|
-
return this.state.nearbyLocs.find(l => regex.test(l.name)) || null;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
/** Get all nearby locations (trees, rocks, etc). */
|
|
672
|
-
getNearbyLocs(): NearbyLoc[] {
|
|
673
|
-
return this.state?.nearbyLocs || [];
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
/** Find ground item by name pattern. */
|
|
677
|
-
findGroundItem(pattern: string | RegExp): GroundItem | null {
|
|
678
|
-
if (!this.state) return null;
|
|
679
|
-
const regex = typeof pattern === 'string'
|
|
680
|
-
? new RegExp(pattern, 'i')
|
|
681
|
-
: pattern;
|
|
682
|
-
return this.state.groundItems.find(i => regex.test(i.name)) || null;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/** Get all ground items. */
|
|
686
|
-
getGroundItems(): GroundItem[] {
|
|
687
|
-
return this.state?.groundItems || [];
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/** Get current dialog state. */
|
|
691
|
-
getDialog(): DialogState | null {
|
|
692
|
-
return this.state?.dialog || null;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
// ============ On-Demand Scanning ============
|
|
696
|
-
// These methods scan the environment on-demand rather than relying on pushed state
|
|
697
|
-
// Use these for expensive scans of nearby locations and ground items
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* Scan for nearby locations with custom radius.
|
|
701
|
-
* @param radius - Scan radius in tiles (default 15)
|
|
702
|
-
* @returns Array of nearby locations sorted by distance
|
|
703
|
-
*/
|
|
704
|
-
async scanNearbyLocs(radius?: number): Promise<NearbyLoc[]> {
|
|
705
|
-
const result = await this.sendAction({ type: 'scanNearbyLocs', radius, reason: 'SDK' });
|
|
706
|
-
if (result.success && result.data) {
|
|
707
|
-
return result.data as NearbyLoc[];
|
|
708
|
-
}
|
|
709
|
-
return [];
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
/**
|
|
713
|
-
* Scan for ground items on-demand.
|
|
714
|
-
* This is more efficient than constantly pushing this data in state updates.
|
|
715
|
-
* @param radius - Scan radius in tiles (default 15)
|
|
716
|
-
* @returns Array of ground items sorted by distance
|
|
717
|
-
*/
|
|
718
|
-
async scanGroundItems(radius?: number): Promise<GroundItem[]> {
|
|
719
|
-
const result = await this.sendAction({ type: 'scanGroundItems', radius, reason: 'SDK' });
|
|
720
|
-
if (result.success && result.data) {
|
|
721
|
-
return result.data as GroundItem[];
|
|
722
|
-
}
|
|
723
|
-
return [];
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
* Find a nearby location by name pattern (on-demand scan).
|
|
728
|
-
* @param pattern - String or RegExp to match location name
|
|
729
|
-
* @param radius - Scan radius in tiles (default 15)
|
|
730
|
-
* @returns First matching location or null
|
|
731
|
-
*/
|
|
732
|
-
async scanFindNearbyLoc(pattern: string | RegExp, radius?: number): Promise<NearbyLoc | null> {
|
|
733
|
-
const locs = await this.scanNearbyLocs(radius);
|
|
734
|
-
const regex = typeof pattern === 'string'
|
|
735
|
-
? new RegExp(pattern, 'i')
|
|
736
|
-
: pattern;
|
|
737
|
-
return locs.find(l => regex.test(l.name)) || null;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
/**
|
|
741
|
-
* Find a ground item by name pattern (on-demand scan).
|
|
742
|
-
* @param pattern - String or RegExp to match item name
|
|
743
|
-
* @param radius - Scan radius in tiles (default 15)
|
|
744
|
-
* @returns First matching item or null
|
|
745
|
-
*/
|
|
746
|
-
async scanFindGroundItem(pattern: string | RegExp, radius?: number): Promise<GroundItem | null> {
|
|
747
|
-
const items = await this.scanGroundItems(radius);
|
|
748
|
-
const regex = typeof pattern === 'string'
|
|
749
|
-
? new RegExp(pattern, 'i')
|
|
750
|
-
: pattern;
|
|
751
|
-
return items.find(i => regex.test(i.name)) || null;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// ============ State Subscriptions ============
|
|
755
|
-
|
|
756
|
-
onStateUpdate(listener: (state: BotWorldState) => void): () => void {
|
|
757
|
-
this.stateListeners.add(listener);
|
|
758
|
-
return () => this.stateListeners.delete(listener);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
// ============ Plumbing: Raw Actions ============
|
|
762
|
-
|
|
763
|
-
private async sendAction(action: BotAction): Promise<ActionResult> {
|
|
764
|
-
if (this.connectionState === 'reconnecting') {
|
|
765
|
-
console.log(`[BotSDK] Waiting for reconnection before sending action: ${action.type}`);
|
|
766
|
-
await this.waitForConnection();
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
if (!this.isConnected()) {
|
|
770
|
-
throw new Error(`Not connected (state: ${this.connectionState})`);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
const actionId = `act-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
774
|
-
|
|
775
|
-
return new Promise((resolve, reject) => {
|
|
776
|
-
const timeout = setTimeout(() => {
|
|
777
|
-
this.pendingActions.delete(actionId);
|
|
778
|
-
reject(new Error(`Action timed out: ${action.type}`));
|
|
779
|
-
}, this.config.actionTimeout);
|
|
780
|
-
|
|
781
|
-
this.pendingActions.set(actionId, { resolve, reject, timeout });
|
|
782
|
-
|
|
783
|
-
this.send({
|
|
784
|
-
type: 'sdk_action',
|
|
785
|
-
username: this.config.botUsername,
|
|
786
|
-
actionId,
|
|
787
|
-
action
|
|
788
|
-
});
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
/** Send walk command to coordinates. */
|
|
793
|
-
async sendWalk(x: number, z: number, running: boolean = true): Promise<ActionResult> {
|
|
794
|
-
return this.sendAction({ type: 'walkTo', x, z, running, reason: 'SDK' });
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
/** Interact with a location (tree, rock, door, etc). */
|
|
798
|
-
async sendInteractLoc(x: number, z: number, locId: number, option: number = 1): Promise<ActionResult> {
|
|
799
|
-
return this.sendAction({ type: 'interactLoc', x, z, locId, optionIndex: option, reason: 'SDK' });
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
/** Interact with an NPC by index and option. */
|
|
803
|
-
async sendInteractNpc(npcIndex: number, option: number = 1): Promise<ActionResult> {
|
|
804
|
-
return this.sendAction({ type: 'interactNpc', npcIndex, optionIndex: option, reason: 'SDK' });
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
/** Interact with a player by index and option. Option 2 = Attack (wilderness), 3 = Follow, 4 = Trade. */
|
|
808
|
-
async sendInteractPlayer(playerIndex: number, option: number = 2): Promise<ActionResult> {
|
|
809
|
-
return this.sendAction({ type: 'interactPlayer', playerIndex, optionIndex: option, reason: 'SDK' });
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
/** Talk to an NPC by index. */
|
|
813
|
-
async sendTalkToNpc(npcIndex: number): Promise<ActionResult> {
|
|
814
|
-
return this.sendAction({ type: 'talkToNpc', npcIndex, reason: 'SDK' });
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
/** Pick up a ground item. */
|
|
818
|
-
async sendPickup(x: number, z: number, itemId: number): Promise<ActionResult> {
|
|
819
|
-
return this.sendAction({ type: 'pickupItem', x, z, itemId, reason: 'SDK' });
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
/** Use an inventory item (eat, equip, etc). */
|
|
823
|
-
async sendUseItem(slot: number, option: number = 1): Promise<ActionResult> {
|
|
824
|
-
return this.sendAction({ type: 'useInventoryItem', slot, optionIndex: option, reason: 'SDK' });
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
/** Use an equipped item (remove, operate, etc). */
|
|
828
|
-
async sendUseEquipmentItem(slot: number, option: number = 1): Promise<ActionResult> {
|
|
829
|
-
return this.sendAction({ type: 'useEquipmentItem', slot, optionIndex: option, reason: 'SDK' });
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
/** Drop an inventory item. */
|
|
833
|
-
async sendDropItem(slot: number): Promise<ActionResult> {
|
|
834
|
-
return this.sendAction({ type: 'dropItem', slot, reason: 'SDK' });
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
/** Use one inventory item on another. */
|
|
838
|
-
async sendUseItemOnItem(sourceSlot: number, targetSlot: number): Promise<ActionResult> {
|
|
839
|
-
return this.sendAction({ type: 'useItemOnItem', sourceSlot, targetSlot, reason: 'SDK' });
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
/** Use an inventory item on a location. */
|
|
843
|
-
async sendUseItemOnLoc(itemSlot: number, x: number, z: number, locId: number): Promise<ActionResult> {
|
|
844
|
-
return this.sendAction({ type: 'useItemOnLoc', itemSlot, x, z, locId, reason: 'SDK' });
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
/** Use an inventory item on an NPC. */
|
|
848
|
-
async sendUseItemOnNpc(itemSlot: number, npcIndex: number): Promise<ActionResult> {
|
|
849
|
-
return this.sendAction({ type: 'useItemOnNpc', itemSlot, npcIndex, reason: 'SDK' });
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
/** Click a dialog option by index. */
|
|
853
|
-
async sendClickDialog(option: number = 0): Promise<ActionResult> {
|
|
854
|
-
return this.sendAction({ type: 'clickDialogOption', optionIndex: option, reason: 'SDK' });
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
/** Click a component using IF_BUTTON packet - for simple buttons, spellcasting, etc. */
|
|
858
|
-
async sendClickComponent(componentId: number): Promise<ActionResult> {
|
|
859
|
-
return this.sendAction({ type: 'clickComponent', componentId, reason: 'SDK' });
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
/** Click a component using INV_BUTTON packet - for components with inventory operations (smithing, crafting, etc.) */
|
|
863
|
-
async sendClickComponentWithOption(componentId: number, optionIndex: number = 1, slot: number = 0): Promise<ActionResult> {
|
|
864
|
-
return this.sendAction({ type: 'clickComponentWithOption', componentId, optionIndex, slot, reason: 'SDK' });
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
/** Click an interface option by index. Convenience wrapper that looks up componentId from state. */
|
|
868
|
-
async sendClickInterfaceOption(optionIndex: number): Promise<ActionResult> {
|
|
869
|
-
const state = this.getState();
|
|
870
|
-
if (!state?.interface?.isOpen) {
|
|
871
|
-
return { success: false, message: 'No interface open' };
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
const options = state.interface.options;
|
|
875
|
-
if (optionIndex < 0 || optionIndex >= options.length) {
|
|
876
|
-
return { success: false, message: `Invalid option index ${optionIndex}, interface has ${options.length} options` };
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
const option = options[optionIndex];
|
|
880
|
-
if (!option) {
|
|
881
|
-
return { success: false, message: `Option ${optionIndex} not found` };
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
return this.sendClickComponent(option.componentId);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
/** Accept character design in tutorial. */
|
|
888
|
-
async sendAcceptCharacterDesign(): Promise<ActionResult> {
|
|
889
|
-
return this.sendAction({ type: 'acceptCharacterDesign', reason: 'SDK' });
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
/** Randomize character appearance in tutorial. */
|
|
893
|
-
async sendRandomizeCharacterDesign(): Promise<ActionResult> {
|
|
894
|
-
return this.sendAction({ type: 'randomizeCharacterDesign', reason: 'SDK' });
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
/** Buy from shop by slot and amount. */
|
|
898
|
-
async sendShopBuy(slot: number, amount: number = 1): Promise<ActionResult> {
|
|
899
|
-
return this.sendAction({ type: 'shopBuy', slot, amount, reason: 'SDK' });
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
/** Sell to shop by slot and amount. */
|
|
903
|
-
async sendShopSell(slot: number, amount: number = 1): Promise<ActionResult> {
|
|
904
|
-
return this.sendAction({ type: 'shopSell', slot, amount, reason: 'SDK' });
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
/** Close shop interface. */
|
|
908
|
-
async sendCloseShop(): Promise<ActionResult> {
|
|
909
|
-
return this.sendAction({ type: 'closeShop', reason: 'SDK' });
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/** Close any modal interface. */
|
|
913
|
-
async sendCloseModal(): Promise<ActionResult> {
|
|
914
|
-
return this.sendAction({ type: 'closeModal', reason: 'SDK' });
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
/** Set combat style (0-3). */
|
|
918
|
-
async sendSetCombatStyle(style: number): Promise<ActionResult> {
|
|
919
|
-
return this.sendAction({ type: 'setCombatStyle', style, reason: 'SDK' });
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
// ============ Prayer ============
|
|
923
|
-
|
|
924
|
-
/** Toggle a prayer on or off by name or index (0-14). */
|
|
925
|
-
async sendTogglePrayer(prayer: PrayerName | number): Promise<ActionResult> {
|
|
926
|
-
const index = typeof prayer === 'number' ? prayer : PRAYER_INDICES[prayer];
|
|
927
|
-
if (index === undefined || index < 0 || index > 14) {
|
|
928
|
-
return { success: false, message: `Invalid prayer: ${prayer}` };
|
|
929
|
-
}
|
|
930
|
-
return this.sendAction({ type: 'togglePrayer', prayerIndex: index, reason: 'SDK' });
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/** Get current prayer state from world state. */
|
|
934
|
-
getPrayerState(): PrayerState | null {
|
|
935
|
-
return this.state?.prayers || null;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
/** Check if a specific prayer is currently active. */
|
|
939
|
-
isPrayerActive(prayer: PrayerName | number): boolean {
|
|
940
|
-
const prayerState = this.state?.prayers;
|
|
941
|
-
if (!prayerState) return false;
|
|
942
|
-
const index = typeof prayer === 'number' ? prayer : PRAYER_INDICES[prayer];
|
|
943
|
-
if (index === undefined || index < 0 || index >= prayerState.activePrayers.length) return false;
|
|
944
|
-
return !!prayerState.activePrayers[index];
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
/** Get list of all currently active prayer names. */
|
|
948
|
-
getActivePrayers(): PrayerName[] {
|
|
949
|
-
const prayerState = this.state?.prayers;
|
|
950
|
-
if (!prayerState) return [];
|
|
951
|
-
return prayerState.activePrayers
|
|
952
|
-
.map((active, i) => active ? PRAYER_NAMES[i] : null)
|
|
953
|
-
.filter((name): name is PrayerName => name !== null);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
/** Cast spell on NPC using spell component ID. */
|
|
957
|
-
async sendSpellOnNpc(npcIndex: number, spellComponent: number): Promise<ActionResult> {
|
|
958
|
-
return this.sendAction({ type: 'spellOnNpc', npcIndex, spellComponent, reason: 'SDK' });
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
/** Cast spell on inventory item. */
|
|
962
|
-
async sendSpellOnItem(slot: number, spellComponent: number): Promise<ActionResult> {
|
|
963
|
-
return this.sendAction({ type: 'spellOnItem', slot, spellComponent, reason: 'SDK' });
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
/** Switch to a UI tab by index. */
|
|
967
|
-
async sendSetTab(tabIndex: number): Promise<ActionResult> {
|
|
968
|
-
return this.sendAction({ type: 'setTab', tabIndex, reason: 'SDK' });
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
/** Send a chat message. */
|
|
972
|
-
async sendSay(message: string): Promise<ActionResult> {
|
|
973
|
-
return this.sendAction({ type: 'say', message, reason: 'SDK' });
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
/** Wait for specified number of game ticks. */
|
|
977
|
-
async sendWait(ticks: number = 1): Promise<ActionResult> {
|
|
978
|
-
return this.sendAction({ type: 'wait', ticks, reason: 'SDK' });
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
/** Deposit item to bank by slot. */
|
|
982
|
-
async sendBankDeposit(slot: number, amount: number = 1): Promise<ActionResult> {
|
|
983
|
-
return this.sendAction({ type: 'bankDeposit', slot, amount, reason: 'SDK' });
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
/** Withdraw item from bank by slot. */
|
|
987
|
-
async sendBankWithdraw(slot: number, amount: number = 1): Promise<ActionResult> {
|
|
988
|
-
return this.sendAction({ type: 'bankWithdraw', slot, amount, reason: 'SDK' });
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// ============ Screenshot ============
|
|
992
|
-
|
|
993
|
-
/**
|
|
994
|
-
* Request a screenshot from the bot client.
|
|
995
|
-
* Returns the screenshot as a data URL (data:image/png;base64,...).
|
|
996
|
-
* @param timeout - Timeout in milliseconds (default 10000)
|
|
997
|
-
*/
|
|
998
|
-
async sendScreenshot(timeout: number = 10000): Promise<string> {
|
|
999
|
-
if (this.connectionState === 'reconnecting') {
|
|
1000
|
-
console.log(`[BotSDK] Waiting for reconnection before requesting screenshot`);
|
|
1001
|
-
await this.waitForConnection();
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
if (!this.isConnected()) {
|
|
1005
|
-
throw new Error(`Not connected (state: ${this.connectionState})`);
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
const screenshotId = `ss-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1009
|
-
|
|
1010
|
-
return new Promise((resolve, reject) => {
|
|
1011
|
-
const timeoutHandle = setTimeout(() => {
|
|
1012
|
-
this.pendingScreenshots.delete(screenshotId);
|
|
1013
|
-
reject(new Error('Screenshot request timed out'));
|
|
1014
|
-
}, timeout);
|
|
1015
|
-
|
|
1016
|
-
this.pendingScreenshots.set(screenshotId, { resolve, reject, timeout: timeoutHandle });
|
|
1017
|
-
|
|
1018
|
-
this.send({
|
|
1019
|
-
type: 'sdk_screenshot_request',
|
|
1020
|
-
username: this.config.botUsername,
|
|
1021
|
-
screenshotId
|
|
1022
|
-
});
|
|
1023
|
-
});
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// ============ Local Pathfinding ============
|
|
1027
|
-
|
|
1028
|
-
/** Find path to destination using local collision data. */
|
|
1029
|
-
findPath(
|
|
1030
|
-
destX: number,
|
|
1031
|
-
destZ: number,
|
|
1032
|
-
maxWaypoints: number = 500
|
|
1033
|
-
): { success: boolean; waypoints: Array<{ x: number; z: number; level: number }>; reachedDestination?: boolean; error?: string } {
|
|
1034
|
-
const state = this.getState();
|
|
1035
|
-
if (!state?.player) {
|
|
1036
|
-
return { success: false, waypoints: [], error: 'No player state available' };
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
const { worldX: srcX, worldZ: srcZ, level } = state.player;
|
|
1040
|
-
|
|
1041
|
-
// Only require source zone to be allocated - we need to know where we ARE
|
|
1042
|
-
// Destination zone may be unallocated (e.g., past a gate we haven't opened yet)
|
|
1043
|
-
// The pathfinder will find a partial path to the edge of known areas
|
|
1044
|
-
if (!pathfinding.isZoneAllocated(level, srcX, srcZ)) {
|
|
1045
|
-
return { success: false, waypoints: [], error: 'Source zone not allocated (no collision data for current position)' };
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
const destZoneAllocated = pathfinding.isZoneAllocated(level, destX, destZ);
|
|
1049
|
-
|
|
1050
|
-
// 2048x2048 BFS grid handles any in-game distance in a single call.
|
|
1051
|
-
const waypoints = pathfinding.findLongPath(level, srcX, srcZ, destX, destZ, maxWaypoints);
|
|
1052
|
-
|
|
1053
|
-
// If no waypoints and destination zone isn't allocated, that's expected -
|
|
1054
|
-
// we just can't path there yet (might need to open a door first)
|
|
1055
|
-
if (waypoints.length === 0 && !destZoneAllocated) {
|
|
1056
|
-
// Return success with empty waypoints - caller should try raw walking toward destination
|
|
1057
|
-
return { success: true, waypoints: [], reachedDestination: false, error: 'Destination zone not allocated - try walking toward it' };
|
|
1058
|
-
}
|
|
1059
|
-
const lastWaypoint = waypoints[waypoints.length - 1];
|
|
1060
|
-
const reachedDestination = lastWaypoint !== undefined &&
|
|
1061
|
-
lastWaypoint.x === destX &&
|
|
1062
|
-
lastWaypoint.z === destZ;
|
|
1063
|
-
|
|
1064
|
-
return { success: true, waypoints, reachedDestination };
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
/** Find path to destination (async alias for findPath). */
|
|
1068
|
-
async sendFindPath(
|
|
1069
|
-
destX: number,
|
|
1070
|
-
destZ: number,
|
|
1071
|
-
maxWaypoints: number = 500
|
|
1072
|
-
): Promise<{ success: boolean; waypoints: Array<{ x: number; z: number; level: number }>; reachedDestination?: boolean; error?: string }> {
|
|
1073
|
-
return this.findPath(destX, destZ, maxWaypoints);
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
// ============ Plumbing: State Waiting ============
|
|
1077
|
-
|
|
1078
|
-
/**
|
|
1079
|
-
* Wait for game state to be fully loaded and ready.
|
|
1080
|
-
* Ensures player position is valid (not 0,0), bot is in-game, and state is recent.
|
|
1081
|
-
*
|
|
1082
|
-
* @param timeout - Maximum time to wait in milliseconds (default: 15000)
|
|
1083
|
-
* @returns Promise that resolves when state is ready
|
|
1084
|
-
* @throws Error if timeout is reached
|
|
1085
|
-
*
|
|
1086
|
-
* @example
|
|
1087
|
-
* ```ts
|
|
1088
|
-
* await sdk.waitForReady();
|
|
1089
|
-
* // Now safe to access player position, NPCs, etc.
|
|
1090
|
-
* ```
|
|
1091
|
-
*/
|
|
1092
|
-
async waitForReady(timeout: number = 15000): Promise<BotWorldState> {
|
|
1093
|
-
console.log('[BotSDK] Waiting for game state to be ready...');
|
|
1094
|
-
|
|
1095
|
-
try {
|
|
1096
|
-
const state = await this.waitForCondition(s => {
|
|
1097
|
-
const validPosition = !!(s.player && s.player.worldX !== 0 && s.player.worldZ !== 0);
|
|
1098
|
-
const inGame = s.inGame;
|
|
1099
|
-
const hasEntities = s.nearbyNpcs.length > 0 || s.nearbyLocs.length > 0 || s.groundItems.length > 0;
|
|
1100
|
-
|
|
1101
|
-
// Log progress for debugging
|
|
1102
|
-
if (!validPosition) {
|
|
1103
|
-
console.log(`[BotSDK] Waiting - invalid position: (${s.player?.worldX}, ${s.player?.worldZ})`);
|
|
1104
|
-
} else if (!inGame) {
|
|
1105
|
-
console.log('[BotSDK] Waiting - not in game');
|
|
1106
|
-
} else if (!hasEntities) {
|
|
1107
|
-
console.log('[BotSDK] Waiting - no entities loaded yet');
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
return inGame && validPosition && hasEntities;
|
|
1111
|
-
}, timeout);
|
|
1112
|
-
|
|
1113
|
-
console.log('[BotSDK] Game state ready!');
|
|
1114
|
-
return state;
|
|
1115
|
-
} catch (error) {
|
|
1116
|
-
console.error('[BotSDK] Timeout waiting for game state to be ready');
|
|
1117
|
-
throw new Error('Game state not ready within timeout');
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
async waitForCondition(
|
|
1122
|
-
predicate: (state: BotWorldState) => boolean,
|
|
1123
|
-
timeout: number = 30000
|
|
1124
|
-
): Promise<BotWorldState> {
|
|
1125
|
-
if (this.state && predicate(this.state)) {
|
|
1126
|
-
return this.state;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
return new Promise((resolve, reject) => {
|
|
1130
|
-
const timeoutId = setTimeout(() => {
|
|
1131
|
-
unsubscribe();
|
|
1132
|
-
reject(new Error('waitForCondition timed out'));
|
|
1133
|
-
}, timeout);
|
|
1134
|
-
|
|
1135
|
-
const unsubscribe = this.onStateUpdate((state) => {
|
|
1136
|
-
if (predicate(state)) {
|
|
1137
|
-
clearTimeout(timeoutId);
|
|
1138
|
-
unsubscribe();
|
|
1139
|
-
resolve(state);
|
|
1140
|
-
}
|
|
1141
|
-
});
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
/** Wait for next state update from server. */
|
|
1146
|
-
async waitForStateChange(timeout: number = 30000): Promise<BotWorldState> {
|
|
1147
|
-
return new Promise((resolve, reject) => {
|
|
1148
|
-
const timeoutId = setTimeout(() => {
|
|
1149
|
-
unsubscribe();
|
|
1150
|
-
reject(new Error('waitForStateChange timed out'));
|
|
1151
|
-
}, timeout);
|
|
1152
|
-
|
|
1153
|
-
const unsubscribe = this.onStateUpdate((state) => {
|
|
1154
|
-
clearTimeout(timeoutId);
|
|
1155
|
-
unsubscribe();
|
|
1156
|
-
resolve(state);
|
|
1157
|
-
});
|
|
1158
|
-
});
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
/**
|
|
1162
|
-
* Wait for a specific number of server ticks (~300ms each).
|
|
1163
|
-
*
|
|
1164
|
-
* @param ticks - Number of server ticks to wait
|
|
1165
|
-
* @returns The state after waiting
|
|
1166
|
-
*/
|
|
1167
|
-
async waitForTicks(ticks: number = 1): Promise<BotWorldState> {
|
|
1168
|
-
if (!this.state) {
|
|
1169
|
-
throw new Error('waitForTicks: no state available');
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
if (ticks <= 0) {
|
|
1173
|
-
return this.state;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
const startTick = this.state.tick;
|
|
1177
|
-
const targetTick = startTick + ticks;
|
|
1178
|
-
|
|
1179
|
-
return new Promise((resolve, reject) => {
|
|
1180
|
-
// Safety timeout: ticks * 1s + 5s buffer (server tick is ~300ms, so 1s is generous)
|
|
1181
|
-
const safetyTimeout = setTimeout(() => {
|
|
1182
|
-
unsubscribe();
|
|
1183
|
-
reject(new Error(`waitForTicks(${ticks}) safety timeout - no state updates received`));
|
|
1184
|
-
}, ticks * 1000 + 5000);
|
|
1185
|
-
|
|
1186
|
-
const unsubscribe = this.onStateUpdate((state) => {
|
|
1187
|
-
if (state.tick >= targetTick) {
|
|
1188
|
-
clearTimeout(safetyTimeout);
|
|
1189
|
-
unsubscribe();
|
|
1190
|
-
resolve(state);
|
|
1191
|
-
}
|
|
1192
|
-
});
|
|
1193
|
-
});
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
/**
|
|
1197
|
-
* Wait for the next state update from the server.
|
|
1198
|
-
* This is the most common waiting pattern - ensures fresh data after an action.
|
|
1199
|
-
*
|
|
1200
|
-
* State updates arrive once per server tick (~300ms) when PLAYER_INFO is received.
|
|
1201
|
-
*
|
|
1202
|
-
* @example
|
|
1203
|
-
* ```ts
|
|
1204
|
-
* await sdk.sendClickDialog(0);
|
|
1205
|
-
* await sdk.waitForStateUpdate(); // Wait for server to confirm
|
|
1206
|
-
* ```
|
|
1207
|
-
*
|
|
1208
|
-
* @returns The new state after the update
|
|
1209
|
-
*/
|
|
1210
|
-
async waitForStateUpdate(): Promise<BotWorldState> {
|
|
1211
|
-
return this.waitForStateChange(5000);
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
// ============ Internal ============
|
|
1217
|
-
|
|
1218
|
-
private send(message: object) {
|
|
1219
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
1220
|
-
this.ws.send(JSON.stringify(message));
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
private handleMessage(data: string) {
|
|
1225
|
-
let message: SyncToSDKMessage;
|
|
1226
|
-
try {
|
|
1227
|
-
message = JSON.parse(data);
|
|
1228
|
-
} catch {
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
if (message.type === 'sdk_state' && message.state) {
|
|
1233
|
-
// Filter out player chat messages unless showChat is enabled
|
|
1234
|
-
// Type 2 = public chat, Type 3 = private message received
|
|
1235
|
-
if (!this.config.showChat && message.state.gameMessages) {
|
|
1236
|
-
message.state.gameMessages = message.state.gameMessages.filter(
|
|
1237
|
-
msg => msg.type !== 2 && msg.type !== 3
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
this.state = message.state;
|
|
1242
|
-
// Use server timestamp if available, otherwise use local time
|
|
1243
|
-
this.stateReceivedAt = message.stateReceivedAt || Date.now();
|
|
1244
|
-
for (const listener of this.stateListeners) {
|
|
1245
|
-
try {
|
|
1246
|
-
listener(message.state);
|
|
1247
|
-
} catch (e) {
|
|
1248
|
-
console.error('State listener error:', e);
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
if (message.type === 'sdk_action_result' && message.actionId) {
|
|
1254
|
-
const pending = this.pendingActions.get(message.actionId);
|
|
1255
|
-
if (pending) {
|
|
1256
|
-
clearTimeout(pending.timeout);
|
|
1257
|
-
this.pendingActions.delete(message.actionId);
|
|
1258
|
-
if (message.result) {
|
|
1259
|
-
pending.resolve(message.result);
|
|
1260
|
-
} else {
|
|
1261
|
-
pending.reject(new Error('No result in action response'));
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
if (message.type === 'sdk_error') {
|
|
1267
|
-
if (message.actionId) {
|
|
1268
|
-
const pending = this.pendingActions.get(message.actionId);
|
|
1269
|
-
if (pending) {
|
|
1270
|
-
clearTimeout(pending.timeout);
|
|
1271
|
-
this.pendingActions.delete(message.actionId);
|
|
1272
|
-
pending.reject(new Error(message.error || 'Unknown error'));
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
if (message.screenshotId) {
|
|
1276
|
-
const pending = this.pendingScreenshots.get(message.screenshotId);
|
|
1277
|
-
if (pending) {
|
|
1278
|
-
clearTimeout(pending.timeout);
|
|
1279
|
-
this.pendingScreenshots.delete(message.screenshotId);
|
|
1280
|
-
pending.reject(new Error(message.error || 'Screenshot error'));
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
if (message.type === 'sdk_screenshot_response' && message.dataUrl) {
|
|
1286
|
-
// Try to find by screenshotId first, then fall back to any pending
|
|
1287
|
-
let pending: PendingScreenshot | undefined;
|
|
1288
|
-
if (message.screenshotId) {
|
|
1289
|
-
pending = this.pendingScreenshots.get(message.screenshotId);
|
|
1290
|
-
if (pending) {
|
|
1291
|
-
this.pendingScreenshots.delete(message.screenshotId);
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
// If no screenshotId or not found, resolve the first pending screenshot
|
|
1295
|
-
if (!pending && this.pendingScreenshots.size > 0) {
|
|
1296
|
-
const entry = this.pendingScreenshots.entries().next().value;
|
|
1297
|
-
if (entry) {
|
|
1298
|
-
const [firstId, firstPending] = entry;
|
|
1299
|
-
pending = firstPending;
|
|
1300
|
-
this.pendingScreenshots.delete(firstId);
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
if (pending) {
|
|
1305
|
-
clearTimeout(pending.timeout);
|
|
1306
|
-
pending.resolve(message.dataUrl);
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// Re-export types for convenience
|
|
1313
|
-
export * from './types';
|