ppsspp_press_buttons
Set persistent PSP button states to control emulated games. Buttons remain held until you send a release command.
Instructions
PURPOSE: Set the PSP joypad button state — the buttons in the map are 'held' until you send another buttons command. USAGE: Drive games with input. Unlike one-frame-only schemes on other emulators, PPSSPP's input.buttons.send updates the persistent button state — the buttons stay held until you call ppsspp_press_buttons again with them set false (or use ppsspp_press_button for a timed one-shot). To release all buttons, call with all keys set to false. BEHAVIOR: Modifies emulator input state until changed. PSP buttons (case-sensitive): cross, circle, triangle, square, up, down, left, right, start, select, ltrigger, rtrigger, home. Unrecognized button names return an error. RETURNS: Single line 'Set buttons: BUTTON+BUTTON+...' or '... (all released)' if nothing was pressed.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| buttons | Yes | Map of PSP button name → pressed (boolean). Valid names: cross, circle, triangle, square, up, down, left, right, start, select, ltrigger, rtrigger, home. Example: {"cross": true, "right": true} holds X and Right. |
Implementation Reference
- src/tools.ts:223-241 (schema)Tool definition (name, description, and inputSchema) for ppsspp_press_buttons. Describes setting persistent held-button state via PPSSPP's input.buttons.send. Input is a map of button name -> boolean.
{ name: "ppsspp_press_buttons", description: "PURPOSE: Set the PSP joypad button state — the buttons in the map are 'held' until you send another buttons command. " + "USAGE: Drive games with input. Unlike one-frame-only schemes on other emulators, PPSSPP's input.buttons.send updates the persistent button state — the buttons stay held until you call ppsspp_press_buttons again with them set false (or use ppsspp_press_button for a timed one-shot). To release all buttons, call with all keys set to false. " + `BEHAVIOR: Modifies emulator input state until changed. PSP buttons (case-sensitive): ${PSP_BUTTONS.join(", ")}. Unrecognized button names return an error. ` + "RETURNS: Single line 'Set buttons: BUTTON+BUTTON+...' or '... (all released)' if nothing was pressed.", inputSchema: { type: "object", required: ["buttons"], properties: { buttons: { type: "object", description: `Map of PSP button name → pressed (boolean). Valid names: ${PSP_BUTTONS.join(", ")}. Example: {"cross": true, "right": true} holds X and Right.`, additionalProperties: { type: "boolean" }, }, }, additionalProperties: false, }, - src/tools.ts:476-481 (handler)Handler for ppsspp_press_buttons tool. Calls ppsspp. call('input.buttons.send') with the button map, then returns a summary of which buttons are pressed.
case "ppsspp_press_buttons": { await pp.call("input.buttons.send", { buttons: p.buttons }); const pressed = Object.entries(p.buttons as Record<string, boolean>) .filter(([, v]) => v).map(([k]) => k); return ok(`Set buttons: ${pressed.length ? pressed.join("+") : "(all released)"}`); } - src/tools.ts:10-16 (helper)Canonical PSP button names array used by the tool's description and input schema documentation. Includes: cross, circle, triangle, square, up, down, left, right, start, select, ltrigger, rtrigger, home.
const PSP_BUTTONS = [ "cross", "circle", "triangle", "square", // Face buttons "up", "down", "left", "right", // D-pad "start", "select", // System "ltrigger", "rtrigger", // Shoulder buttons "home", // Home ]; - src/tools.ts:405-613 (registration)Registration of all tools (including ppsspp_press_buttons) via server.setRequestHandler for ListToolsRequestSchema and the switch-case in CallToolRequestSchema.
export function registerTools(server: Server, pp: PpssppClient): 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; switch (name) { case "ppsspp_ping": { const r = await pp.call<{ version?: string; name?: string }>("version"); return ok(`pong (${r.name ?? "PPSSPP"} ${r.version ?? "(unknown version)"})`); } case "ppsspp_get_info": { const status = await pp.call<{ game?: { id?: string; title?: string; version?: string } | null; paused?: boolean; stepping?: boolean }>("game.status"); const lines: string[] = []; if (status.game) { lines.push(`Title: ${status.game.title ?? "(unavailable)"}`); lines.push(`Disc ID: ${status.game.id ?? "(unavailable)"}`); lines.push(`Version: ${status.game.version ?? "(unavailable)"}`); } else { lines.push("No game loaded."); } const state = status.stepping ? "stepping (paused)" : status.paused ? "paused" : "running"; lines.push(`State: ${state}`); return ok(lines.join("\n")); } case "ppsspp_read8": { const r = await pp.call<{ value: number }>("memory.read_u8", { address: a() }); return ok(`${addrHex(a())}: ${fmtHex(r.value)}`); } case "ppsspp_read16": { const r = await pp.call<{ value: number }>("memory.read_u16", { address: a() }); return ok(`${addrHex(a())}: ${fmtHex(r.value)}`); } case "ppsspp_read32": { const r = await pp.call<{ value: number }>("memory.read_u32", { address: a() }); return ok(`${addrHex(a())}: ${fmtHex(r.value)}`); } case "ppsspp_read_range": { const r = await pp.call<{ base64: string }>("memory.read", { address: a(), size: p.size }); const bytes = Buffer.from(r.base64 ?? "", "base64"); const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0").toUpperCase()).join(" "); return ok(`${addrHex(a())} [${bytes.length} bytes]:\n${hex}`); } case "ppsspp_read_string": { const r = await pp.call<{ value: string }>("memory.readString", { address: a(), type: "utf-8" }); return ok(`${addrHex(a())}: ${JSON.stringify(r.value ?? "")}`); } case "ppsspp_write8": { await pp.call("memory.write_u8", { address: a(), value: p.value }); return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}`); } case "ppsspp_write16": { await pp.call("memory.write_u16", { address: a(), value: p.value }); return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}`); } case "ppsspp_write32": { await pp.call("memory.write_u32", { address: a(), value: p.value }); return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}`); } case "ppsspp_write_range": { const bytes = Buffer.from(p.bytes as number[]); const base64 = bytes.toString("base64"); await pp.call("memory.write", { address: a(), base64 }); return ok(`Wrote ${bytes.length} bytes → ${addrHex(a())}`); } case "ppsspp_press_buttons": { await pp.call("input.buttons.send", { buttons: p.buttons }); const pressed = Object.entries(p.buttons as Record<string, boolean>) .filter(([, v]) => v).map(([k]) => k); return ok(`Set buttons: ${pressed.length ? pressed.join("+") : "(all released)"}`); } case "ppsspp_press_button": { await pp.call("input.buttons.press", { button: p.button, duration: p.duration ?? 1 }); return ok(`Pressed ${p.button} for ${p.duration ?? 1} frames (auto-released)`); } case "ppsspp_send_analog": { await pp.call("input.analog.send", { stick: p.stick, x: p.x, y: p.y }); return ok(`Set analog stick ${p.stick} to (${p.x}, ${p.y})`); } case "ppsspp_pause": { // cpu.stepping is fire-and-forget per PPSSPP source ("No immediate // response. Once CPU is stepping, a 'cpu.stepping' event will be // sent."). Send it, then poll cpu.status until stepping=true. await pp.fireAndForget("cpu.stepping"); await pp.waitForState((s) => s.stepping === true); return ok("Emulation paused"); } case "ppsspp_resume": { await pp.fireAndForget("cpu.resume"); await pp.waitForState((s) => s.stepping === false); return ok("Emulation resumed"); } case "ppsspp_step": { const r = await pp.call<{ pc?: number }>("cpu.stepInto"); return ok(`Stepped one instruction. PC: ${r.pc !== undefined ? addrHex(r.pc) : "(unknown)"}`); } case "ppsspp_reset": { await pp.call("game.reset"); return ok("Game reset"); } case "ppsspp_screenshot": { // PPSSPP's gpu.buffer.* events all require CORE_STEPPING_CPU (or GPU // stepping) state — they fail with "Neither CPU or GPU is stepping" // otherwise. We transparently pause→capture→resume so callers can // screenshot any time without managing pause state. If the emulator // was already paused, we leave it paused. // // source='render' (default) uses gpu.buffer.renderColor → reads the // active GPU render target. Safer: GPU_GetCurrentFramebuffer hits a // different code path than the crash-prone GPU_GetOutputFramebuffer. // // source='output' uses gpu.buffer.screenshot → reads the final // composited output (what's on screen, post scaling/shaders). Can // CRASH PPSSPP on some games: upstream has an `_assert_(buf != nullptr)` // after GPU_GetOutputFramebuffer that fires when the function returns // true with a null buffer (observed on some homebrew). We can't catch // a process abort from outside, but v0.1.2's auto-reconnect means MCP // recovers when PPSSPP is relaunched. const source = (p.source as string | undefined) ?? "render"; const event = source === "output" ? "gpu.buffer.screenshot" : "gpu.buffer.renderColor"; const statusBefore = await pp.call<{ stepping?: boolean; paused?: boolean }>("cpu.status"); const wasStepping = !!statusBefore.stepping; if (!wasStepping) { await pp.fireAndForget("cpu.stepping"); await pp.waitForState((s) => s.stepping === true); } try { // type: "base64" returns the raw base64 payload; the default "uri" // returns a "data:image/png;base64,..." prefix which we'd have to strip. const r = await pp.call<{ base64?: string; uri?: string }>(event, { type: "base64" }); let b64 = r.base64; if (!b64 && r.uri) { // Belt-and-suspenders: if PPSSPP returned a URI anyway, strip the prefix. const m = /^data:image\/png;base64,(.*)$/.exec(r.uri); if (m) b64 = m[1]; } if (!b64) { throw new Error(`PPSSPP did not return screenshot data from ${event} (no game loaded, or framebuffer not readable?)`); } return { content: [ { type: "text" as const, text: `Screenshot captured (source: ${source}, event: ${event}).` }, { type: "image" as const, data: b64, mimeType: "image/png" }, ], }; } finally { if (!wasStepping) { try { await pp.fireAndForget("cpu.resume"); await pp.waitForState((s) => s.stepping === false, { timeoutMs: 2000 }); } catch { /* best-effort */ } } } } case "ppsspp_get_registers": { // PPSSPP's cpu.getAllRegs returns categories with PARALLEL arrays: // { categories: [{ name, registerNames: [...], uintValues: [...], floatValues: [...] }] } // Not an array of {name, value} objects as I first assumed. const r = await pp.call<{ categories?: Array<{ name: string; registerNames?: string[]; uintValues?: number[]; floatValues?: string[]; }>; }>("cpu.getAllRegs"); const lines: string[] = []; for (const cat of r.categories ?? []) { lines.push(`── ${cat.name} ──`); const names = cat.registerNames ?? []; const vals = cat.uintValues ?? []; for (let i = 0; i < Math.max(names.length, vals.length); i++) { const nm = names[i] ?? `r${i}`; const v = vals[i]; lines.push(` ${nm.padEnd(8)} = ${v !== undefined ? addrHex(v) : "(unavailable)"}`); } } return ok(lines.join("\n") || "(no registers returned)"); } case "ppsspp_breakpoint_add": { await pp.call("cpu.breakpoint.add", { address: a() }); return ok(`Breakpoint added at ${addrHex(a())}`); } case "ppsspp_breakpoint_remove": { await pp.call("cpu.breakpoint.remove", { address: a() }); return ok(`Breakpoint removed at ${addrHex(a())}`); } case "ppsspp_breakpoint_list": { const r = await pp.call<{ breakpoints?: Array<{ address: number; enabled?: boolean; condition?: string }> }>("cpu.breakpoint.list"); const bps = r.breakpoints ?? []; if (bps.length === 0) return ok("No breakpoints set."); const lines = bps.map((b) => ` ${addrHex(b.address)} ${b.enabled === false ? "(disabled)" : ""}${b.condition ? ` if ${b.condition}` : ""}`); return ok(`${bps.length} breakpoint${bps.length === 1 ? "" : "s"}:\n${lines.join("\n")}`); } default: throw new Error(`Unknown tool: ${name}`); } }); }