Skip to content

Commit 9963e22

Browse files
authored
Fix #439: validate input_map deadzone + diagnostic bind_event errors (#440)
1 parent 8f2fce5 commit 9963e22

3 files changed

Lines changed: 123 additions & 17 deletions

File tree

plugin/addons/godot_ai/handlers/input_handler.gd

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ func add_action(params: Dictionary) -> Dictionary:
5454
if action.is_empty():
5555
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: action")
5656

57+
if deadzone < 0.0 or deadzone > 1.0:
58+
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
59+
"deadzone must be in [0.0, 1.0] (got %s). Typical values are 0.2-0.5; default is 0.5." % deadzone)
60+
5761
if InputMap.has_action(action):
5862
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "Action '%s' already exists" % action)
5963

@@ -127,11 +131,13 @@ func bind_event(params: Dictionary) -> Dictionary:
127131
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: event_type")
128132

129133
if not InputMap.has_action(action):
130-
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Action '%s' not found" % action)
134+
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
135+
"Action '%s' not found. Call input_map_manage(op='add_action', params={action: '%s'}) first." % [action, action])
131136

132-
var event: InputEvent = _create_event(event_type, params)
133-
if event == null:
134-
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Unsupported event_type: %s (use key, mouse_button, or joy_button)" % event_type)
137+
var event_or_error = _create_event(event_type, params)
138+
if event_or_error is Dictionary:
139+
return event_or_error
140+
var event: InputEvent = event_or_error
135141

136142
InputMap.action_add_event(action, event)
137143

@@ -151,35 +157,45 @@ func bind_event(params: Dictionary) -> Dictionary:
151157
}
152158

153159

154-
func _create_event(event_type: String, params: Dictionary) -> InputEvent:
160+
## Returns an InputEvent on success, or a Dictionary error on failure.
161+
## Caller must check ``result is Dictionary`` before treating it as an event.
162+
func _create_event(event_type: String, params: Dictionary):
155163
match event_type:
156164
"key":
157165
var ev := InputEventKey.new()
158166
var keycode_str: String = params.get("keycode", "")
159167
if keycode_str.is_empty():
160-
return null
168+
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM,
169+
"event_type='key' requires keycode (e.g. 'Space', 'A', 'Enter', 'Escape', 'F1').")
161170
ev.keycode = OS.find_keycode_from_string(keycode_str)
162171
if ev.keycode == KEY_NONE:
163-
return null
172+
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
173+
"Invalid keycode '%s'. Use Godot keycode names like 'A', 'Space', 'Enter', 'Escape', 'F1', 'Left', 'Right'." % keycode_str)
164174
ev.ctrl_pressed = params.get("ctrl", false)
165175
ev.alt_pressed = params.get("alt", false)
166176
ev.shift_pressed = params.get("shift", false)
167177
ev.meta_pressed = params.get("meta", false)
168178
return ev
169179
"mouse_button":
170-
var ev := InputEventMouseButton.new()
171-
var button: int = params.get("button", 0)
180+
if not params.has("button"):
181+
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM,
182+
"event_type='mouse_button' requires button (1=left, 2=right, 3=middle, 4=wheel up, 5=wheel down).")
183+
var button: int = int(params.get("button", 0))
172184
if button <= 0:
173-
return null
185+
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
186+
"mouse_button button must be > 0 (got %d). Use 1=left, 2=right, 3=middle, 4=wheel up, 5=wheel down." % button)
187+
var ev := InputEventMouseButton.new()
174188
ev.button_index = button
175189
return ev
176190
"joy_button":
177-
var ev := InputEventJoypadButton.new()
178191
if not params.has("button"):
179-
return null
192+
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM,
193+
"event_type='joy_button' requires button (JoyButton index, e.g. 0=A/Cross, 1=B/Circle).")
194+
var ev := InputEventJoypadButton.new()
180195
ev.button_index = int(params.get("button", 0))
181196
return ev
182-
return null
197+
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE,
198+
"Unsupported event_type: '%s'. Use 'key', 'mouse_button', or 'joy_button'." % event_type)
183199

184200

185201
func _serialize_event(event: InputEvent) -> Dictionary:

src/godot_ai/tools/input_map.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,26 @@
2222
(``spatial_editor/*``, etc.). The ``is_builtin`` field on each
2323
entry is true for any action not authored by the user.
2424
• add_action(action, deadzone=0.5)
25-
Create a new empty input action.
25+
Create a new empty input action. ``deadzone`` must be in
26+
``[0.0, 1.0]`` — Godot uses it as the analog-stick dead-zone
27+
threshold; values outside this range are rejected with
28+
``VALUE_OUT_OF_RANGE``. Typical values are 0.2-0.5; leave the
29+
default 0.5 unless you have a reason. Not a key-repeat delay.
2630
• remove_action(action)
2731
Remove an action and all its event bindings.
2832
• bind_event(action, event_type, keycode="", ctrl=False, alt=False,
2933
shift=False, meta=False, button=None)
30-
Bind a key/mouse/gamepad event to an action. event_type is
31-
"key" | "mouse_button" | "joy_button". keycode is required for
32-
"key"; button is required for the others.
34+
Bind a key/mouse/gamepad event to an action. The action must
35+
already exist (call ``add_action`` first). ``event_type`` is
36+
``"key"`` | ``"mouse_button"`` | ``"joy_button"``.
37+
- ``key``: ``keycode`` is a Godot keycode *name string* like
38+
``"A"``, ``"Space"``, ``"Enter"``, ``"Escape"``, ``"F1"``,
39+
``"Left"`` — not an integer and not ``KEY_*``. Modifier
40+
booleans ``ctrl`` / ``alt`` / ``shift`` / ``meta`` optional.
41+
- ``mouse_button``: ``button`` is an int — 1=left, 2=right,
42+
3=middle, 4=wheel up, 5=wheel down.
43+
- ``joy_button``: ``button`` is the ``JoyButton`` index
44+
(e.g. 0=A/Cross, 1=B/Circle).
3345
"""
3446

3547

test_project/tests/test_input.gd

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,29 @@ func test_add_action_duplicate() -> void:
101101
_handler.remove_action({"action": TEST_ACTION})
102102

103103

104+
func test_add_action_rejects_deadzone_below_zero() -> void:
105+
## Issue #439: callers (LLMs) pass deadzone values outside [0, 1].
106+
## Reject explicitly with VALUE_OUT_OF_RANGE so retries can converge.
107+
var result := _handler.add_action({"action": TEST_ACTION, "deadzone": -0.1})
108+
assert_is_error(result, ErrorCodes.VALUE_OUT_OF_RANGE)
109+
assert_false(InputMap.has_action(TEST_ACTION), "Action must not be created on validation failure")
110+
111+
112+
func test_add_action_rejects_deadzone_above_one() -> void:
113+
var result := _handler.add_action({"action": TEST_ACTION, "deadzone": 1.5})
114+
assert_is_error(result, ErrorCodes.VALUE_OUT_OF_RANGE)
115+
assert_false(InputMap.has_action(TEST_ACTION), "Action must not be created on validation failure")
116+
117+
118+
func test_add_action_accepts_boundary_deadzones() -> void:
119+
var result := _handler.add_action({"action": TEST_ACTION, "deadzone": 0.0})
120+
assert_has_key(result, "data")
121+
_handler.remove_action({"action": TEST_ACTION})
122+
result = _handler.add_action({"action": TEST_ACTION, "deadzone": 1.0})
123+
assert_has_key(result, "data")
124+
_handler.remove_action({"action": TEST_ACTION})
125+
126+
104127
# ----- remove_action -----
105128

106129
func test_remove_action_missing_name() -> void:
@@ -142,6 +165,61 @@ func test_bind_event_unsupported_type() -> void:
142165
_handler.remove_action({"action": TEST_ACTION})
143166

144167

168+
func test_bind_event_key_missing_keycode() -> void:
169+
## Issue #439: was collapsed into "Unsupported event_type" — now reports
170+
## the actual missing param so retries can converge.
171+
_handler.add_action({"action": TEST_ACTION})
172+
var result := _handler.bind_event({
173+
"action": TEST_ACTION,
174+
"event_type": "key",
175+
})
176+
assert_is_error(result, ErrorCodes.MISSING_REQUIRED_PARAM)
177+
_handler.remove_action({"action": TEST_ACTION})
178+
179+
180+
func test_bind_event_key_invalid_keycode() -> void:
181+
_handler.add_action({"action": TEST_ACTION})
182+
var result := _handler.bind_event({
183+
"action": TEST_ACTION,
184+
"event_type": "key",
185+
"keycode": "NotARealKey",
186+
})
187+
assert_is_error(result, ErrorCodes.VALUE_OUT_OF_RANGE)
188+
_handler.remove_action({"action": TEST_ACTION})
189+
190+
191+
func test_bind_event_mouse_button_missing_button() -> void:
192+
_handler.add_action({"action": TEST_ACTION})
193+
var result := _handler.bind_event({
194+
"action": TEST_ACTION,
195+
"event_type": "mouse_button",
196+
})
197+
assert_is_error(result, ErrorCodes.MISSING_REQUIRED_PARAM)
198+
_handler.remove_action({"action": TEST_ACTION})
199+
200+
201+
func test_bind_event_mouse_button_zero_button() -> void:
202+
_handler.add_action({"action": TEST_ACTION})
203+
var result := _handler.bind_event({
204+
"action": TEST_ACTION,
205+
"event_type": "mouse_button",
206+
"button": 0,
207+
})
208+
assert_is_error(result, ErrorCodes.VALUE_OUT_OF_RANGE)
209+
_handler.remove_action({"action": TEST_ACTION})
210+
211+
212+
func test_bind_event_unknown_action_message_suggests_add_action() -> void:
213+
## The error string should point the caller at the fix so they don't loop.
214+
var result := _handler.bind_event({
215+
"action": "_nope_xyz",
216+
"event_type": "key",
217+
"keycode": "Space",
218+
})
219+
assert_is_error(result, ErrorCodes.VALUE_OUT_OF_RANGE)
220+
assert_contains(result.error.message, "add_action")
221+
222+
145223
func test_bind_key_event() -> void:
146224
_handler.add_action({"action": TEST_ACTION})
147225
var result := _handler.bind_event({

0 commit comments

Comments
 (0)