163 lines
5.2 KiB
GDScript
163 lines
5.2 KiB
GDScript
@tool
|
|
extends "res://addons/godot_mcp/commands/base_command.gd"
|
|
|
|
const COMMANDS_PATH := "user://mcp_input_commands"
|
|
|
|
|
|
func get_commands() -> Dictionary:
|
|
return {
|
|
"simulate_key": _simulate_key,
|
|
"simulate_mouse_click": _simulate_mouse_click,
|
|
"simulate_mouse_move": _simulate_mouse_move,
|
|
"simulate_action": _simulate_action,
|
|
"simulate_sequence": _simulate_sequence,
|
|
}
|
|
|
|
|
|
func _simulate_key(params: Dictionary) -> Dictionary:
|
|
var result := require_string(params, "keycode")
|
|
if result[1] != null:
|
|
return result[1]
|
|
var keycode: String = result[0]
|
|
|
|
var pressed: bool = optional_bool(params, "pressed", true)
|
|
var shift: bool = optional_bool(params, "shift", false)
|
|
var ctrl: bool = optional_bool(params, "ctrl", false)
|
|
var alt: bool = optional_bool(params, "alt", false)
|
|
|
|
var event := {
|
|
"type": "key",
|
|
"keycode": keycode,
|
|
"pressed": pressed,
|
|
"shift": shift,
|
|
"ctrl": ctrl,
|
|
"alt": alt,
|
|
}
|
|
_write_commands([event])
|
|
return success({"sent": true, "event": event})
|
|
|
|
|
|
func _simulate_mouse_click(params: Dictionary) -> Dictionary:
|
|
var button: int = optional_int(params, "button", 1) # MOUSE_BUTTON_LEFT
|
|
var pressed: bool = optional_bool(params, "pressed", true)
|
|
var double_click: bool = optional_bool(params, "double_click", false)
|
|
var auto_release: bool = optional_bool(params, "auto_release", true)
|
|
var x: float = float(params.get("x", 0))
|
|
var y: float = float(params.get("y", 0))
|
|
|
|
var press_event := {
|
|
"type": "mouse_button",
|
|
"button": button,
|
|
"pressed": pressed,
|
|
"double_click": double_click,
|
|
"position": {"x": x, "y": y},
|
|
}
|
|
|
|
# Auto-release: send press + release in sequence so UI buttons actually fire
|
|
if pressed and auto_release:
|
|
var release_event := press_event.duplicate()
|
|
release_event["pressed"] = false
|
|
var sequence_data := {
|
|
"sequence_events": [press_event, release_event],
|
|
"frame_delay": 1,
|
|
}
|
|
var json := JSON.stringify(sequence_data)
|
|
var file := FileAccess.open(COMMANDS_PATH, FileAccess.WRITE)
|
|
if file == null:
|
|
return error_internal("Failed to write commands: %s" % error_string(FileAccess.get_open_error()))
|
|
file.store_string(json)
|
|
file.close()
|
|
return success({"sent": true, "event": press_event, "auto_release": true})
|
|
|
|
_write_commands([press_event])
|
|
return success({"sent": true, "event": press_event})
|
|
|
|
|
|
func _simulate_mouse_move(params: Dictionary) -> Dictionary:
|
|
var x: float = float(params.get("x", 0))
|
|
var y: float = float(params.get("y", 0))
|
|
var rel_x: float = float(params.get("relative_x", 0))
|
|
var rel_y: float = float(params.get("relative_y", 0))
|
|
var button_mask: int = optional_int(params, "button_mask", 0)
|
|
var unhandled_explicit: bool = params.has("unhandled")
|
|
var unhandled: bool = optional_bool(params, "unhandled", false)
|
|
|
|
var event := {
|
|
"type": "mouse_motion",
|
|
"position": {"x": x, "y": y},
|
|
"relative": {"x": rel_x, "y": rel_y},
|
|
"button_mask": button_mask,
|
|
}
|
|
# Auto-enable unhandled for drag motions (camera-pan use case) ONLY when
|
|
# the caller did NOT explicitly pass an "unhandled" key. If they passed
|
|
# one — true or false — honor it. This lets UI drag-and-drop tests opt
|
|
# back into normal GUI dispatch by passing unhandled: false explicitly.
|
|
if unhandled_explicit:
|
|
event["unhandled"] = unhandled
|
|
elif button_mask > 0:
|
|
event["unhandled"] = true
|
|
_write_commands([event])
|
|
return success({"sent": true, "event": event})
|
|
|
|
|
|
func _simulate_action(params: Dictionary) -> Dictionary:
|
|
var result := require_string(params, "action")
|
|
if result[1] != null:
|
|
return result[1]
|
|
var action_name: String = result[0]
|
|
|
|
var pressed: bool = optional_bool(params, "pressed", true)
|
|
var strength: float = float(params.get("strength", 1.0))
|
|
|
|
var event := {
|
|
"type": "action",
|
|
"action": action_name,
|
|
"pressed": pressed,
|
|
"strength": strength,
|
|
}
|
|
_write_commands([event])
|
|
return success({"sent": true, "event": event})
|
|
|
|
|
|
func _simulate_sequence(params: Dictionary) -> Dictionary:
|
|
if not params.has("events") or not params["events"] is Array:
|
|
return error_invalid_params("Missing required parameter: events (Array)")
|
|
|
|
var events: Array = params["events"]
|
|
if events.is_empty():
|
|
return error_invalid_params("Events array is empty")
|
|
|
|
var frame_delay: int = optional_int(params, "frame_delay", 1)
|
|
|
|
for event_data: Dictionary in events:
|
|
if not event_data.has("type") or (event_data["type"] as String).is_empty():
|
|
return error_invalid_params("Invalid event in sequence: %s" % str(event_data))
|
|
|
|
if frame_delay <= 0:
|
|
# All events in one frame - write as plain array
|
|
_write_commands(events)
|
|
else:
|
|
# Sequence with frame delay - game side handles timing
|
|
var sequence_data := {
|
|
"sequence_events": events,
|
|
"frame_delay": frame_delay,
|
|
}
|
|
var json := JSON.stringify(sequence_data)
|
|
var file := FileAccess.open(COMMANDS_PATH, FileAccess.WRITE)
|
|
if file == null:
|
|
return error_internal("Failed to write commands: %s" % error_string(FileAccess.get_open_error()))
|
|
file.store_string(json)
|
|
file.close()
|
|
|
|
return success({"sent": true, "event_count": events.size(), "frame_delay": frame_delay})
|
|
|
|
|
|
func _write_commands(events: Array) -> void:
|
|
var json := JSON.stringify(events)
|
|
var file := FileAccess.open(COMMANDS_PATH, FileAccess.WRITE)
|
|
if file == null:
|
|
push_error("[MCP Input] Failed to write commands: %s" % error_string(FileAccess.get_open_error()))
|
|
return
|
|
file.store_string(json)
|
|
file.close()
|