bizhawk_read16
Read a 16-bit little-endian value from emulator memory at a specified address. Use for game-state values like HP, score, or coordinates that occupy two bytes.
Instructions
PURPOSE: Read an unsigned 16-bit little-endian value from emulator memory at the given address. USAGE: Use for 16-bit fields (most game-state values: HP, score, coordinates). For single bytes use bizhawk_read8; for 32-bit values use bizhawk_read32; for non-aligned spans or big-endian fields use bizhawk_read_range and decode the bytes yourself (this tool always interprets bytes as little-endian regardless of the target system's native endianness). BEHAVIOR: No side effects — pure read. Reads two consecutive bytes (low byte at address, high byte at address+1) and combines them as little-endian. Returns an error if the named domain doesn't exist, address+2 exceeds domain size, or the core doesn't expose memory.read_u16_le. RETURNS: Single line 'ADDR_HEX: VAL_DEC (0xVAL_HEX)'.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| address | Yes | Byte offset within the chosen memory domain. Per-domain offsets are 0-based and INDEPENDENT of system bus addresses (e.g. SNES WRAM uses 0x09C6, NOT 0x7E09C6). Reads 2 consecutive bytes starting here. Returns an error if address < 0 or address + 2 exceeds the domain's size. | |
| 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:98-113 (schema)Tool definition and input schema for bizhawk_read16. Defines the tool name, description, and input schema with required 'address' (integer, min 0) and optional 'domain' (string) parameters.
{ name: "bizhawk_read16", description: "PURPOSE: Read an unsigned 16-bit little-endian value from emulator memory at the given address. " + "USAGE: Use for 16-bit fields (most game-state values: HP, score, coordinates). For single bytes use bizhawk_read8; for 32-bit values use bizhawk_read32; for non-aligned spans or big-endian fields use bizhawk_read_range and decode the bytes yourself (this tool always interprets bytes as little-endian regardless of the target system's native endianness). " + "BEHAVIOR: No side effects — pure read. Reads two consecutive bytes (low byte at `address`, high byte at `address+1`) and combines them as little-endian. Returns an error if the named domain doesn't exist, address+2 exceeds domain size, or the core doesn't expose memory.read_u16_le. " + "RETURNS: Single line 'ADDR_HEX: VAL_DEC (0xVAL_HEX)'.", inputSchema: { type: "object", required: ["address"], properties: { address: { type: "integer", minimum: 0, description: ADDRESS_PARAM_DESC(2) }, domain: { type: "string", description: DOMAIN_PARAM_DESC }, }, additionalProperties: false, }, - src/tools.ts:537-539 (handler)Handler case for bizhawk_read16 in the CallToolRequestSchema switch statement. Delegates to bh.call('read16', { address, domain }) and formats the result as 'ADDR_HEX: VAL_DEC (0xVAL_HEX)'.
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() }))}`); - src/tools.ts:486-660 (registration)The registerTools function wires up ListToolsRequestSchema and CallToolRequestSchema handlers on the MCP server, making all tools (including bizhawk_read16) available via their definitions in TOOLS array and handled via the switch statement.
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/tools.ts:475-484 (helper)Helper functions used by the bizhawk_read16 handler: ok() formats a success response, fmtHex() formats the numeric value as 'DEC (0xHEX)', and addrHex() formats the address as '0xXXXX'.
function ok(text: string) { return { content: [{ type: "text" as const, text }] }; } function fmtHex(n: unknown): string { if (typeof n !== "number") return String(n); return `${n} (0x${n.toString(16).toUpperCase()})`; } function addrHex(n: number): string { return `0x${n.toString(16).toUpperCase().padStart(4, "0")}`; } - src/bizhawk.ts:235-264 (helper)The BizhawkServer.call() method used by bizhawk_read16 handler to send the 'read16' RPC command to the BizHawk Lua bridge over TCP. Enqueues a command and returns a promise that resolves with the BizHawk response.
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); }); }