将原 claude-dev-stack 目录拆分为独立的 Windows 和 WSL 部署栈,便于分别维护和使用。 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
109 lines
8.0 KiB
JavaScript
109 lines
8.0 KiB
JavaScript
import { z } from "zod";
|
|
import { formatErrorForMcp } from "../utils/errors.js";
|
|
import { coerceNumber } from "../utils/zod-coerce.js";
|
|
export function registerInputTools(server, godot) {
|
|
server.tool("simulate_key", "Simulate a keyboard key press or release in the running game. Use `duration` to hold a key for a set time (auto-releases after). Without duration: keys are NOT auto-released — you must explicitly call with pressed=false to release them.", {
|
|
keycode: z.string().describe("Key constant (e.g. 'KEY_SPACE', 'KEY_W', 'KEY_ESCAPE')"),
|
|
pressed: z.boolean().optional().describe("true for press, false for release (default: true)"),
|
|
duration: coerceNumber().optional().describe("Hold duration in seconds (e.g. 1.5). Key is pressed, held for this duration, then auto-released. Cannot be used with pressed=false."),
|
|
shift: z.boolean().optional().describe("Shift modifier (default: false)"),
|
|
ctrl: z.boolean().optional().describe("Ctrl modifier (default: false)"),
|
|
alt: z.boolean().optional().describe("Alt modifier (default: false)"),
|
|
}, async (params) => {
|
|
try {
|
|
if (params.duration !== undefined && params.duration > 0) {
|
|
const { duration, ...keyParams } = params;
|
|
// Press
|
|
await godot.sendCommand("simulate_key", { ...keyParams, pressed: true });
|
|
// Hold
|
|
await new Promise(resolve => setTimeout(resolve, duration * 1000));
|
|
// Release
|
|
await godot.sendCommand("simulate_key", { ...keyParams, pressed: false });
|
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
event: { keycode: params.keycode, duration, shift: params.shift, ctrl: params.ctrl, alt: params.alt, auto_released: true },
|
|
sent: true
|
|
}, null, 2) }] };
|
|
}
|
|
const result = await godot.sendCommand("simulate_key", params);
|
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
}
|
|
catch (e) {
|
|
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
|
|
}
|
|
});
|
|
server.tool("simulate_mouse_click", "Simulate a mouse button click at a position in the running game. By default sends both press and release (auto_release) so UI buttons work correctly.", {
|
|
x: z.number().optional().describe("X position in viewport (default: 0)"),
|
|
y: z.number().optional().describe("Y position in viewport (default: 0)"),
|
|
button: z.number().optional().describe("Mouse button index: 1=left, 2=right, 3=middle (default: 1)"),
|
|
pressed: z.boolean().optional().describe("true for press, false for release (default: true)"),
|
|
double_click: z.boolean().optional().describe("Double click (default: false)"),
|
|
auto_release: z.boolean().optional().describe("Auto-send release after press so buttons fire (default: true). Set false for drag operations."),
|
|
}, async (params) => {
|
|
try {
|
|
const result = await godot.sendCommand("simulate_mouse_click", params);
|
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
}
|
|
catch (e) {
|
|
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
|
|
}
|
|
});
|
|
server.tool("simulate_mouse_move", "Simulate mouse movement in the running game. Use x/y for absolute viewport positioning (UI interaction), or relative_x/relative_y for relative motion (camera rotation in 3D games, FPS-style look). For 3D camera rotation: relative_x rotates yaw (negative = look left, positive = look right), relative_y rotates pitch (negative = look up, positive = look down). Typical values: 200-400px for a ~90° turn. Use navigate_to tool to calculate exact relative_x needed to face a target.", {
|
|
x: z.number().optional().describe("Absolute X position in viewport (for UI interaction)"),
|
|
y: z.number().optional().describe("Absolute Y position in viewport (for UI interaction)"),
|
|
relative_x: z.number().optional().describe("Relative X movement in pixels. For 3D camera: negative = look left, positive = look right. ~400px ≈ 180° turn"),
|
|
relative_y: z.number().optional().describe("Relative Y movement in pixels. For 3D camera: negative = look up, positive = look down"),
|
|
button_mask: z.number().optional().describe("Mouse button mask to simulate drag. 1=left button held, 2=right button held, 4=middle button held. Required for drag operations like camera pan. (default: 0)"),
|
|
unhandled: z.boolean().optional().describe("Force event to bypass GUI layer and go directly to _unhandled_input(). Auto-enabled when button_mask > 0. Use for camera pan/drag when UI overlays consume mouse events. (default: false)"),
|
|
}, async (params) => {
|
|
try {
|
|
const result = await godot.sendCommand("simulate_mouse_move", params);
|
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
}
|
|
catch (e) {
|
|
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
|
|
}
|
|
});
|
|
server.tool("simulate_action", "Simulate a Godot Input Action (e.g. 'jump', 'move_left') in the running game", {
|
|
action: z.string().describe("Action name as defined in Input Map (e.g. 'jump', 'move_left')"),
|
|
pressed: z.boolean().optional().describe("true for press, false for release (default: true)"),
|
|
strength: z.number().optional().describe("Action strength 0.0-1.0 (default: 1.0)"),
|
|
}, async (params) => {
|
|
try {
|
|
const result = await godot.sendCommand("simulate_action", params);
|
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
}
|
|
catch (e) {
|
|
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
|
|
}
|
|
});
|
|
server.tool("simulate_sequence", "Simulate a sequence of input events with optional frame delays between them. Useful for complex input patterns like press W → wait 30 frames → press Space → wait → release all. After the sequence, use capture_frames to verify the visual result.", {
|
|
events: z.array(z.object({
|
|
type: z.string().describe("Event type: 'key', 'mouse_button', 'mouse_motion', or 'action'"),
|
|
keycode: z.string().optional().describe("For 'key': key constant (e.g. 'KEY_SPACE')"),
|
|
action: z.string().optional().describe("For 'action': action name"),
|
|
button: z.number().optional().describe("For 'mouse_button': button index"),
|
|
pressed: z.boolean().optional().describe("Press state (default: true)"),
|
|
x: z.number().optional().describe("X position for mouse events"),
|
|
y: z.number().optional().describe("Y position for mouse events"),
|
|
relative_x: z.number().optional().describe("Relative X for mouse_motion"),
|
|
relative_y: z.number().optional().describe("Relative Y for mouse_motion"),
|
|
button_mask: z.number().optional().describe("Mouse button mask for mouse_motion drag: 1=left, 2=right, 4=middle"),
|
|
unhandled: z.boolean().optional().describe("Bypass GUI, send directly to _unhandled_input. Auto-enabled for mouse_motion with button_mask > 0"),
|
|
shift: z.boolean().optional(),
|
|
ctrl: z.boolean().optional(),
|
|
alt: z.boolean().optional(),
|
|
strength: z.number().optional(),
|
|
double_click: z.boolean().optional(),
|
|
})).describe("Array of input events to send"),
|
|
frame_delay: z.number().optional().describe("Frames to wait between events (default: 1, 0 = all in one frame)"),
|
|
}, async (params) => {
|
|
try {
|
|
const result = await godot.sendCommand("simulate_sequence", params);
|
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
}
|
|
catch (e) {
|
|
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
|
|
}
|
|
});
|
|
}
|
|
//# sourceMappingURL=input-tools.js.map
|