@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.
- package/README.md +58 -27
- package/addon/command_router.gd +39 -0
- package/addon/command_router.gd.uid +1 -0
- package/addon/commands/animation_commands.gd +633 -0
- package/addon/commands/animation_commands.gd.uid +1 -0
- package/addon/commands/debug_commands.gd +109 -0
- package/addon/commands/debug_commands.gd.uid +1 -0
- package/addon/commands/file_commands.gd +95 -0
- package/addon/commands/file_commands.gd.uid +1 -0
- package/addon/commands/node_commands.gd +255 -0
- package/addon/commands/node_commands.gd.uid +1 -0
- package/addon/commands/project_commands.gd +114 -0
- package/addon/commands/project_commands.gd.uid +1 -0
- package/addon/commands/resource_commands.gd +293 -0
- package/addon/commands/resource_commands.gd.uid +1 -0
- package/addon/commands/scene3d_commands.gd +162 -0
- package/addon/commands/scene3d_commands.gd.uid +1 -0
- package/addon/commands/scene_commands.gd +131 -0
- package/addon/commands/scene_commands.gd.uid +1 -0
- package/addon/commands/screenshot_commands.gd +130 -0
- package/addon/commands/screenshot_commands.gd.uid +1 -0
- package/addon/commands/script_commands.gd +156 -0
- package/addon/commands/script_commands.gd.uid +1 -0
- package/addon/commands/selection_commands.gd +170 -0
- package/addon/commands/selection_commands.gd.uid +1 -0
- package/addon/commands/system_commands.gd +29 -0
- package/addon/commands/tilemap_commands.gd +657 -0
- package/addon/commands/tilemap_commands.gd.uid +1 -0
- package/addon/core/base_command.gd +58 -0
- package/addon/core/base_command.gd.uid +1 -0
- package/addon/core/mcp_debugger_plugin.gd +149 -0
- package/addon/core/mcp_debugger_plugin.gd.uid +1 -0
- package/addon/core/mcp_logger.gd +40 -0
- package/addon/core/mcp_logger.gd.uid +1 -0
- package/addon/core/mcp_utils.gd +129 -0
- package/addon/core/mcp_utils.gd.uid +1 -0
- package/addon/game_bridge/mcp_game_bridge.gd +195 -0
- package/addon/game_bridge/mcp_game_bridge.gd.uid +1 -0
- package/addon/plugin.cfg +8 -0
- package/addon/plugin.gd +89 -0
- package/addon/plugin.gd.uid +1 -0
- package/addon/ui/status_panel.gd +23 -0
- package/addon/ui/status_panel.gd.uid +1 -0
- package/addon/ui/status_panel.tscn +41 -0
- package/addon/websocket_server.gd +143 -0
- package/addon/websocket_server.gd.uid +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -0
- package/dist/connection/websocket.d.ts +14 -0
- package/dist/connection/websocket.d.ts.map +1 -1
- package/dist/connection/websocket.js +69 -1
- package/dist/connection/websocket.js.map +1 -1
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -6
- package/dist/index.js.map +1 -1
- package/dist/installer/install.d.ts +13 -0
- package/dist/installer/install.d.ts.map +1 -0
- package/dist/installer/install.js +77 -0
- package/dist/installer/install.js.map +1 -0
- package/dist/tools/editor.d.ts.map +1 -1
- package/dist/tools/editor.js.map +1 -1
- package/dist/tools/project.d.ts +3 -3
- package/dist/tools/project.d.ts.map +1 -1
- package/dist/tools/project.js +28 -2
- package/dist/tools/project.js.map +1 -1
- package/dist/tools/scene3d.d.ts.map +1 -1
- package/dist/tools/scene3d.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +20 -0
- package/dist/version.js.map +1 -0
- 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
|