2026-10-09 12:49:07 +02:00

277 lines
11 KiB
GDScript

@tool
extends RefCounted
## input_map.* command handlers — action (add/remove) and event (bind/unbind).
const Modules := preload("res://addons/godot_mcp_toolkit/core/modules.gd")
const BUILTIN_UI_ACTIONS: Array[String] = [
"ui_accept", "ui_cancel", "ui_focus_next", "ui_focus_prev",
"ui_up", "ui_down", "ui_left", "ui_right",
"ui_page_up", "ui_page_down", "ui_home", "ui_end",
"ui_cut", "ui_copy", "ui_paste", "ui_undo", "ui_redo",
"ui_text_completion_query", "ui_text_completion_accept",
"ui_text_completion_replace",
"ui_text_newline", "ui_text_newline_blank", "ui_text_newline_above",
"ui_text_backspace", "ui_text_backspace_word",
"ui_text_backspace_all_to_left",
"ui_text_delete", "ui_text_delete_word",
"ui_text_delete_all_to_right",
"ui_text_caret_left", "ui_text_caret_word_left",
"ui_text_caret_right", "ui_text_caret_word_right",
"ui_text_caret_up", "ui_text_caret_down",
"ui_text_caret_line_start", "ui_text_caret_line_end",
"ui_text_caret_page_up", "ui_text_caret_page_down",
"ui_text_caret_document_start", "ui_text_caret_document_end",
"ui_text_caret_add_below", "ui_text_caret_add_above",
"ui_text_scroll_up", "ui_text_scroll_down",
"ui_text_select_all", "ui_text_select_word_under_caret",
"ui_text_add_selection_for_next_occurrence",
"ui_text_clear_carets_and_selection",
"ui_text_toggle_insert_mode", "ui_menu", "ui_text_submit",
"ui_graph_duplicate", "ui_graph_delete",
"ui_filedialog_up_one_level", "ui_filedialog_refresh",
"ui_filedialog_show_hidden",
"ui_swap_input_direction",
]
static func register(registry: MCPToolkitCommandRegistry, _server: Node) -> void:
registry.add("input_map.action", func(parameters: Dictionary) -> Dictionary:
return _cmd_input_map_action(parameters)
, MCPToolkitCommandOptions.new().mark_scene_independent())
registry.add("input_map.event", func(parameters: Dictionary) -> Dictionary:
return _cmd_input_map_event(parameters)
, MCPToolkitCommandOptions.new().mark_scene_independent())
# -- Input event helpers ------------------------------------------------------
static func _build_input_event(event: Variant) -> Variant:
if typeof(event) != TYPE_DICTIONARY:
return {"code": "INVALID_PARAMS", "error": "event must be an object"}
var event_type := str(event.get("type", ""))
match event_type:
"key":
var raw_keycode = event.get("keycode", "")
var resolved_keycode := 0
if typeof(raw_keycode) == TYPE_STRING:
var keycode_str: String = raw_keycode
if keycode_str.begins_with("KEY_"):
keycode_str = keycode_str.substr(4)
resolved_keycode = OS.find_keycode_from_string(keycode_str)
if resolved_keycode == 0:
return {"code": "INVALID_PARAMS",
"error": "unknown keycode '%s'; use symbolic names like 'SPACE' / 'A' / 'F1' or raw ints" % raw_keycode}
elif typeof(raw_keycode) == TYPE_INT or typeof(raw_keycode) == TYPE_FLOAT:
resolved_keycode = int(raw_keycode)
else:
return {"code": "INVALID_PARAMS",
"error": "event.keycode must be a string symbolic name or int"}
var key_event := InputEventKey.new()
key_event.physical_keycode = resolved_keycode
key_event.shift_pressed = bool(event.get("shift", false))
key_event.ctrl_pressed = bool(event.get("ctrl", false))
key_event.alt_pressed = bool(event.get("alt", false))
key_event.meta_pressed = bool(event.get("meta", false))
return key_event
"mouse_button":
var mouse_event := InputEventMouseButton.new()
mouse_event.button_index = int(event.get("button_index", 1))
mouse_event.pressed = bool(event.get("pressed", true))
return mouse_event
"joypad_button":
var joypad_button_event := InputEventJoypadButton.new()
joypad_button_event.button_index = int(event.get("button_index", 0))
joypad_button_event.device = int(event.get("device", -1))
return joypad_button_event
"joypad_motion":
var joypad_motion_event := InputEventJoypadMotion.new()
joypad_motion_event.axis = int(event.get("axis", 0))
joypad_motion_event.axis_value = float(event.get("axis_value", 0.0))
joypad_motion_event.device = int(event.get("device", -1))
return joypad_motion_event
_:
return {"code": "INVALID_PARAMS",
"error": "event.type must be one of 'key' | 'mouse_button' | 'joypad_button' | 'joypad_motion' (got %s)" % event_type}
static func _serialise_input_event(event: InputEvent) -> Dictionary:
if event is InputEventKey:
var kc: int = int((event as InputEventKey).physical_keycode)
return {
"type": "key",
"keycode": kc,
"keycode_name": OS.get_keycode_string(kc),
"shift": (event as InputEventKey).shift_pressed,
"ctrl": (event as InputEventKey).ctrl_pressed,
"alt": (event as InputEventKey).alt_pressed,
"meta": (event as InputEventKey).meta_pressed,
}
if event is InputEventMouseButton:
return {
"type": "mouse_button",
"button_index": int((event as InputEventMouseButton).button_index),
"pressed": (event as InputEventMouseButton).pressed,
}
if event is InputEventJoypadButton:
return {
"type": "joypad_button",
"button_index": int((event as InputEventJoypadButton).button_index),
"device": int((event as InputEventJoypadButton).device),
}
if event is InputEventJoypadMotion:
return {
"type": "joypad_motion",
"axis": int((event as InputEventJoypadMotion).axis),
"axis_value": float((event as InputEventJoypadMotion).axis_value),
"device": int((event as InputEventJoypadMotion).device),
}
return {"type": "unknown", "class": event.get_class()}
static func _input_events_equivalent(
event_a: InputEvent, event_b: InputEvent,
) -> bool:
if event_a == null or event_b == null:
return false
if event_a.get_class() != event_b.get_class():
return false
if event_a is InputEventKey and event_b is InputEventKey:
var key_a := event_a as InputEventKey
var key_b := event_b as InputEventKey
return key_a.physical_keycode == key_b.physical_keycode \
and key_a.shift_pressed == key_b.shift_pressed \
and key_a.ctrl_pressed == key_b.ctrl_pressed \
and key_a.alt_pressed == key_b.alt_pressed \
and key_a.meta_pressed == key_b.meta_pressed
if event_a is InputEventMouseButton and event_b is InputEventMouseButton:
var mouse_a := event_a as InputEventMouseButton
var mouse_b := event_b as InputEventMouseButton
return mouse_a.button_index == mouse_b.button_index \
and mouse_a.pressed == mouse_b.pressed
if event_a is InputEventJoypadButton and event_b is InputEventJoypadButton:
var joypad_a := event_a as InputEventJoypadButton
var joypad_b := event_b as InputEventJoypadButton
return joypad_a.button_index == joypad_b.button_index \
and joypad_a.device == joypad_b.device
if event_a is InputEventJoypadMotion and event_b is InputEventJoypadMotion:
var motion_a := event_a as InputEventJoypadMotion
var motion_b := event_b as InputEventJoypadMotion
return motion_a.axis == motion_b.axis \
and motion_a.device == motion_b.device \
and is_equal_approx(motion_a.axis_value, motion_b.axis_value)
return false
static func _persist_input_action(action: String, deadzone: float) -> void:
var events: Array = []
for event in InputMap.action_get_events(action):
events.append(event)
ProjectSettings.set_setting("input/" + action, {
"deadzone": deadzone,
"events": events,
})
var error := ProjectSettings.save()
if error != OK:
push_warning(
"[MCPServer] ProjectSettings.save after input_map mutation failed (err %d, action=%s); change is in-memory only" % [
error, action])
# -- Commands -----------------------------------------------------------------
static func _cmd_input_map_action(parameters: Dictionary) -> Dictionary:
var action := str(parameters.get("action", ""))
if not (action in ["add", "remove"]):
return MCPToolkitError.fail("INVALID_PARAMS",
"action must be 'add' or 'remove' (got '%s')" % action)
var action_name := str(parameters.get("name", ""))
if action_name.is_empty():
return MCPToolkitError.fail("INVALID_PARAMS", "name must be a non-empty string")
if action == "add":
var deadzone_raw = parameters.get("deadzone", 0.5)
var deadzone := float(deadzone_raw) \
if (typeof(deadzone_raw) == TYPE_FLOAT or typeof(deadzone_raw) == TYPE_INT) else 0.5
if deadzone < 0.0 or deadzone > 1.0:
return MCPToolkitError.fail("INVALID_PARAMS",
"deadzone must be in [0.0, 1.0] (got %f)" % deadzone)
if InputMap.has_action(action_name):
return MCPToolkitSuccess.ok({
"status": "returned",
"name": action_name,
"deadzone": InputMap.action_get_deadzone(action_name),
})
InputMap.add_action(action_name, deadzone)
_persist_input_action(action_name, deadzone)
return MCPToolkitSuccess.ok({
"status": "created",
"name": action_name,
"deadzone": deadzone,
})
else:
if not InputMap.has_action(action_name):
return MCPToolkitError.fail("NOT_FOUND", "no action '%s'" % action_name)
if action_name in BUILTIN_UI_ACTIONS:
return MCPToolkitError.fail("INVALID_PARAMS",
"refusing to remove built-in UI action '%s'; use input_map.event to unbind its events instead" % action_name)
InputMap.erase_action(action_name)
if ProjectSettings.has_setting("input/" + action_name):
ProjectSettings.clear("input/" + action_name)
var error := ProjectSettings.save()
if error != OK:
push_warning(
"[MCPServer] ProjectSettings.save after input_map.action remove failed (err %d, action=%s)" % [
error, action_name])
return MCPToolkitSuccess.ok({"name": action_name})
static func _cmd_input_map_event(parameters: Dictionary) -> Dictionary:
var action := str(parameters.get("action", ""))
if not (action in ["bind", "unbind"]):
return MCPToolkitError.fail("INVALID_PARAMS",
"action must be 'bind' or 'unbind' (got '%s')" % action)
var action_name := str(parameters.get("name", ""))
if action_name.is_empty():
return MCPToolkitError.fail("INVALID_PARAMS", "name must be a non-empty string")
if not InputMap.has_action(action_name):
return MCPToolkitError.fail("NOT_FOUND",
"no action '%s'; call input_map.action with action='add' first" % action_name)
var built: Variant = _build_input_event(parameters.get("event", null))
if typeof(built) == TYPE_DICTIONARY and built.has("error"):
return MCPToolkitError.fail(str(built["code"]), str(built["error"]))
var event: InputEvent = built
if action == "bind":
for existing in InputMap.action_get_events(action_name):
if _input_events_equivalent(existing, event):
return MCPToolkitSuccess.ok({
"status": "returned",
"name": action_name,
"event": _serialise_input_event(existing),
})
InputMap.action_add_event(action_name, event)
_persist_input_action(action_name, InputMap.action_get_deadzone(action_name))
return MCPToolkitSuccess.ok({
"status": "created",
"name": action_name,
"event": _serialise_input_event(event),
})
else:
var existing_events := InputMap.action_get_events(action_name)
var matched: InputEvent = null
for existing in existing_events:
if _input_events_equivalent(existing, event):
matched = existing
break
if matched == null:
return MCPToolkitError.fail("NOT_FOUND",
"no matching event on action '%s' (found %d events)" % [
action_name, existing_events.size()])
InputMap.action_erase_event(action_name, matched)
_persist_input_action(action_name, InputMap.action_get_deadzone(action_name))
return MCPToolkitSuccess.ok({
"name": action_name,
"event": _serialise_input_event(matched),
})