@satelliteoflove/godot-mcp 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +3 -4
  2. package/addon/command_router.gd +39 -0
  3. package/addon/command_router.gd.uid +1 -0
  4. package/addon/commands/animation_commands.gd +633 -0
  5. package/addon/commands/animation_commands.gd.uid +1 -0
  6. package/addon/commands/debug_commands.gd +109 -0
  7. package/addon/commands/debug_commands.gd.uid +1 -0
  8. package/addon/commands/file_commands.gd +95 -0
  9. package/addon/commands/file_commands.gd.uid +1 -0
  10. package/addon/commands/node_commands.gd +252 -0
  11. package/addon/commands/node_commands.gd.uid +1 -0
  12. package/addon/commands/project_commands.gd +114 -0
  13. package/addon/commands/project_commands.gd.uid +1 -0
  14. package/addon/commands/resource_commands.gd +293 -0
  15. package/addon/commands/resource_commands.gd.uid +1 -0
  16. package/addon/commands/scene3d_commands.gd +169 -0
  17. package/addon/commands/scene3d_commands.gd.uid +1 -0
  18. package/addon/commands/scene_commands.gd +131 -0
  19. package/addon/commands/scene_commands.gd.uid +1 -0
  20. package/addon/commands/screenshot_commands.gd +130 -0
  21. package/addon/commands/screenshot_commands.gd.uid +1 -0
  22. package/addon/commands/script_commands.gd +156 -0
  23. package/addon/commands/script_commands.gd.uid +1 -0
  24. package/addon/commands/selection_commands.gd +170 -0
  25. package/addon/commands/selection_commands.gd.uid +1 -0
  26. package/addon/commands/system_commands.gd +29 -0
  27. package/addon/commands/tilemap_commands.gd +657 -0
  28. package/addon/commands/tilemap_commands.gd.uid +1 -0
  29. package/addon/core/base_command.gd +29 -0
  30. package/addon/core/base_command.gd.uid +1 -0
  31. package/addon/core/mcp_debugger_plugin.gd +144 -0
  32. package/addon/core/mcp_debugger_plugin.gd.uid +1 -0
  33. package/addon/core/mcp_logger.gd +40 -0
  34. package/addon/core/mcp_logger.gd.uid +1 -0
  35. package/addon/core/mcp_utils.gd +125 -0
  36. package/addon/core/mcp_utils.gd.uid +1 -0
  37. package/addon/game_bridge/mcp_game_bridge.gd +195 -0
  38. package/addon/game_bridge/mcp_game_bridge.gd.uid +1 -0
  39. package/addon/plugin.cfg +8 -0
  40. package/addon/plugin.gd +89 -0
  41. package/addon/plugin.gd.uid +1 -0
  42. package/addon/ui/status_panel.gd +23 -0
  43. package/addon/ui/status_panel.gd.uid +1 -0
  44. package/addon/ui/status_panel.tscn +41 -0
  45. package/addon/websocket_server.gd +143 -0
  46. package/addon/websocket_server.gd.uid +1 -0
  47. package/dist/cli.d.ts +3 -0
  48. package/dist/cli.d.ts.map +1 -0
  49. package/dist/cli.js +60 -0
  50. package/dist/cli.js.map +1 -0
  51. package/dist/connection/websocket.d.ts +14 -0
  52. package/dist/connection/websocket.d.ts.map +1 -1
  53. package/dist/connection/websocket.js +69 -1
  54. package/dist/connection/websocket.js.map +1 -1
  55. package/dist/index.d.ts +1 -2
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1 -6
  58. package/dist/index.js.map +1 -1
  59. package/dist/installer/install.d.ts +13 -0
  60. package/dist/installer/install.d.ts.map +1 -0
  61. package/dist/installer/install.js +77 -0
  62. package/dist/installer/install.js.map +1 -0
  63. package/dist/tools/project.d.ts +3 -3
  64. package/dist/tools/project.d.ts.map +1 -1
  65. package/dist/tools/project.js +28 -2
  66. package/dist/tools/project.js.map +1 -1
  67. package/dist/version.d.ts +2 -0
  68. package/dist/version.d.ts.map +1 -0
  69. package/dist/version.js +20 -0
  70. package/dist/version.js.map +1 -0
  71. package/package.json +6 -4
@@ -0,0 +1,109 @@
1
+ @tool
2
+ extends MCPBaseCommand
3
+ class_name MCPDebugCommands
4
+
5
+ const DEBUG_OUTPUT_TIMEOUT := 5.0
6
+ const PERFORMANCE_METRICS_TIMEOUT := 5.0
7
+
8
+ var _debug_output_result: PackedStringArray = []
9
+ var _debug_output_pending: bool = false
10
+
11
+ var _performance_metrics_result: Dictionary = {}
12
+ var _performance_metrics_pending: bool = false
13
+
14
+
15
+ func get_commands() -> Dictionary:
16
+ return {
17
+ "run_project": run_project,
18
+ "stop_project": stop_project,
19
+ "get_debug_output": get_debug_output,
20
+ "get_performance_metrics": get_performance_metrics
21
+ }
22
+
23
+
24
+ func run_project(params: Dictionary) -> Dictionary:
25
+ var scene_path: String = params.get("scene_path", "")
26
+
27
+ MCPLogger.clear()
28
+
29
+ if scene_path.is_empty():
30
+ EditorInterface.play_main_scene()
31
+ else:
32
+ EditorInterface.play_custom_scene(scene_path)
33
+
34
+ return _success({})
35
+
36
+
37
+ func stop_project(_params: Dictionary) -> Dictionary:
38
+ EditorInterface.stop_playing_scene()
39
+ return _success({})
40
+
41
+
42
+ func get_debug_output(params: Dictionary) -> Dictionary:
43
+ var clear: bool = params.get("clear", false)
44
+
45
+ if not EditorInterface.is_playing_scene():
46
+ var output := "\n".join(MCPLogger.get_output())
47
+ if clear:
48
+ MCPLogger.clear()
49
+ return _success({"output": output})
50
+
51
+ var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
52
+ if debugger_plugin == null or not debugger_plugin.has_active_session():
53
+ var output := "\n".join(MCPLogger.get_output())
54
+ if clear:
55
+ MCPLogger.clear()
56
+ return _success({"output": output})
57
+
58
+ _debug_output_pending = true
59
+ _debug_output_result = PackedStringArray()
60
+
61
+ debugger_plugin.debug_output_received.connect(_on_debug_output_received, CONNECT_ONE_SHOT)
62
+ debugger_plugin.request_debug_output(clear)
63
+
64
+ var start_time := Time.get_ticks_msec()
65
+ while _debug_output_pending:
66
+ await Engine.get_main_loop().process_frame
67
+ if (Time.get_ticks_msec() - start_time) / 1000.0 > DEBUG_OUTPUT_TIMEOUT:
68
+ _debug_output_pending = false
69
+ if debugger_plugin.debug_output_received.is_connected(_on_debug_output_received):
70
+ debugger_plugin.debug_output_received.disconnect(_on_debug_output_received)
71
+ return _success({"output": "\n".join(MCPLogger.get_output())})
72
+
73
+ return _success({"output": "\n".join(_debug_output_result)})
74
+
75
+
76
+ func _on_debug_output_received(output: PackedStringArray) -> void:
77
+ _debug_output_pending = false
78
+ _debug_output_result = output
79
+
80
+
81
+ func get_performance_metrics(_params: Dictionary) -> Dictionary:
82
+ if not EditorInterface.is_playing_scene():
83
+ return _error("NOT_RUNNING", "No game is currently running")
84
+
85
+ var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
86
+ if debugger_plugin == null or not debugger_plugin.has_active_session():
87
+ return _error("NO_SESSION", "No active debug session")
88
+
89
+ _performance_metrics_pending = true
90
+ _performance_metrics_result = {}
91
+
92
+ debugger_plugin.performance_metrics_received.connect(_on_performance_metrics_received, CONNECT_ONE_SHOT)
93
+ debugger_plugin.request_performance_metrics()
94
+
95
+ var start_time := Time.get_ticks_msec()
96
+ while _performance_metrics_pending:
97
+ await Engine.get_main_loop().process_frame
98
+ if (Time.get_ticks_msec() - start_time) / 1000.0 > PERFORMANCE_METRICS_TIMEOUT:
99
+ _performance_metrics_pending = false
100
+ if debugger_plugin.performance_metrics_received.is_connected(_on_performance_metrics_received):
101
+ debugger_plugin.performance_metrics_received.disconnect(_on_performance_metrics_received)
102
+ return _error("TIMEOUT", "Timed out waiting for performance metrics")
103
+
104
+ return _success(_performance_metrics_result)
105
+
106
+
107
+ func _on_performance_metrics_received(metrics: Dictionary) -> void:
108
+ _performance_metrics_pending = false
109
+ _performance_metrics_result = metrics
@@ -0,0 +1 @@
1
+ uid://b8870g3hyn1fd
@@ -0,0 +1,95 @@
1
+ @tool
2
+ extends MCPBaseCommand
3
+ class_name MCPFileCommands
4
+
5
+
6
+ func get_commands() -> Dictionary:
7
+ return {
8
+ "list_project_files": list_project_files,
9
+ "search_files": search_files
10
+ }
11
+
12
+
13
+ func list_project_files(params: Dictionary) -> Dictionary:
14
+ var file_type: String = params.get("file_type", "all")
15
+ var directory: String = params.get("directory", "res://")
16
+ var recursive: bool = params.get("recursive", true)
17
+
18
+ var extensions: PackedStringArray
19
+ match file_type:
20
+ "scripts":
21
+ extensions = PackedStringArray(["gd", "cs"])
22
+ "scenes":
23
+ extensions = PackedStringArray(["tscn", "scn"])
24
+ "resources":
25
+ extensions = PackedStringArray(["tres", "res"])
26
+ "images":
27
+ extensions = PackedStringArray(["png", "jpg", "jpeg", "webp", "svg"])
28
+ "audio":
29
+ extensions = PackedStringArray(["ogg", "mp3", "wav"])
30
+ "all":
31
+ extensions = PackedStringArray()
32
+ _:
33
+ return _error("INVALID_TYPE", "Unknown file type: %s" % file_type)
34
+
35
+ var files := _scan_directory(directory, extensions, recursive)
36
+ return _success({"files": files})
37
+
38
+
39
+ func search_files(params: Dictionary) -> Dictionary:
40
+ var pattern: String = params.get("pattern", "")
41
+ var directory: String = params.get("directory", "res://")
42
+
43
+ if pattern.is_empty():
44
+ return _error("INVALID_PARAMS", "pattern is required")
45
+
46
+ var all_files := _scan_directory(directory, PackedStringArray(), true)
47
+ var matching: Array[String] = []
48
+
49
+ for file_path in all_files:
50
+ var file_name := file_path.get_file()
51
+ if _matches_pattern(file_name, pattern):
52
+ matching.append(file_path)
53
+
54
+ return _success({"files": matching})
55
+
56
+
57
+ func _scan_directory(path: String, extensions: PackedStringArray, recursive: bool) -> Array[String]:
58
+ var files: Array[String] = []
59
+ var dir := DirAccess.open(path)
60
+
61
+ if not dir:
62
+ return files
63
+
64
+ dir.list_dir_begin()
65
+ var file_name := dir.get_next()
66
+
67
+ while not file_name.is_empty():
68
+ var full_path := path.path_join(file_name)
69
+
70
+ if dir.current_is_dir():
71
+ if recursive and not file_name.begins_with("."):
72
+ files.append_array(_scan_directory(full_path, extensions, recursive))
73
+ else:
74
+ if extensions.is_empty():
75
+ files.append(full_path)
76
+ else:
77
+ var ext := file_name.get_extension().to_lower()
78
+ if ext in extensions:
79
+ files.append(full_path)
80
+
81
+ file_name = dir.get_next()
82
+
83
+ dir.list_dir_end()
84
+ return files
85
+
86
+
87
+ func _matches_pattern(text: String, pattern: String) -> bool:
88
+ if not "*" in pattern:
89
+ return pattern.to_lower() in text.to_lower()
90
+
91
+ var regex_pattern := "^" + pattern.replace(".", "\\.").replace("*", ".*") + "$"
92
+ var regex := RegEx.new()
93
+ regex.compile(regex_pattern)
94
+
95
+ return regex.search(text.to_lower()) != null
@@ -0,0 +1 @@
1
+ uid://cfxkg4lhdnxgw
@@ -0,0 +1,252 @@
1
+ @tool
2
+ extends MCPBaseCommand
3
+ class_name MCPNodeCommands
4
+
5
+ var _find_nodes_pending := false
6
+ var _find_nodes_result: Dictionary = {}
7
+
8
+
9
+ func get_commands() -> Dictionary:
10
+ return {
11
+ "get_node_properties": get_node_properties,
12
+ "find_nodes": find_nodes,
13
+ "create_node": create_node,
14
+ "update_node": update_node,
15
+ "delete_node": delete_node,
16
+ "reparent_node": reparent_node
17
+ }
18
+
19
+
20
+ func _require_scene_open() -> Dictionary:
21
+ var root := EditorInterface.get_edited_scene_root()
22
+ if not root:
23
+ return _error("NO_SCENE", "No scene is currently open")
24
+ return {}
25
+
26
+
27
+ func get_node_properties(params: Dictionary) -> Dictionary:
28
+ var node_path: String = params.get("node_path", "")
29
+ if node_path.is_empty():
30
+ return _error("INVALID_PARAMS", "node_path is required")
31
+
32
+ var node := _get_node(node_path)
33
+ if not node:
34
+ return _error("NODE_NOT_FOUND", "Node not found: %s" % node_path)
35
+
36
+ var properties := {}
37
+ for prop in node.get_property_list():
38
+ var name: String = prop["name"]
39
+ if name.begins_with("_") or prop["usage"] & PROPERTY_USAGE_SCRIPT_VARIABLE == 0:
40
+ if prop["usage"] & PROPERTY_USAGE_EDITOR == 0:
41
+ continue
42
+
43
+ var value = node.get(name)
44
+ properties[name] = _serialize_value(value)
45
+
46
+ return _success({"properties": properties})
47
+
48
+
49
+ func find_nodes(params: Dictionary) -> Dictionary:
50
+ var name_pattern: String = params.get("name_pattern", "")
51
+ var type_filter: String = params.get("type", "")
52
+ var root_path: String = params.get("root_path", "")
53
+
54
+ if name_pattern.is_empty() and type_filter.is_empty():
55
+ return _error("INVALID_PARAMS", "At least one of name_pattern or type is required")
56
+
57
+ var debugger := _plugin.get_debugger_plugin() as MCPDebuggerPlugin
58
+ if debugger and debugger.has_active_session():
59
+ return await _find_nodes_via_game(debugger, name_pattern, type_filter, root_path)
60
+
61
+ var scene_check := _require_scene_open()
62
+ if not scene_check.is_empty():
63
+ return scene_check
64
+
65
+ var scene_root := EditorInterface.get_edited_scene_root()
66
+ var search_root: Node = scene_root
67
+
68
+ if not root_path.is_empty():
69
+ search_root = _get_node(root_path)
70
+ if not search_root:
71
+ return _error("NODE_NOT_FOUND", "Root node not found: %s" % root_path)
72
+
73
+ var matches: Array[Dictionary] = []
74
+ _find_recursive(search_root, scene_root, name_pattern, type_filter, matches)
75
+
76
+ return _success({"matches": matches, "count": matches.size()})
77
+
78
+
79
+ func _find_nodes_via_game(debugger: MCPDebuggerPlugin, name_pattern: String, type_filter: String, root_path: String) -> Dictionary:
80
+ _find_nodes_pending = true
81
+ _find_nodes_result = {}
82
+
83
+ debugger.find_nodes_received.connect(_on_find_nodes_received, CONNECT_ONE_SHOT)
84
+ debugger.request_find_nodes(name_pattern, type_filter, root_path)
85
+
86
+ while _find_nodes_pending:
87
+ await Engine.get_main_loop().process_frame
88
+
89
+ return _find_nodes_result
90
+
91
+
92
+ func _on_find_nodes_received(matches: Array, count: int, error: String) -> void:
93
+ _find_nodes_pending = false
94
+ if not error.is_empty():
95
+ _find_nodes_result = _error("GAME_ERROR", error)
96
+ else:
97
+ _find_nodes_result = _success({"matches": matches, "count": count})
98
+
99
+
100
+ func _find_recursive(node: Node, scene_root: Node, name_pattern: String, type_filter: String, results: Array[Dictionary]) -> void:
101
+ var name_matches := name_pattern.is_empty() or node.name.matchn(name_pattern)
102
+ var type_matches := type_filter.is_empty() or node.is_class(type_filter)
103
+
104
+ if name_matches and type_matches:
105
+ var relative_path := scene_root.get_path_to(node)
106
+ var usable_path := "/root/" + scene_root.name
107
+ if relative_path != NodePath("."):
108
+ usable_path += "/" + str(relative_path)
109
+
110
+ results.append({
111
+ "path": usable_path,
112
+ "type": node.get_class()
113
+ })
114
+
115
+ for child in node.get_children():
116
+ _find_recursive(child, scene_root, name_pattern, type_filter, results)
117
+
118
+
119
+ func create_node(params: Dictionary) -> Dictionary:
120
+ var scene_check := _require_scene_open()
121
+ if not scene_check.is_empty():
122
+ return scene_check
123
+
124
+ var parent_path: String = params.get("parent_path", "")
125
+ var node_type: String = params.get("node_type", "")
126
+ var scene_path: String = params.get("scene_path", "")
127
+ var node_name: String = params.get("node_name", "")
128
+ var properties: Dictionary = params.get("properties", {})
129
+
130
+ if parent_path.is_empty():
131
+ return _error("INVALID_PARAMS", "parent_path is required")
132
+ if node_name.is_empty():
133
+ return _error("INVALID_PARAMS", "node_name is required")
134
+ if node_type.is_empty() and scene_path.is_empty():
135
+ return _error("INVALID_PARAMS", "Either node_type or scene_path is required")
136
+ if not node_type.is_empty() and not scene_path.is_empty():
137
+ return _error("INVALID_PARAMS", "Provide node_type OR scene_path, not both")
138
+
139
+ var parent := _get_node(parent_path)
140
+ if not parent:
141
+ return _error("NODE_NOT_FOUND", "Parent node not found: %s" % parent_path)
142
+
143
+ var node: Node
144
+ if not scene_path.is_empty():
145
+ if not ResourceLoader.exists(scene_path):
146
+ return _error("SCENE_NOT_FOUND", "Scene not found: %s" % scene_path)
147
+ var packed_scene: PackedScene = load(scene_path)
148
+ if not packed_scene:
149
+ return _error("LOAD_FAILED", "Failed to load scene: %s" % scene_path)
150
+ node = packed_scene.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)
151
+ if not node:
152
+ return _error("INSTANTIATE_FAILED", "Failed to instantiate: %s" % scene_path)
153
+ else:
154
+ if not ClassDB.class_exists(node_type):
155
+ return _error("INVALID_TYPE", "Unknown node type: %s" % node_type)
156
+ node = ClassDB.instantiate(node_type)
157
+ if not node:
158
+ return _error("CREATE_FAILED", "Failed to create node of type: %s" % node_type)
159
+
160
+ node.name = node_name
161
+
162
+ for key in properties:
163
+ if node.has_method("set") and key in node:
164
+ var deserialized := MCPUtils.deserialize_value(properties[key])
165
+ node.set(key, deserialized)
166
+
167
+ parent.add_child(node)
168
+ _set_owner_recursive(node, EditorInterface.get_edited_scene_root())
169
+
170
+ return _success({"node_path": str(node.get_path())})
171
+
172
+
173
+ func _set_owner_recursive(node: Node, owner: Node) -> void:
174
+ node.owner = owner
175
+ for child in node.get_children():
176
+ _set_owner_recursive(child, owner)
177
+
178
+
179
+ func update_node(params: Dictionary) -> Dictionary:
180
+ var node_path: String = params.get("node_path", "")
181
+ var properties: Dictionary = params.get("properties", {})
182
+
183
+ if node_path.is_empty():
184
+ return _error("INVALID_PARAMS", "node_path is required")
185
+ if properties.is_empty():
186
+ return _error("INVALID_PARAMS", "properties is required")
187
+
188
+ var node := _get_node(node_path)
189
+ if not node:
190
+ return _error("NODE_NOT_FOUND", "Node not found: %s" % node_path)
191
+
192
+ for key in properties:
193
+ if key in node:
194
+ var deserialized := MCPUtils.deserialize_value(properties[key])
195
+ node.set(key, deserialized)
196
+
197
+ return _success({})
198
+
199
+
200
+ func delete_node(params: Dictionary) -> Dictionary:
201
+ var scene_check := _require_scene_open()
202
+ if not scene_check.is_empty():
203
+ return scene_check
204
+
205
+ var node_path: String = params.get("node_path", "")
206
+ if node_path.is_empty():
207
+ return _error("INVALID_PARAMS", "node_path is required")
208
+
209
+ var node := _get_node(node_path)
210
+ if not node:
211
+ return _error("NODE_NOT_FOUND", "Node not found: %s" % node_path)
212
+
213
+ var root := EditorInterface.get_edited_scene_root()
214
+ if node == root:
215
+ return _error("CANNOT_DELETE_ROOT", "Cannot delete the root node")
216
+
217
+ node.get_parent().remove_child(node)
218
+ node.queue_free()
219
+
220
+ return _success({})
221
+
222
+
223
+ func reparent_node(params: Dictionary) -> Dictionary:
224
+ var scene_check := _require_scene_open()
225
+ if not scene_check.is_empty():
226
+ return scene_check
227
+
228
+ var node_path: String = params.get("node_path", "")
229
+ var new_parent_path: String = params.get("new_parent_path", "")
230
+
231
+ if node_path.is_empty():
232
+ return _error("INVALID_PARAMS", "node_path is required")
233
+ if new_parent_path.is_empty():
234
+ return _error("INVALID_PARAMS", "new_parent_path is required")
235
+
236
+ var node := _get_node(node_path)
237
+ if not node:
238
+ return _error("NODE_NOT_FOUND", "Node not found: %s" % node_path)
239
+
240
+ var new_parent := _get_node(new_parent_path)
241
+ if not new_parent:
242
+ return _error("NODE_NOT_FOUND", "New parent not found: %s" % new_parent_path)
243
+
244
+ var root := EditorInterface.get_edited_scene_root()
245
+ if node == root:
246
+ return _error("CANNOT_REPARENT_ROOT", "Cannot reparent the root node")
247
+
248
+ node.reparent(new_parent)
249
+
250
+ return _success({"new_path": str(node.get_path())})
251
+
252
+
@@ -0,0 +1 @@
1
+ uid://68p58x7fq2ve
@@ -0,0 +1,114 @@
1
+ @tool
2
+ extends MCPBaseCommand
3
+ class_name MCPProjectCommands
4
+
5
+
6
+ func get_commands() -> Dictionary:
7
+ return {
8
+ "get_project_info": get_project_info,
9
+ "get_project_settings": get_project_settings
10
+ }
11
+
12
+
13
+ func get_project_info(_params: Dictionary) -> Dictionary:
14
+ return _success({
15
+ "name": ProjectSettings.get_setting("application/config/name", "Unknown"),
16
+ "path": ProjectSettings.globalize_path("res://"),
17
+ "godot_version": Engine.get_version_info()["string"],
18
+ "main_scene": ProjectSettings.get_setting("application/run/main_scene", null)
19
+ })
20
+
21
+
22
+ func get_project_settings(params: Dictionary) -> Dictionary:
23
+ var category: String = params.get("category", "")
24
+
25
+ if category == "input":
26
+ return _get_input_mappings(params)
27
+
28
+ var settings := {}
29
+ var all_settings := ProjectSettings.get_property_list()
30
+
31
+ for prop in all_settings:
32
+ var name: String = prop["name"]
33
+ if not category.is_empty() and not name.begins_with(category):
34
+ continue
35
+ if prop["usage"] & PROPERTY_USAGE_EDITOR:
36
+ settings[name] = _serialize_value(ProjectSettings.get_setting(name))
37
+
38
+ return _success({"settings": settings})
39
+
40
+
41
+ func _get_input_mappings(params: Dictionary) -> Dictionary:
42
+ var include_builtin: bool = params.get("include_builtin", false)
43
+ var actions := {}
44
+
45
+ # Read from ProjectSettings instead of InputMap
46
+ # InputMap in editor context only has editor actions, not game inputs
47
+ # Game inputs are stored as "input/<action_name>" in ProjectSettings
48
+ var all_settings := ProjectSettings.get_property_list()
49
+
50
+ for prop in all_settings:
51
+ var name: String = prop["name"]
52
+ if not name.begins_with("input/"):
53
+ continue
54
+
55
+ var action_name := name.substr(6) # Remove "input/" prefix
56
+
57
+ if not include_builtin and action_name.begins_with("ui_"):
58
+ continue
59
+
60
+ var action_data = ProjectSettings.get_setting(name)
61
+ if action_data is Dictionary:
62
+ var events := []
63
+ var raw_events = action_data.get("events", [])
64
+ for event in raw_events:
65
+ if event is InputEvent:
66
+ events.append(_serialize_input_event(event))
67
+
68
+ actions[action_name] = {
69
+ "deadzone": action_data.get("deadzone", 0.5),
70
+ "events": events
71
+ }
72
+
73
+ return _success({"settings": actions})
74
+
75
+
76
+ func _serialize_input_event(event: InputEvent) -> Dictionary:
77
+ if event is InputEventKey:
78
+ var keycode: int = event.keycode if event.keycode else event.physical_keycode
79
+ return {
80
+ "type": "key",
81
+ "keycode": event.keycode,
82
+ "physical_keycode": event.physical_keycode,
83
+ "key_label": OS.get_keycode_string(keycode),
84
+ "modifiers": _get_modifiers(event)
85
+ }
86
+ elif event is InputEventMouseButton:
87
+ return {
88
+ "type": "mouse_button",
89
+ "button_index": event.button_index,
90
+ "modifiers": _get_modifiers(event)
91
+ }
92
+ elif event is InputEventJoypadButton:
93
+ return {
94
+ "type": "joypad_button",
95
+ "button_index": event.button_index,
96
+ "device": event.device
97
+ }
98
+ elif event is InputEventJoypadMotion:
99
+ return {
100
+ "type": "joypad_motion",
101
+ "axis": event.axis,
102
+ "axis_value": event.axis_value,
103
+ "device": event.device
104
+ }
105
+ return {"type": "unknown", "event": str(event)}
106
+
107
+
108
+ func _get_modifiers(event: InputEventWithModifiers) -> Dictionary:
109
+ return {
110
+ "shift": event.shift_pressed,
111
+ "ctrl": event.ctrl_pressed,
112
+ "alt": event.alt_pressed,
113
+ "meta": event.meta_pressed
114
+ }
@@ -0,0 +1 @@
1
+ uid://cyfa3l85qu83y