Files
tekton/addons/godot_ai/handlers/input_handler.gd
T

281 lines
9.7 KiB
GDScript

@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