277 lines
11 KiB
GDScript
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),
|
|
})
|