bizhawk_read_range
Read contiguous bytes from emulator memory as a hex dump. Supports up to 4 KiB per call, reducing round-trips for memory inspection.
Instructions
PURPOSE: Read a contiguous range of bytes from emulator memory as a hex dump. USAGE: Use for >4 bytes (one round-trip vs N frame-latency hops). Max 4096 bytes/call (BizHawk serialization limit); chunk larger reads in 4 KiB. Powers the two-snapshot RAM-hunt workflow (snapshot before/after a known change, diff for matching deltas). BEHAVIOR: No side effects — pure read. Returns an error if domain is unknown, length is out of 1-4096, or address+length exceeds the domain. RETURNS: 'ADDR_HEX [N bytes, DOMAIN]:' header + space-separated 2-digit uppercase hex bytes.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| address | Yes | Starting byte offset within the chosen memory domain (0-based per-domain, NOT a system-bus address). Reads [address, address+length). | |
| length | Yes | Number of bytes to read (1-4096; hard cap is BizHawk's per-call serialization limit). | |
| domain | No | Optional case-sensitive memory domain name. Omit to use BizHawk's currently selected domain (see bizhawk_get_info → current_memory_domain). Discover available names with bizhawk_list_memory_domains; they vary per system (WRAM on SNES, RAM on NES, RDRAM on N64, 68K RAM on Genesis, MainRAM on PSX, EWRAM/IWRAM on GBA). Returns an error if the name doesn't match any domain on the loaded core. |
Implementation Reference
- src/tools.ts:132-149 (schema)Tool definition (schema) for 'bizhawk_read_range' — declares name, description, and inputSchema requiring 'address' (integer) and 'length' (1-4096 integer), with optional 'domain' string.
{ name: "bizhawk_read_range", description: "PURPOSE: Read a contiguous range of bytes from emulator memory as a hex dump. " + "USAGE: Use for >4 bytes (one round-trip vs N frame-latency hops). Max 4096 bytes/call (BizHawk serialization limit); chunk larger reads in 4 KiB. Powers the two-snapshot RAM-hunt workflow (snapshot before/after a known change, diff for matching deltas). " + "BEHAVIOR: No side effects — pure read. Returns an error if domain is unknown, length is out of 1-4096, or address+length exceeds the domain. " + "RETURNS: 'ADDR_HEX [N bytes, DOMAIN]:' header + space-separated 2-digit uppercase hex bytes.", inputSchema: { type: "object", required: ["address", "length"], properties: { address: { type: "integer", minimum: 0, description: "Starting byte offset within the chosen memory domain (0-based per-domain, NOT a system-bus address). Reads [address, address+length)." }, length: { type: "integer", minimum: 1, maximum: 4096, description: "Number of bytes to read (1-4096; hard cap is BizHawk's per-call serialization limit)." }, domain: { type: "string", description: DOMAIN_PARAM_DESC }, }, additionalProperties: false, }, }, - src/tools.ts:541-545 (handler)Handler for 'bizhawk_read_range' — calls bh.call("read_range") with address, length, and optional domain; formats the returned byte array as space-separated uppercase hex.
case "bizhawk_read_range": { const bytes = await bh.call<number[]>("read_range", { address: a(), length: p.length, ...dom() }); const hex = bytes.map((b) => b.toString(16).padStart(2, "0").toUpperCase()).join(" "); return ok(`${addrHex(a())} [${bytes.length} bytes${p.domain ? `, ${p.domain}` : ""}]:\n${hex}`); } - src/tools.ts:486-660 (registration)Registration of all tools via server.setRequestHandler, including bizhawk_read_range as a case in the CallToolRequestSchema switch.
export function registerTools(server: Server, bh: BizhawkServer): void { server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS })); server.setRequestHandler(CallToolRequestSchema, async (req) => { const { name, arguments: args = {} } = req.params; const p = args as Record<string, unknown>; const a = () => p.address as number; const dom = () => p.domain ? { domain: p.domain } : {}; switch (name) { case "bizhawk_ping": { const r = await bh.call<string>("ping"); return ok(r); } case "bizhawk_get_info": { const r = await bh.call<{ rom_name?: string; rom_hash?: string; framecount?: number; memory_domains?: string[]; current_memory_domain?: string; capabilities?: Record<string, boolean>; }>("get_info"); const lines = [ `ROM: ${r.rom_name ?? "(unavailable)"}`, `ROM hash: ${r.rom_hash ?? "(unavailable)"}`, `Framecount: ${r.framecount ?? "(unavailable)"}`, ]; if (r.memory_domains?.length) { lines.push(""); lines.push(`Memory domains: ${r.memory_domains.join(", ")}`); if (r.current_memory_domain) { lines.push(`Active domain (used when 'domain' is omitted): ${r.current_memory_domain}`); } } if (r.capabilities) { const missing = Object.entries(r.capabilities).filter(([, v]) => !v).map(([k]) => k); if (missing.length) { lines.push(""); lines.push(`Missing capabilities on this BizHawk build: ${missing.join(", ")}`); } } return ok(lines.join("\n")); } case "bizhawk_list_memory_domains": { const r = await bh.call<string[]>("list_memory_domains"); return ok("Memory domains:\n " + r.join("\n ")); } case "bizhawk_read8": return ok(`${addrHex(a())}: ${fmtHex(await bh.call<number>("read8", { address: a(), ...dom() }))}`); case "bizhawk_read16": return ok(`${addrHex(a())}: ${fmtHex(await bh.call<number>("read16", { address: a(), ...dom() }))}`); case "bizhawk_read32": return ok(`${addrHex(a())}: ${fmtHex(await bh.call<number>("read32", { address: a(), ...dom() }))}`); case "bizhawk_read_range": { const bytes = await bh.call<number[]>("read_range", { address: a(), length: p.length, ...dom() }); const hex = bytes.map((b) => b.toString(16).padStart(2, "0").toUpperCase()).join(" "); return ok(`${addrHex(a())} [${bytes.length} bytes${p.domain ? `, ${p.domain}` : ""}]:\n${hex}`); } case "bizhawk_write8": { await bh.call("write8", { address: a(), value: p.value, ...dom() }); return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}${p.domain ? ` (${p.domain})` : ""}`); } case "bizhawk_write16": { await bh.call("write16", { address: a(), value: p.value, ...dom() }); return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}${p.domain ? ` (${p.domain})` : ""}`); } case "bizhawk_write32": { await bh.call("write32", { address: a(), value: p.value, ...dom() }); return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}${p.domain ? ` (${p.domain})` : ""}`); } case "bizhawk_write_range": { const r = await bh.call<{ written: number }>("write_range", { address: a(), bytes: p.bytes, ...dom() }); return ok(`Wrote ${r.written} bytes → ${addrHex(a())}${p.domain ? ` (${p.domain})` : ""}`); } case "bizhawk_press_buttons": { await bh.call("press_buttons", { buttons: p.buttons, player: p.player ?? 1 }); const pressed = Object.entries(p.buttons as Record<string, boolean>) .filter(([, v]) => v).map(([k]) => k); return ok(`Set joypad ${p.player ?? 1}: ${pressed.length ? pressed.join("+") : "(all released)"}`); } case "bizhawk_play_input_sequence": { const params: Record<string, unknown> = { frames: p.frames }; if (p.screenshot_every !== undefined) params.screenshot_every = p.screenshot_every; if (p.screenshot_dir !== undefined) params.screenshot_dir = p.screenshot_dir; if (p.screenshot_prefix !== undefined) params.screenshot_prefix = p.screenshot_prefix; if (p.observe_memory !== undefined) params.observe_memory = p.observe_memory; if (p.stop_on_memory_change !== undefined) params.stop_on_memory_change = p.stop_on_memory_change; const r = await bh.call<{ played: number; final_framecount?: number; stopped_early?: boolean; stop_reason?: string; observations?: { frame_offset: number; path?: string; memory?: Record<string, number>; }[]; }>("play_input_sequence", params); const obs = r.observations ?? []; const lines = [ `Played ${r.played} frames. Final framecount: ${r.final_framecount ?? "(unavailable)"}.`, ]; if (r.stopped_early) { lines.push(`Stopped early — reason: ${r.stop_reason ?? "(unspecified)"}.`); } lines.push(`Captured ${obs.length} observation${obs.length === 1 ? "" : "s"}.`); // Per-observation lines so the agent can correlate inline images with state for (let i = 0; i < obs.length; i++) { const o = obs[i]; const memStr = o.memory ? ` memory={${Object.entries(o.memory).map(([k, v]) => `${k}=${v}`).join(", ")}}` : ""; const imgStr = o.path ? ` (image ${i + 1})` : ""; lines.push(` obs[${i}] frame_offset=${o.frame_offset}${memStr}${imgStr}`); } // Build the multi-content response: text summary + per-observation // inline image blocks. We read each PNG from disk (Lua wrote it), // base64-encode for MCP transport. const content: ({ type: "text"; text: string } | { type: "image"; data: string; mimeType: string })[] = [ { type: "text", text: lines.join("\n") }, ]; const fs = await import("node:fs"); for (const o of obs) { if (!o.path) continue; try { const bytes = fs.readFileSync(o.path); content.push({ type: "image", data: bytes.toString("base64"), mimeType: "image/png", }); } catch (err) { content.push({ type: "text", text: `(failed to read observation at frame ${o.frame_offset} from ${o.path}: ${(err as Error).message})`, }); } } return { content }; } case "bizhawk_pause": await bh.call("pause"); return ok("Emulation paused"); case "bizhawk_unpause": await bh.call("unpause"); return ok("Emulation resumed"); case "bizhawk_reset": await bh.call("reset"); return ok("Core reset"); case "bizhawk_frame_advance": { const f = await bh.call<number>("frame_advance", { count: p.count ?? 1 }); return ok(`Advanced ${p.count ?? 1} frame(s). Framecount: ${f}`); } case "bizhawk_screenshot": { const path = await bh.call<string>("screenshot", { path: p.path }); return ok(`Screenshot saved: ${path}`); } case "bizhawk_save_state": { const r = await bh.call<{ path: string }>("save_state", { path: p.path }); return ok(`Saved state to ${r.path}`); } case "bizhawk_load_state": { const r = await bh.call<{ path: string }>("load_state", { path: p.path }); return ok(`Loaded state from ${r.path}`); } default: throw new Error(`Unknown tool: ${name}`); } }); } - src/bizhawk.ts:235-264 (helper)BizhawkServer.call() — generic RPC call method that enqueues commands (including 'read_range') and sends them to the BizHawk Lua bridge.
async call<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> { return new Promise<T>((resolve, reject) => { const id = this.nextId++; const pending: PendingCmd = { id, method, params, resolve: (r) => resolve(r as T), reject, }; const timer = setTimeout(() => { // Drop from queue if still waiting; from inflight if already sent. this.queue = this.queue.filter((p) => p.id !== id); this.inflight.delete(id); if (this.inflight.size === 0) this.awaitingResult = false; reject(new Error( `BizHawk call "${method}" timed out (${this.timeoutMs}ms) — ` + `is the bridge.lua script still polling?`, )); }, this.timeoutMs); // Wrap so the timer always clears const origResolve = pending.resolve, origReject = pending.reject; pending.resolve = (r) => { clearTimeout(timer); origResolve(r); }; pending.reject = (e) => { clearTimeout(timer); origReject(e); }; this.queue.push(pending); }); }