bizhawk_press_buttons
Set joypad button state for one player for the next emulated frame only. Interleave with frame advance to hold buttons across multiple frames.
Instructions
PURPOSE: Set the joypad button state for one player for EXACTLY the next emulated frame. USAGE: Drive games with input. Each call sets joypad state for ONE frame only — the very next frame BizHawk processes. After that frame, BizHawk's input goes back to whatever the human user is holding (typically nothing). To HOLD a button across N consecutive frames, INTERLEAVE: call bizhawk_press_buttons + bizhawk_frame_advance(count=1) N times in a loop. DO NOT call bizhawk_press_buttons once and then bizhawk_frame_advance(count=N) — only the first of those N frames sees the button; the rest are no-input. Verified empirically against SNES Super Metroid in May 2026: a 60-frame advance after a single press_buttons(Right) moved Samus the same +1 pixel as a 10-frame advance, because frames 2-60 had no input. To release a button mid-hold, just stop calling press_buttons for it; the next frame_advance will see it released. BEHAVIOR: Modifies emulator input state for the next frame poll only — no other side effects. Returns an error if the loaded core doesn't expose joypad.set. Button names that aren't valid for the active core are silently ignored by BizHawk (no error). RETURNS: Single line 'Set joypad N: BUTTON+BUTTON+...' or '... (all released)' if nothing was pressed.
Button names vary per system. Common names across cores: A, B, X, Y, Up, Down, Left, Right, Start, Select, L, R, L1, R1, L2, R2, L3, R3, C, Z, C-Up, C-Down, C-Left, C-Right, Mode. Use whatever names the active core understands — if unsure, try a name and check BizHawk's input display, or use bizhawk_get_info to confirm joypad_set is available.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| buttons | Yes | Map of button name (string, case-sensitive per the active core) → pressed (boolean: true=pressed, false=released). Example: {"A": true, "Up": true} presses A and Up while leaving everything else released. Names not recognized by the active core are silently ignored. | |
| player | No | Player number (1-based). Default 1. For multi-controller cores (e.g. N64 with 4 controllers) pass 2/3/4 to address other players. |
Implementation Reference
- src/tools.ts:234-263 (registration)Tool definition (metadata + inputSchema) for bizhawk_press_buttons as part of the TOOLS array. Registers the tool name, description, and input schema (required 'buttons' map and optional 'player' integer).
{ name: "bizhawk_press_buttons", description: "PURPOSE: Set the joypad button state for one player for EXACTLY the next emulated frame. " + "USAGE: Drive games with input. Each call sets joypad state for ONE frame only — the very next frame BizHawk processes. After that frame, BizHawk's input goes back to whatever the human user is holding (typically nothing). " + "To HOLD a button across N consecutive frames, INTERLEAVE: call bizhawk_press_buttons + bizhawk_frame_advance(count=1) N times in a loop. " + "DO NOT call bizhawk_press_buttons once and then bizhawk_frame_advance(count=N) — only the first of those N frames sees the button; the rest are no-input. Verified empirically against SNES Super Metroid in May 2026: a 60-frame advance after a single press_buttons(Right) moved Samus the same +1 pixel as a 10-frame advance, because frames 2-60 had no input. " + "To release a button mid-hold, just stop calling press_buttons for it; the next frame_advance will see it released. " + "BEHAVIOR: Modifies emulator input state for the next frame poll only — no other side effects. Returns an error if the loaded core doesn't expose joypad.set. Button names that aren't valid for the active core are silently ignored by BizHawk (no error). " + "RETURNS: Single line 'Set joypad N: BUTTON+BUTTON+...' or '... (all released)' if nothing was pressed. " + `\n\nButton names vary per system. Common names across cores: ${VALID_BUTTONS.join(", ")}. Use whatever names the active core understands — if unsure, try a name and check BizHawk's input display, or use bizhawk_get_info to confirm joypad_set is available.`, inputSchema: { type: "object", required: ["buttons"], properties: { buttons: { type: "object", description: "Map of button name (string, case-sensitive per the active core) → pressed (boolean: true=pressed, false=released). Example: {\"A\": true, \"Up\": true} presses A and Up while leaving everything else released. Names not recognized by the active core are silently ignored.", additionalProperties: { type: "boolean" }, }, player: { type: "integer", minimum: 1, default: 1, description: "Player number (1-based). Default 1. For multi-controller cores (e.g. N64 with 4 controllers) pass 2/3/4 to address other players.", }, }, additionalProperties: false, }, }, - src/tools.ts:245-262 (schema)Input schema for bizhawk_press_buttons: requires 'buttons' (object mapping button name strings to booleans) and optional 'player' (integer, min 1, default 1).
inputSchema: { type: "object", required: ["buttons"], properties: { buttons: { type: "object", description: "Map of button name (string, case-sensitive per the active core) → pressed (boolean: true=pressed, false=released). Example: {\"A\": true, \"Up\": true} presses A and Up while leaving everything else released. Names not recognized by the active core are silently ignored.", additionalProperties: { type: "boolean" }, }, player: { type: "integer", minimum: 1, default: 1, description: "Player number (1-based). Default 1. For multi-controller cores (e.g. N64 with 4 controllers) pass 2/3/4 to address other players.", }, }, additionalProperties: false, }, - src/tools.ts:564-569 (handler)Handler implementation for bizhawk_press_buttons: calls bh.call('press_buttons', ...) to set joypad state via the BizHawk Lua bridge, then returns a summary line showing which buttons were pressed.
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)"}`); } - src/tools.ts:486-660 (registration)registerTools function that sets up ListToolsRequestSchema and CallToolRequestSchema handlers, routing tool calls (including 'bizhawk_press_buttons') via a 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:9-16 (helper)VALID_BUTTONS constant listing common button names referenced in the tool description for documentation purposes.
const VALID_BUTTONS = [ "A", "B", "X", "Y", // Face buttons "Up", "Down", "Left", "Right", // D-pad "Start", "Select", // System "L", "R", "L1", "R1", "L2", "R2", "L3", "R3", // Shoulders/triggers/sticks "C", "Z", "C-Up", "C-Down", "C-Left", "C-Right", // N64 "Mode", // Genesis 6-button ];