@satelliteoflove/godot-mcp 2.3.0 → 2.4.1

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 (77) hide show
  1. package/README.md +58 -27
  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 +255 -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 +162 -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 +58 -0
  30. package/addon/core/base_command.gd.uid +1 -0
  31. package/addon/core/mcp_debugger_plugin.gd +149 -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 +129 -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/core/types.d.ts +5 -0
  56. package/dist/core/types.d.ts.map +1 -1
  57. package/dist/index.d.ts +1 -2
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +1 -6
  60. package/dist/index.js.map +1 -1
  61. package/dist/installer/install.d.ts +13 -0
  62. package/dist/installer/install.d.ts.map +1 -0
  63. package/dist/installer/install.js +77 -0
  64. package/dist/installer/install.js.map +1 -0
  65. package/dist/tools/editor.d.ts.map +1 -1
  66. package/dist/tools/editor.js.map +1 -1
  67. package/dist/tools/project.d.ts +3 -3
  68. package/dist/tools/project.d.ts.map +1 -1
  69. package/dist/tools/project.js +28 -2
  70. package/dist/tools/project.js.map +1 -1
  71. package/dist/tools/scene3d.d.ts.map +1 -1
  72. package/dist/tools/scene3d.js.map +1 -1
  73. package/dist/version.d.ts +2 -0
  74. package/dist/version.d.ts.map +1 -0
  75. package/dist/version.js +20 -0
  76. package/dist/version.js.map +1 -0
  77. package/package.json +6 -4
@@ -0,0 +1,293 @@
1
+ @tool
2
+ extends MCPBaseCommand
3
+ class_name MCPResourceCommands
4
+
5
+
6
+ const MAX_ARRAY_PREVIEW := 100
7
+ const BINARY_ARRAY_TYPES := [
8
+ TYPE_PACKED_BYTE_ARRAY,
9
+ TYPE_PACKED_INT32_ARRAY,
10
+ TYPE_PACKED_INT64_ARRAY,
11
+ TYPE_PACKED_FLOAT32_ARRAY,
12
+ TYPE_PACKED_FLOAT64_ARRAY,
13
+ ]
14
+
15
+
16
+ func get_commands() -> Dictionary:
17
+ return {
18
+ "get_resource_info": get_resource_info
19
+ }
20
+
21
+
22
+ func get_resource_info(params: Dictionary) -> Dictionary:
23
+ var resource_path: String = params.get("resource_path", "")
24
+ var max_depth: int = params.get("max_depth", 1)
25
+ var include_internal: bool = params.get("include_internal", false)
26
+
27
+ if resource_path.is_empty():
28
+ return _error("INVALID_PARAMS", "resource_path is required")
29
+
30
+ if not ResourceLoader.exists(resource_path):
31
+ return _error("RESOURCE_NOT_FOUND", "Resource not found: %s" % resource_path)
32
+
33
+ var resource := load(resource_path)
34
+ if not resource:
35
+ return _error("LOAD_FAILED", "Failed to load resource: %s" % resource_path)
36
+
37
+ var result := {
38
+ "resource_path": resource_path,
39
+ "resource_type": resource.get_class()
40
+ }
41
+
42
+ var type_specific := _get_type_specific_info(resource, max_depth)
43
+ if not type_specific.is_empty():
44
+ result["type_specific"] = type_specific
45
+ else:
46
+ var properties := _get_generic_properties(resource, max_depth, include_internal)
47
+ if not properties.is_empty():
48
+ result["properties"] = properties
49
+
50
+ return _success(result)
51
+
52
+
53
+ func _get_type_specific_info(resource: Resource, max_depth: int) -> Dictionary:
54
+ if resource is SpriteFrames:
55
+ return _format_sprite_frames(resource, max_depth)
56
+ elif resource is TileSet:
57
+ return _format_tileset(resource, max_depth)
58
+ elif resource is ShaderMaterial:
59
+ return _format_shader_material(resource, max_depth)
60
+ elif resource is StandardMaterial3D or resource is ORMMaterial3D:
61
+ return _format_standard_material(resource, max_depth)
62
+ elif resource is Texture2D:
63
+ return _format_texture2d(resource)
64
+ return {}
65
+
66
+
67
+ func _format_sprite_frames(sf: SpriteFrames, max_depth: int) -> Dictionary:
68
+ var animations := []
69
+
70
+ for anim_name in sf.get_animation_names():
71
+ var anim_info := {
72
+ "name": str(anim_name),
73
+ "frame_count": sf.get_frame_count(anim_name),
74
+ "fps": sf.get_animation_speed(anim_name),
75
+ "loop": sf.get_animation_loop(anim_name),
76
+ }
77
+
78
+ if max_depth >= 1:
79
+ var frames := []
80
+ for i in range(sf.get_frame_count(anim_name)):
81
+ var texture := sf.get_frame_texture(anim_name, i)
82
+ var frame_info := {
83
+ "index": i,
84
+ "duration": sf.get_frame_duration(anim_name, i),
85
+ }
86
+
87
+ if texture:
88
+ frame_info["texture_type"] = texture.get_class()
89
+
90
+ if texture is AtlasTexture:
91
+ var atlas := texture as AtlasTexture
92
+ if atlas.atlas:
93
+ frame_info["atlas_source"] = atlas.atlas.resource_path
94
+ frame_info["region"] = _serialize_rect2(atlas.region)
95
+ if atlas.margin != Rect2():
96
+ frame_info["margin"] = _serialize_rect2(atlas.margin)
97
+ elif texture.resource_path:
98
+ frame_info["texture_path"] = texture.resource_path
99
+
100
+ frames.append(frame_info)
101
+ anim_info["frames"] = frames
102
+
103
+ animations.append(anim_info)
104
+
105
+ return {"animations": animations}
106
+
107
+
108
+ func _format_tileset(ts: TileSet, max_depth: int) -> Dictionary:
109
+ var sources := []
110
+
111
+ for i in range(ts.get_source_count()):
112
+ var source_id := ts.get_source_id(i)
113
+ var source := ts.get_source(source_id)
114
+ var source_info := {
115
+ "source_id": source_id,
116
+ "source_type": source.get_class(),
117
+ }
118
+
119
+ if source is TileSetAtlasSource:
120
+ var atlas := source as TileSetAtlasSource
121
+ if atlas.texture:
122
+ source_info["texture_path"] = atlas.texture.resource_path
123
+ source_info["texture_region_size"] = _serialize_vector2i(atlas.texture_region_size)
124
+ source_info["tile_count"] = atlas.get_tiles_count()
125
+
126
+ if max_depth >= 2:
127
+ var tiles := []
128
+ for j in range(atlas.get_tiles_count()):
129
+ var coords := atlas.get_tile_id(j)
130
+ tiles.append({
131
+ "atlas_coords": _serialize_vector2i(coords),
132
+ "size": _serialize_vector2i(atlas.get_tile_size_in_atlas(coords))
133
+ })
134
+ source_info["tiles"] = _truncate_array(tiles)
135
+
136
+ elif source is TileSetScenesCollectionSource:
137
+ var scenes := source as TileSetScenesCollectionSource
138
+ source_info["scene_count"] = scenes.get_scene_tiles_count()
139
+
140
+ sources.append(source_info)
141
+
142
+ return {
143
+ "tile_size": _serialize_vector2i(ts.tile_size),
144
+ "source_count": ts.get_source_count(),
145
+ "physics_layers_count": ts.get_physics_layers_count(),
146
+ "navigation_layers_count": ts.get_navigation_layers_count(),
147
+ "custom_data_layers_count": ts.get_custom_data_layers_count(),
148
+ "terrain_sets_count": ts.get_terrain_sets_count(),
149
+ "sources": sources
150
+ }
151
+
152
+
153
+ func _format_shader_material(mat: ShaderMaterial, max_depth: int) -> Dictionary:
154
+ var result := {
155
+ "shader_path": mat.shader.resource_path if mat.shader else ""
156
+ }
157
+
158
+ if mat.shader and max_depth >= 1:
159
+ var params := {}
160
+ for prop in mat.get_property_list():
161
+ var prop_name: String = prop["name"]
162
+ if prop_name.begins_with("shader_parameter/"):
163
+ var param_name := prop_name.substr(len("shader_parameter/"))
164
+ var value = mat.get_shader_parameter(param_name)
165
+ params[param_name] = _serialize_property_value(value, max_depth - 1)
166
+ if not params.is_empty():
167
+ result["shader_parameters"] = params
168
+
169
+ return result
170
+
171
+
172
+ func _format_standard_material(mat: BaseMaterial3D, max_depth: int) -> Dictionary:
173
+ var result := {
174
+ "albedo_color": _serialize_color(mat.albedo_color),
175
+ "metallic": mat.metallic,
176
+ "roughness": mat.roughness,
177
+ "emission_enabled": mat.emission_enabled,
178
+ "transparency": mat.transparency,
179
+ "cull_mode": mat.cull_mode,
180
+ "shading_mode": mat.shading_mode
181
+ }
182
+
183
+ if mat.albedo_texture:
184
+ result["albedo_texture"] = mat.albedo_texture.resource_path
185
+
186
+ if mat.emission_enabled and mat.emission_texture:
187
+ result["emission_texture"] = mat.emission_texture.resource_path
188
+
189
+ if mat.normal_enabled and mat.normal_texture:
190
+ result["normal_texture"] = mat.normal_texture.resource_path
191
+
192
+ return result
193
+
194
+
195
+ func _format_texture2d(tex: Texture2D) -> Dictionary:
196
+ var result := {
197
+ "width": tex.get_width(),
198
+ "height": tex.get_height(),
199
+ "texture_type": tex.get_class()
200
+ }
201
+
202
+ if tex is CompressedTexture2D:
203
+ var ct := tex as CompressedTexture2D
204
+ result["load_path"] = ct.load_path
205
+
206
+ if tex is AtlasTexture:
207
+ var at := tex as AtlasTexture
208
+ if at.atlas:
209
+ result["atlas_source"] = at.atlas.resource_path
210
+ result["region"] = _serialize_rect2(at.region)
211
+ if at.margin != Rect2():
212
+ result["margin"] = _serialize_rect2(at.margin)
213
+
214
+ return result
215
+
216
+
217
+ func _get_generic_properties(resource: Resource, max_depth: int, include_internal: bool) -> Dictionary:
218
+ var properties := {}
219
+
220
+ for prop in resource.get_property_list():
221
+ var prop_name: String = prop["name"]
222
+
223
+ if prop_name.begins_with("_") and not include_internal:
224
+ continue
225
+ if prop["usage"] & PROPERTY_USAGE_EDITOR == 0:
226
+ continue
227
+ if prop_name in ["resource_local_to_scene", "resource_path", "resource_name", "script"]:
228
+ continue
229
+
230
+ var value = resource.get(prop_name)
231
+ properties[prop_name] = _serialize_property_value(value, max_depth)
232
+
233
+ return properties
234
+
235
+
236
+ func _serialize_property_value(value: Variant, depth: int) -> Variant:
237
+ var value_type := typeof(value)
238
+
239
+ if value_type in BINARY_ARRAY_TYPES:
240
+ return {"_binary_array": true, "size": value.size(), "type": type_string(value_type)}
241
+
242
+ if value_type == TYPE_ARRAY:
243
+ if value.size() > MAX_ARRAY_PREVIEW:
244
+ var preview := []
245
+ for i in range(MAX_ARRAY_PREVIEW):
246
+ preview.append(_serialize_property_value(value[i], depth - 1) if depth > 0 else str(value[i]))
247
+ return {"_truncated": true, "size": value.size(), "preview": preview}
248
+ else:
249
+ var result := []
250
+ for item in value:
251
+ result.append(_serialize_property_value(item, depth - 1) if depth > 0 else str(item))
252
+ return result
253
+
254
+ if value_type == TYPE_DICTIONARY:
255
+ var result := {}
256
+ for key in value:
257
+ result[str(key)] = _serialize_property_value(value[key], depth - 1) if depth > 0 else str(value[key])
258
+ return result
259
+
260
+ if value_type == TYPE_OBJECT:
261
+ if value == null:
262
+ return null
263
+ if value is Resource:
264
+ if value.resource_path and not value.resource_path.is_empty():
265
+ return {"_resource_ref": value.resource_path, "type": value.get_class()}
266
+ elif depth > 0:
267
+ return {"_inline_resource": true, "type": value.get_class()}
268
+ return str(value)
269
+ return str(value)
270
+
271
+ return _serialize_value(value)
272
+
273
+
274
+ func _serialize_rect2(r: Rect2) -> Dictionary:
275
+ return {"x": r.position.x, "y": r.position.y, "width": r.size.x, "height": r.size.y}
276
+
277
+
278
+ func _serialize_vector2i(v: Vector2i) -> Dictionary:
279
+ return {"x": v.x, "y": v.y}
280
+
281
+
282
+ func _serialize_color(c: Color) -> Dictionary:
283
+ return {"r": c.r, "g": c.g, "b": c.b, "a": c.a}
284
+
285
+
286
+ func _truncate_array(arr: Array, limit: int = MAX_ARRAY_PREVIEW) -> Variant:
287
+ if arr.size() <= limit:
288
+ return arr
289
+ return {
290
+ "_truncated": true,
291
+ "size": arr.size(),
292
+ "preview": arr.slice(0, limit)
293
+ }
@@ -0,0 +1 @@
1
+ uid://tg6qm22o8jlf
@@ -0,0 +1,162 @@
1
+ @tool
2
+ extends MCPBaseCommand
3
+ class_name MCPScene3DCommands
4
+
5
+
6
+ func get_commands() -> Dictionary:
7
+ return {
8
+ "get_spatial_info": get_spatial_info,
9
+ "get_scene_bounds": get_scene_bounds,
10
+ }
11
+
12
+
13
+ func get_spatial_info(params: Dictionary) -> Dictionary:
14
+ var scene_check := _require_scene_open()
15
+ if not scene_check.is_empty():
16
+ return scene_check
17
+
18
+ var node_path: String = params.get("node_path", "")
19
+ var include_children: bool = params.get("include_children", false)
20
+ var type_filter: String = params.get("type_filter", "")
21
+ var max_results: int = params.get("max_results", 0)
22
+ var within_aabb: Dictionary = params.get("within_aabb", {})
23
+
24
+ if node_path.is_empty():
25
+ return _error("INVALID_PARAMS", "node_path is required")
26
+
27
+ var node := _get_node(node_path)
28
+ if not node:
29
+ return _error("NODE_NOT_FOUND", "Node not found: %s" % node_path)
30
+
31
+ if not node is Node3D:
32
+ return _error("NOT_NODE3D", "Node is not a Node3D: %s" % node_path)
33
+
34
+ var filter_aabb: AABB = AABB()
35
+ var use_aabb_filter := false
36
+ if not within_aabb.is_empty():
37
+ var pos: Dictionary = within_aabb.get("position", {})
38
+ var size: Dictionary = within_aabb.get("size", {})
39
+ if pos.has("x") and size.has("x"):
40
+ filter_aabb = AABB(
41
+ Vector3(pos.get("x", 0), pos.get("y", 0), pos.get("z", 0)),
42
+ Vector3(size.get("x", 0), size.get("y", 0), size.get("z", 0))
43
+ )
44
+ use_aabb_filter = true
45
+
46
+ var nodes: Array[Dictionary] = []
47
+ var state := {"max": max_results, "count": 0, "stopped": false}
48
+ _collect_spatial_info(node, nodes, type_filter, include_children, use_aabb_filter, filter_aabb, state)
49
+
50
+ var result := {"nodes": nodes, "count": nodes.size()}
51
+ if state.stopped:
52
+ result["truncated"] = true
53
+ result["max_results"] = max_results
54
+ return _success(result)
55
+
56
+
57
+ func _collect_spatial_info(node: Node, results: Array[Dictionary], type_filter: String, include_children: bool, use_aabb_filter: bool, filter_aabb: AABB, state: Dictionary) -> void:
58
+ if state.max > 0 and state.count >= state.max:
59
+ state.stopped = true
60
+ return
61
+
62
+ if node is Node3D:
63
+ var node3d := node as Node3D
64
+ var type_matches := type_filter.is_empty() or node.is_class(type_filter)
65
+ var aabb_matches := true
66
+ if use_aabb_filter:
67
+ aabb_matches = filter_aabb.has_point(node3d.global_position)
68
+ if type_matches and aabb_matches:
69
+ results.append(_get_node3d_info(node3d))
70
+ state.count += 1
71
+
72
+ if include_children and not state.stopped:
73
+ for child in node.get_children():
74
+ _collect_spatial_info(child, results, type_filter, true, use_aabb_filter, filter_aabb, state)
75
+ if state.stopped:
76
+ break
77
+
78
+
79
+ func _get_node3d_info(node: Node3D) -> Dictionary:
80
+ var scene_root := EditorInterface.get_edited_scene_root()
81
+ var relative_path := scene_root.get_path_to(node)
82
+ var usable_path := "/root/" + scene_root.name
83
+ if relative_path != NodePath("."):
84
+ usable_path += "/" + str(relative_path)
85
+
86
+ var gpos: Vector3 = node.global_position
87
+ var grot: Vector3 = node.global_rotation
88
+ var gscale: Vector3 = node.global_transform.basis.get_scale()
89
+
90
+ var info := {
91
+ "path": usable_path,
92
+ "type": node.get_class(),
93
+ "global_position": {"x": gpos.x, "y": gpos.y, "z": gpos.z},
94
+ "global_rotation": {"x": grot.x, "y": grot.y, "z": grot.z},
95
+ "global_scale": {"x": gscale.x, "y": gscale.y, "z": gscale.z},
96
+ "visible": node.visible,
97
+ }
98
+
99
+ if node is VisualInstance3D:
100
+ var aabb := (node as VisualInstance3D).get_aabb()
101
+ var global_aabb := node.global_transform * aabb
102
+ info["aabb"] = _serialize_aabb(aabb)
103
+ info["global_aabb"] = _serialize_aabb(global_aabb)
104
+
105
+ return info
106
+
107
+
108
+ func _serialize_aabb(aabb: AABB) -> Dictionary:
109
+ return {
110
+ "position": _serialize_value(aabb.position),
111
+ "size": _serialize_value(aabb.size),
112
+ "end": _serialize_value(aabb.end),
113
+ }
114
+
115
+
116
+ func get_scene_bounds(params: Dictionary) -> Dictionary:
117
+ var scene_check := _require_scene_open()
118
+ if not scene_check.is_empty():
119
+ return scene_check
120
+
121
+ var root_path: String = params.get("root_path", "")
122
+ var scene_root := EditorInterface.get_edited_scene_root()
123
+
124
+ var search_root: Node = scene_root
125
+ if not root_path.is_empty():
126
+ search_root = _get_node(root_path)
127
+ if not search_root:
128
+ return _error("NODE_NOT_FOUND", "Root node not found: %s" % root_path)
129
+
130
+ var state := {"aabb": AABB(), "count": 0, "first": true}
131
+ _collect_bounds(search_root, state)
132
+
133
+ if state.count == 0:
134
+ return _error("NO_GEOMETRY", "No VisualInstance3D nodes found under: %s" % (root_path if not root_path.is_empty() else "scene root"))
135
+
136
+ var usable_path := "/root/" + scene_root.name
137
+ if search_root != scene_root:
138
+ var relative_path := scene_root.get_path_to(search_root)
139
+ usable_path += "/" + str(relative_path)
140
+
141
+ return _success({
142
+ "root_path": usable_path,
143
+ "node_count": state.count,
144
+ "combined_aabb": _serialize_aabb(state.aabb),
145
+ })
146
+
147
+
148
+ func _collect_bounds(node: Node, state: Dictionary) -> void:
149
+ if node is VisualInstance3D:
150
+ var visual := node as VisualInstance3D
151
+ var local_aabb := visual.get_aabb()
152
+ var global_aabb := visual.global_transform * local_aabb
153
+
154
+ if state.first:
155
+ state.aabb = global_aabb
156
+ state.first = false
157
+ else:
158
+ state.aabb = state.aabb.merge(global_aabb)
159
+ state.count += 1
160
+
161
+ for child in node.get_children():
162
+ _collect_bounds(child, state)
@@ -0,0 +1 @@
1
+ uid://dt8hadn4m25l8
@@ -0,0 +1,131 @@
1
+ @tool
2
+ extends MCPBaseCommand
3
+ class_name MCPSceneCommands
4
+
5
+
6
+ func get_commands() -> Dictionary:
7
+ return {
8
+ "get_current_scene": get_current_scene,
9
+ "get_scene_tree": get_scene_tree,
10
+ "open_scene": open_scene,
11
+ "save_scene": save_scene,
12
+ "create_scene": create_scene
13
+ }
14
+
15
+
16
+ func get_current_scene(_params: Dictionary) -> Dictionary:
17
+ var root := EditorInterface.get_edited_scene_root()
18
+ if not root:
19
+ return _success({
20
+ "path": null,
21
+ "root_name": null,
22
+ "root_type": null
23
+ })
24
+
25
+ return _success({
26
+ "path": root.scene_file_path,
27
+ "root_name": root.name,
28
+ "root_type": root.get_class()
29
+ })
30
+
31
+
32
+ func get_scene_tree(_params: Dictionary) -> Dictionary:
33
+ var root := EditorInterface.get_edited_scene_root()
34
+ if not root:
35
+ return _error("NO_SCENE", "No scene is currently open")
36
+
37
+ return _success({"tree": _build_tree(root)})
38
+
39
+
40
+ func _build_tree(node: Node) -> Dictionary:
41
+ var result := {
42
+ "name": node.name,
43
+ "type": node.get_class(),
44
+ }
45
+
46
+ if node is Node2D:
47
+ var pos: Vector2 = node.position
48
+ result["position"] = {"x": pos.x, "y": pos.y}
49
+ elif node is Node3D:
50
+ var pos: Vector3 = node.position
51
+ result["position"] = {"x": pos.x, "y": pos.y, "z": pos.z}
52
+
53
+ var children: Array[Dictionary] = []
54
+ for child in node.get_children():
55
+ children.append(_build_tree(child))
56
+
57
+ if not children.is_empty():
58
+ result["children"] = children
59
+
60
+ return result
61
+
62
+
63
+ func open_scene(params: Dictionary) -> Dictionary:
64
+ var scene_path: String = params.get("scene_path", "")
65
+ if scene_path.is_empty():
66
+ return _error("INVALID_PARAMS", "scene_path is required")
67
+
68
+ if not FileAccess.file_exists(scene_path):
69
+ return _error("FILE_NOT_FOUND", "Scene file not found: %s" % scene_path)
70
+
71
+ EditorInterface.open_scene_from_path(scene_path)
72
+ return _success({"path": scene_path})
73
+
74
+
75
+ func save_scene(params: Dictionary) -> Dictionary:
76
+ var root := EditorInterface.get_edited_scene_root()
77
+ if not root:
78
+ return _error("NO_SCENE", "No scene is currently open")
79
+
80
+ var path: String = params.get("path", "")
81
+ if path.is_empty():
82
+ path = root.scene_file_path
83
+
84
+ if path.is_empty():
85
+ return _error("NO_PATH", "Scene has no path and none was provided")
86
+
87
+ var packed_scene := PackedScene.new()
88
+ var err := packed_scene.pack(root)
89
+ if err != OK:
90
+ return _error("PACK_FAILED", "Failed to pack scene: %s" % error_string(err))
91
+
92
+ err = ResourceSaver.save(packed_scene, path)
93
+ if err != OK:
94
+ return _error("SAVE_FAILED", "Failed to save scene: %s" % error_string(err))
95
+
96
+ return _success({"path": path})
97
+
98
+
99
+ func create_scene(params: Dictionary) -> Dictionary:
100
+ var root_type: String = params.get("root_type", "")
101
+ var root_name: String = params.get("root_name", root_type)
102
+ var scene_path: String = params.get("scene_path", "")
103
+
104
+ if root_type.is_empty():
105
+ return _error("INVALID_PARAMS", "root_type is required")
106
+ if scene_path.is_empty():
107
+ return _error("INVALID_PARAMS", "scene_path is required")
108
+
109
+ if not ClassDB.class_exists(root_type):
110
+ return _error("INVALID_TYPE", "Unknown node type: %s" % root_type)
111
+
112
+ var root: Node = ClassDB.instantiate(root_type)
113
+ if not root:
114
+ return _error("CREATE_FAILED", "Failed to create node of type: %s" % root_type)
115
+
116
+ root.name = root_name
117
+
118
+ var packed_scene := PackedScene.new()
119
+ var err := packed_scene.pack(root)
120
+ root.free()
121
+
122
+ if err != OK:
123
+ return _error("PACK_FAILED", "Failed to pack scene: %s" % error_string(err))
124
+
125
+ err = ResourceSaver.save(packed_scene, scene_path)
126
+ if err != OK:
127
+ return _error("SAVE_FAILED", "Failed to save scene: %s" % error_string(err))
128
+
129
+ EditorInterface.open_scene_from_path(scene_path)
130
+ return _success({"path": scene_path})
131
+
@@ -0,0 +1 @@
1
+ uid://odetea75q17p