@tool extends RefCounted const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd") ## Handles input action listing, creation, removal, and event binding. ## Actions are persisted via ProjectSettings so they survive editor restarts. func list_actions(params: Dictionary) -> Dictionary: var include_builtin: bool = params.get("include_builtin", false) ## Authoritative source for user-authored actions is the ``[input]`` ## section of ``project.godot``. ``ProjectSettings.has_setting`` is not ## reliable here because Godot registers ``ui_*`` defaults via ## ``GLOBAL_DEF_BASIC``, which makes ``has_setting`` return true for ## them. Reading the file via ``ConfigFile`` distinguishes the user's ## entries from engine-registered defaults regardless of namespace. ## See #213. var user_authored := _read_user_authored_actions() var actions: Array[Dictionary] = [] for action_name in InputMap.get_actions(): var name_str := str(action_name) var is_user_action := user_authored.has(name_str) if not include_builtin and not is_user_action: continue var events: Array[Dictionary] = [] for event in InputMap.action_get_events(action_name): events.append(_serialize_event(event)) actions.append({ "name": name_str, "events": events, "event_count": events.size(), "is_builtin": not is_user_action, }) return {"data": {"actions": actions, "count": actions.size()}} func _read_user_authored_actions() -> Dictionary: var cfg := ConfigFile.new() if cfg.load("res://project.godot") != OK: return {} if not cfg.has_section("input"): return {} var result: Dictionary = {} for key in cfg.get_section_keys("input"): result[key] = true return result func add_action(params: Dictionary) -> Dictionary: var action: String = params.get("action", "") var deadzone: float = params.get("deadzone", 0.5) if action.is_empty(): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: action") if deadzone < 0.0 or deadzone > 1.0: return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "deadzone must be in [0.0, 1.0] (got %s). Typical values are 0.2-0.5; default is 0.5." % deadzone) if InputMap.has_action(action): return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "Action '%s' already exists" % action) InputMap.add_action(action, deadzone) var key := "input/%s" % action ProjectSettings.set_setting(key, { "deadzone": deadzone, "events": [], }) var err := ProjectSettings.save() if err != OK: InputMap.erase_action(action) ProjectSettings.clear(key) return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to save project settings while adding action '%s': %s (error %d)" % [action, error_string(err), err]) return { "data": { "action": action, "deadzone": deadzone, "undoable": false, "reason": "Input actions are saved to project.godot", } } func remove_action(params: Dictionary) -> Dictionary: var action: String = params.get("action", "") if action.is_empty(): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: action") if not InputMap.has_action(action): return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Action '%s' not found" % action) var key := "input/%s" % action var old_setting = ProjectSettings.get_setting(key) if ProjectSettings.has_setting(key) else null InputMap.erase_action(action) if old_setting != null: ProjectSettings.clear(key) var err := ProjectSettings.save() if err != OK: var dz: float = old_setting.get("deadzone", 0.5) if old_setting is Dictionary else 0.5 InputMap.add_action(action, dz) if old_setting is Dictionary: for ev in old_setting.get("events", []): if ev is InputEvent: InputMap.action_add_event(action, ev) ProjectSettings.set_setting(key, old_setting) return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to save project settings while removing action '%s': %s (error %d)" % [action, error_string(err), err]) return { "data": { "action": action, "removed": true, "undoable": false, "reason": "Input actions are saved to project.godot", } } func bind_event(params: Dictionary) -> Dictionary: var action: String = params.get("action", "") var event_type: String = params.get("event_type", "") if action.is_empty(): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: action") if event_type.is_empty(): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: event_type") if not InputMap.has_action(action): return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Action '%s' not found. Call input_map_manage(op='add_action', params={action: '%s'}) first." % [action, action]) var event_or_error = _create_event(event_type, params) if event_or_error is Dictionary: return event_or_error var event: InputEvent = event_or_error InputMap.action_add_event(action, event) var err := _save_action_events(action) if err != OK: InputMap.action_erase_event(action, event) return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to save project settings while binding event to action '%s': %s (error %d)" % [action, error_string(err), err]) return { "data": { "action": action, "event": _serialize_event(event), "undoable": false, "reason": "Input bindings are saved to project.godot", } } ## Returns an InputEvent on success, or a Dictionary error on failure. ## Caller must check ``result is Dictionary`` before treating it as an event. func _create_event(event_type: String, params: Dictionary): match event_type: "key": var ev := InputEventKey.new() var keycode_str: String = params.get("keycode", "") if keycode_str.is_empty(): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "event_type='key' requires keycode (e.g. 'Space', 'A', 'Enter', 'Escape', 'F1').") ev.keycode = OS.find_keycode_from_string(keycode_str) if ev.keycode == KEY_NONE: return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid keycode '%s'. Use Godot keycode names like 'A', 'Space', 'Enter', 'Escape', 'F1', 'Left', 'Right'." % keycode_str) ev.ctrl_pressed = params.get("ctrl", false) ev.alt_pressed = params.get("alt", false) ev.shift_pressed = params.get("shift", false) ev.meta_pressed = params.get("meta", false) ev.device = -1 return ev "mouse_button": if not params.has("button"): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "event_type='mouse_button' requires button (1=left, 2=right, 3=middle, 4=wheel up, 5=wheel down).") var button: int = int(params.get("button", 0)) if button <= 0: return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "mouse_button button must be > 0 (got %d). Use 1=left, 2=right, 3=middle, 4=wheel up, 5=wheel down." % button) var ev := InputEventMouseButton.new() ev.button_index = button ev.device = -1 return ev "joy_button": if not params.has("button"): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "event_type='joy_button' requires button (JoyButton index, e.g. 0=A/Cross, 1=B/Circle).") var ev := InputEventJoypadButton.new() ev.button_index = int(params.get("button", 0)) return ev "joy_axis": var axis_param = params.get("axis", null) if axis_param == null: return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "event_type='joy_axis' requires axis (JoyAxis index, e.g. 0=left stick X, 1=left stick Y).") var axis: int match typeof(axis_param): TYPE_INT: axis = axis_param TYPE_FLOAT: if axis_param != floor(axis_param): return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "joy_axis axis must be an integer JoyAxis index (got %s)." % str(axis_param)) axis = int(axis_param) TYPE_STRING: var axis_text := str(axis_param) if not axis_text.is_valid_int(): return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "joy_axis axis must be an integer JoyAxis index (got '%s')." % axis_text) axis = int(axis_text) _: return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "joy_axis axis must be an integer JoyAxis index (got %s)." % type_string(typeof(axis_param))) var ev := InputEventJoypadMotion.new() ev.axis = axis ev.axis_value = float(params.get("axis_value", 1.0)) return ev return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Unsupported event_type: '%s'. Use 'key', 'mouse_button', 'joy_button', or 'joy_axis'." % event_type) func _serialize_event(event: InputEvent) -> Dictionary: if event is InputEventKey: return { "type": "key", "keycode": OS.get_keycode_string(event.keycode), "physical_keycode": OS.get_keycode_string(event.physical_keycode), "ctrl": event.ctrl_pressed, "alt": event.alt_pressed, "shift": event.shift_pressed, "meta": event.meta_pressed, } if event is InputEventMouseButton: return { "type": "mouse_button", "button": event.button_index, } if event is InputEventJoypadButton: return { "type": "joy_button", "button": event.button_index, } if event is InputEventJoypadMotion: return { "type": "joy_axis", "axis": event.axis, "axis_value": event.axis_value, } return {"type": event.get_class(), "string": str(event)} func _save_action_events(action: String) -> int: var events: Array = [] for event in InputMap.action_get_events(action): events.append(event) var key := "input/%s" % action var had_setting := ProjectSettings.has_setting(key) var old_setting = ProjectSettings.get_setting(key) if had_setting else null var deadzone: float = 0.5 if old_setting is Dictionary: deadzone = old_setting.get("deadzone", 0.5) ProjectSettings.set_setting(key, { "deadzone": deadzone, "events": events, }) var err := ProjectSettings.save() if err != OK: if had_setting: ProjectSettings.set_setting(key, old_setting) else: ProjectSettings.clear(key) return err