gdharness 0.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/LICENSE +22 -0
- package/README.md +125 -0
- package/build/cli.js +30353 -0
- package/build/godot/addons/auto_reload/auto_reload.gd +89 -0
- package/build/godot/addons/auto_reload/plugin.cfg +6 -0
- package/build/godot/addons/gdharness_editor/bridge_client.gd +197 -0
- package/build/godot/addons/gdharness_editor/plugin.cfg +6 -0
- package/build/godot/addons/gdharness_editor/plugin.gd +88 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +104 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +397 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +85 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +427 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +885 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +292 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +77 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +254 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +283 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +169 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +119 -0
- package/build/godot/operations/audio_buses.gd +125 -0
- package/build/godot/operations/class_cache.gd +180 -0
- package/build/godot/operations/classdb_queries.gd +252 -0
- package/build/godot/operations/dependencies.gd +293 -0
- package/build/godot/operations/file_walk.gd +55 -0
- package/build/godot/operations/gdscript_analysis.gd +288 -0
- package/build/godot/operations/gdscript_authoring.gd +419 -0
- package/build/godot/operations/godot_operations.gd +186 -0
- package/build/godot/operations/import_pipeline.gd +390 -0
- package/build/godot/operations/input_actions.gd +237 -0
- package/build/godot/operations/logger.gd +33 -0
- package/build/godot/operations/plugins.gd +159 -0
- package/build/godot/operations/project_config.gd +153 -0
- package/build/godot/operations/project_diagnostics.gd +159 -0
- package/build/godot/operations/resource_files.gd +132 -0
- package/build/godot/operations/serialisation.gd +154 -0
- package/build/index.js +29252 -0
- package/package.json +57 -0
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends Node
|
|
3
|
+
|
|
4
|
+
## Scenes and their nodes, edited in the open editor and read back from what it holds.
|
|
5
|
+
|
|
6
|
+
var _editor_plugin: EditorPlugin = null
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
10
|
+
_editor_plugin = plugin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
func _refresh_and_reload(scene_path: String) -> void:
|
|
14
|
+
_refresh_filesystem()
|
|
15
|
+
_reload_scene_in_editor(scene_path)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
func _refresh_filesystem() -> void:
|
|
19
|
+
if _editor_plugin:
|
|
20
|
+
EditorInterface.get_resource_filesystem().scan()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
func _reload_scene_in_editor(scene_path: String) -> void:
|
|
24
|
+
if not _editor_plugin:
|
|
25
|
+
return
|
|
26
|
+
var edited: Node = EditorInterface.get_edited_scene_root()
|
|
27
|
+
if edited and edited.scene_file_path == scene_path:
|
|
28
|
+
EditorInterface.reload_scene_from_path(scene_path)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
func _ensure_res_path(path: String) -> String:
|
|
32
|
+
if not path.begins_with("res://"):
|
|
33
|
+
return "res://" + path
|
|
34
|
+
return path
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
func _to_scene_res_path(project_path: String, scene_path: String) -> String:
|
|
38
|
+
var p: String = scene_path.strip_edges()
|
|
39
|
+
if p.begins_with("res://"):
|
|
40
|
+
return p
|
|
41
|
+
|
|
42
|
+
if project_path.strip_edges() != "":
|
|
43
|
+
var normalized_project: String = project_path.replace("\\", "/")
|
|
44
|
+
var normalized_scene: String = p.replace("\\", "/")
|
|
45
|
+
if normalized_scene.begins_with(normalized_project):
|
|
46
|
+
var rel: String = normalized_scene.substr(normalized_project.length())
|
|
47
|
+
if rel.begins_with("/"):
|
|
48
|
+
rel = rel.substr(1)
|
|
49
|
+
return _ensure_res_path(rel)
|
|
50
|
+
|
|
51
|
+
return _ensure_res_path(p)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
func _load_scene(scene_path: String) -> Array:
|
|
55
|
+
if not FileAccess.file_exists(scene_path):
|
|
56
|
+
return [null, {"ok": false, "error": "Scene not found: " + scene_path}]
|
|
57
|
+
var packed: PackedScene = load(scene_path)
|
|
58
|
+
if not packed:
|
|
59
|
+
return [null, {"ok": false, "error": "Failed to load: " + scene_path}]
|
|
60
|
+
var root: Node = packed.instantiate()
|
|
61
|
+
if not root:
|
|
62
|
+
return [null, {"ok": false, "error": "Failed to instantiate: " + scene_path}]
|
|
63
|
+
return [root, {}]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
func _save_scene(scene_root: Node, scene_path: String) -> Dictionary:
|
|
67
|
+
var packed: PackedScene = PackedScene.new()
|
|
68
|
+
if packed.pack(scene_root) != OK:
|
|
69
|
+
scene_root.queue_free()
|
|
70
|
+
return {"ok": false, "error": "Failed to pack scene"}
|
|
71
|
+
if ResourceSaver.save(packed, scene_path) != OK:
|
|
72
|
+
scene_root.queue_free()
|
|
73
|
+
return {"ok": false, "error": "Failed to save scene"}
|
|
74
|
+
scene_root.queue_free()
|
|
75
|
+
_refresh_and_reload(scene_path)
|
|
76
|
+
return {}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
func _find_node(root: Node, path: String) -> Node:
|
|
80
|
+
if path == "." or path.is_empty():
|
|
81
|
+
return root
|
|
82
|
+
return root.get_node_or_null(path)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## Turns what arrived over the wire into the Godot value a property wants.
|
|
86
|
+
##
|
|
87
|
+
## Three separate questions, asked in order, because a caller may say what it means in three
|
|
88
|
+
## ways: a dictionary that names its own type, a dictionary shaped like the type the property
|
|
89
|
+
## declares, or a bare array positional for a vector. Each is its own function; asking all three
|
|
90
|
+
## in one was twenty-four exits deep and impossible to follow.
|
|
91
|
+
func _parse_value(value: Variant, expected_type: int = TYPE_NIL) -> Variant:
|
|
92
|
+
if typeof(value) == TYPE_DICTIONARY:
|
|
93
|
+
var tagged: Array = _parse_tagged_dictionary(value)
|
|
94
|
+
if tagged[0]:
|
|
95
|
+
return tagged[1]
|
|
96
|
+
return _parse_shaped_dictionary(value, expected_type)
|
|
97
|
+
if typeof(value) == TYPE_ARRAY:
|
|
98
|
+
return _parse_array(value, expected_type)
|
|
99
|
+
return value
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## A dictionary carrying its own type name, as the serialiser writes it.
|
|
103
|
+
##
|
|
104
|
+
## Answers [handled, value] rather than just the value, because a handled tag may legitimately
|
|
105
|
+
## produce null: a Resource with no path is "this is nothing", not "this is not mine". A tag
|
|
106
|
+
## that names a type but lacks the keys to build it is left unhandled on purpose, so the caller
|
|
107
|
+
## can still read it against the type the property declares.
|
|
108
|
+
func _parse_tagged_dictionary(value: Dictionary) -> Array:
|
|
109
|
+
var type_tag: String = ""
|
|
110
|
+
if value.has("type"):
|
|
111
|
+
type_tag = str(value["type"])
|
|
112
|
+
elif value.has("_type"):
|
|
113
|
+
type_tag = str(value["_type"])
|
|
114
|
+
|
|
115
|
+
match type_tag:
|
|
116
|
+
"Vector2":
|
|
117
|
+
return [true, Vector2(value.get("x", 0), value.get("y", 0))]
|
|
118
|
+
"Vector3":
|
|
119
|
+
return [true, Vector3(value.get("x", 0), value.get("y", 0), value.get("z", 0))]
|
|
120
|
+
"Color":
|
|
121
|
+
return [true, Color(value.get("r", 1), value.get("g", 1), value.get("b", 1), value.get("a", 1))]
|
|
122
|
+
"Vector2i":
|
|
123
|
+
return [true, Vector2i(value.get("x", 0), value.get("y", 0))]
|
|
124
|
+
"Vector3i":
|
|
125
|
+
return [true, Vector3i(value.get("x", 0), value.get("y", 0), value.get("z", 0))]
|
|
126
|
+
"Rect2":
|
|
127
|
+
return [
|
|
128
|
+
true,
|
|
129
|
+
Rect2(value.get("x", 0), value.get("y", 0), value.get("width", 0), value.get("height", 0))
|
|
130
|
+
]
|
|
131
|
+
"Transform2D":
|
|
132
|
+
return _parse_transform2d(value)
|
|
133
|
+
"Transform3D":
|
|
134
|
+
return _parse_transform3d(value)
|
|
135
|
+
"NodePath":
|
|
136
|
+
return [true, NodePath(value.get("path", ""))]
|
|
137
|
+
"Resource":
|
|
138
|
+
var resource_path: String = str(value.get("path", ""))
|
|
139
|
+
return [true, null if resource_path.is_empty() else load(resource_path)]
|
|
140
|
+
_:
|
|
141
|
+
return _parse_new_resource(type_tag, value)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
## A tag naming a Resource class builds a fresh one, its other keys set as properties, so a
|
|
145
|
+
## NavigationRegion2D can arrive with its NavigationPolygon and an AnimationTree with its root
|
|
146
|
+
## state machine in the same add as any other property.
|
|
147
|
+
func _parse_new_resource(type_tag: String, value: Dictionary) -> Array:
|
|
148
|
+
if (
|
|
149
|
+
type_tag.is_empty()
|
|
150
|
+
or not ClassDB.class_exists(type_tag)
|
|
151
|
+
or not ClassDB.is_parent_class(type_tag, "Resource")
|
|
152
|
+
or not ClassDB.can_instantiate(type_tag)
|
|
153
|
+
):
|
|
154
|
+
return [false, null]
|
|
155
|
+
|
|
156
|
+
var built: Resource = ClassDB.instantiate(type_tag)
|
|
157
|
+
for key: Variant in value:
|
|
158
|
+
var property: String = str(key)
|
|
159
|
+
if property == "_type" or property == "type":
|
|
160
|
+
continue
|
|
161
|
+
built.set(property, _parse_value(value[key], typeof(built.get(property))))
|
|
162
|
+
return [true, built]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
func _parse_transform2d(value: Dictionary) -> Array:
|
|
166
|
+
if not (value.has("x") and value.has("y") and value.has("origin")):
|
|
167
|
+
return [false, null]
|
|
168
|
+
|
|
169
|
+
var basis_x: Dictionary = value["x"]
|
|
170
|
+
var basis_y: Dictionary = value["y"]
|
|
171
|
+
var origin: Dictionary = value["origin"]
|
|
172
|
+
return [
|
|
173
|
+
true,
|
|
174
|
+
Transform2D(
|
|
175
|
+
Vector2(basis_x.get("x", 1), basis_x.get("y", 0)),
|
|
176
|
+
Vector2(basis_y.get("x", 0), basis_y.get("y", 1)),
|
|
177
|
+
Vector2(origin.get("x", 0), origin.get("y", 0))
|
|
178
|
+
)
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
func _parse_transform3d(value: Dictionary) -> Array:
|
|
183
|
+
if not (value.has("basis") and value.has("origin")):
|
|
184
|
+
return [false, null]
|
|
185
|
+
|
|
186
|
+
var b: Dictionary = value["basis"]
|
|
187
|
+
var o: Dictionary = value["origin"]
|
|
188
|
+
var x: Dictionary = b.get("x", {})
|
|
189
|
+
var y: Dictionary = b.get("y", {})
|
|
190
|
+
var z: Dictionary = b.get("z", {})
|
|
191
|
+
var basis: Basis = Basis(
|
|
192
|
+
Vector3(x.get("x", 1), x.get("y", 0), x.get("z", 0)),
|
|
193
|
+
Vector3(y.get("x", 0), y.get("y", 1), y.get("z", 0)),
|
|
194
|
+
Vector3(z.get("x", 0), z.get("y", 0), z.get("z", 1))
|
|
195
|
+
)
|
|
196
|
+
return [true, Transform3D(basis, Vector3(o.get("x", 0), o.get("y", 0), o.get("z", 0)))]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
## A dictionary with no tag, read against the type the property declares. Falls back to the
|
|
200
|
+
## dictionary itself, since a property may genuinely want one.
|
|
201
|
+
func _parse_shaped_dictionary(value: Dictionary, expected_type: int) -> Variant:
|
|
202
|
+
match expected_type:
|
|
203
|
+
TYPE_VECTOR2:
|
|
204
|
+
if value.has("x") and value.has("y"):
|
|
205
|
+
return Vector2(value.get("x", 0), value.get("y", 0))
|
|
206
|
+
TYPE_VECTOR2I:
|
|
207
|
+
if value.has("x") and value.has("y"):
|
|
208
|
+
return Vector2i(value.get("x", 0), value.get("y", 0))
|
|
209
|
+
TYPE_VECTOR3:
|
|
210
|
+
if value.has("x") and value.has("y") and value.has("z"):
|
|
211
|
+
return Vector3(value.get("x", 0), value.get("y", 0), value.get("z", 0))
|
|
212
|
+
TYPE_VECTOR3I:
|
|
213
|
+
if value.has("x") and value.has("y") and value.has("z"):
|
|
214
|
+
return Vector3i(value.get("x", 0), value.get("y", 0), value.get("z", 0))
|
|
215
|
+
TYPE_COLOR:
|
|
216
|
+
if value.has("r") and value.has("g") and value.has("b"):
|
|
217
|
+
return Color(value.get("r", 1), value.get("g", 1), value.get("b", 1), value.get("a", 1))
|
|
218
|
+
TYPE_RECT2:
|
|
219
|
+
if value.has("x") and value.has("y") and value.has("width") and value.has("height"):
|
|
220
|
+
return Rect2(
|
|
221
|
+
value.get("x", 0), value.get("y", 0), value.get("width", 0), value.get("height", 0)
|
|
222
|
+
)
|
|
223
|
+
TYPE_NODE_PATH:
|
|
224
|
+
if value.has("path"):
|
|
225
|
+
return NodePath(value.get("path", ""))
|
|
226
|
+
return value
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
## An array, either positional for a vector the property declares, or a list to parse per item.
|
|
230
|
+
func _parse_array(value: Array, expected_type: int) -> Variant:
|
|
231
|
+
match expected_type:
|
|
232
|
+
TYPE_VECTOR2:
|
|
233
|
+
if value.size() >= 2:
|
|
234
|
+
return Vector2(value[0], value[1])
|
|
235
|
+
TYPE_VECTOR2I:
|
|
236
|
+
if value.size() >= 2:
|
|
237
|
+
return Vector2i(value[0], value[1])
|
|
238
|
+
TYPE_VECTOR3:
|
|
239
|
+
if value.size() >= 3:
|
|
240
|
+
return Vector3(value[0], value[1], value[2])
|
|
241
|
+
TYPE_VECTOR3I:
|
|
242
|
+
if value.size() >= 3:
|
|
243
|
+
return Vector3i(value[0], value[1], value[2])
|
|
244
|
+
return value.map(func(item: Variant) -> Variant: return _parse_value(item))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
func _get_property_type(node: Node, prop_name: String) -> int:
|
|
248
|
+
for prop: Dictionary in node.get_property_list():
|
|
249
|
+
if str(prop.get("name", "")) == prop_name:
|
|
250
|
+
return int(prop.get("type", TYPE_NIL))
|
|
251
|
+
return TYPE_NIL
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
func _serialize_value(value: Variant) -> Variant:
|
|
255
|
+
match typeof(value):
|
|
256
|
+
TYPE_VECTOR2:
|
|
257
|
+
return {"type": "Vector2", "x": value.x, "y": value.y}
|
|
258
|
+
TYPE_VECTOR3:
|
|
259
|
+
return {"type": "Vector3", "x": value.x, "y": value.y, "z": value.z}
|
|
260
|
+
TYPE_COLOR:
|
|
261
|
+
return {"type": "Color", "r": value.r, "g": value.g, "b": value.b, "a": value.a}
|
|
262
|
+
TYPE_VECTOR2I:
|
|
263
|
+
return {"type": "Vector2i", "x": value.x, "y": value.y}
|
|
264
|
+
TYPE_VECTOR3I:
|
|
265
|
+
return {"type": "Vector3i", "x": value.x, "y": value.y, "z": value.z}
|
|
266
|
+
TYPE_RECT2:
|
|
267
|
+
return {
|
|
268
|
+
"type": "Rect2",
|
|
269
|
+
"x": value.position.x,
|
|
270
|
+
"y": value.position.y,
|
|
271
|
+
"width": value.size.x,
|
|
272
|
+
"height": value.size.y
|
|
273
|
+
}
|
|
274
|
+
TYPE_NODE_PATH:
|
|
275
|
+
return {"type": "NodePath", "path": str(value)}
|
|
276
|
+
TYPE_TRANSFORM2D:
|
|
277
|
+
return {
|
|
278
|
+
"type": "Transform2D",
|
|
279
|
+
"x": {"x": value.x.x, "y": value.x.y},
|
|
280
|
+
"y": {"x": value.y.x, "y": value.y.y},
|
|
281
|
+
"origin": {"x": value.origin.x, "y": value.origin.y}
|
|
282
|
+
}
|
|
283
|
+
TYPE_TRANSFORM3D:
|
|
284
|
+
return {
|
|
285
|
+
"type": "Transform3D",
|
|
286
|
+
"basis":
|
|
287
|
+
{
|
|
288
|
+
"x": {"x": value.basis.x.x, "y": value.basis.x.y, "z": value.basis.x.z},
|
|
289
|
+
"y": {"x": value.basis.y.x, "y": value.basis.y.y, "z": value.basis.y.z},
|
|
290
|
+
"z": {"x": value.basis.z.x, "y": value.basis.z.y, "z": value.basis.z.z}
|
|
291
|
+
},
|
|
292
|
+
"origin": {"x": value.origin.x, "y": value.origin.y, "z": value.origin.z}
|
|
293
|
+
}
|
|
294
|
+
TYPE_OBJECT:
|
|
295
|
+
if value and value is Resource and value.resource_path:
|
|
296
|
+
return {"type": "Resource", "path": value.resource_path}
|
|
297
|
+
return null
|
|
298
|
+
_:
|
|
299
|
+
return value
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
## Set each property, answering with what went wrong or "" when nothing did.
|
|
303
|
+
##
|
|
304
|
+
## A property the node does not have, and a resource path nothing is at, are both refused: Object
|
|
305
|
+
## .set ignores an unknown name and stores null for a resource that would not load, so either one
|
|
306
|
+
## saves a scene that quietly did not change and reports it as a change that did.
|
|
307
|
+
func _set_node_properties(node: Node, properties: Dictionary) -> String:
|
|
308
|
+
for prop_name: Variant in properties:
|
|
309
|
+
var property: String = str(prop_name)
|
|
310
|
+
if not _has_property(node, property):
|
|
311
|
+
return "%s has no property %s" % [node.get_class(), property]
|
|
312
|
+
|
|
313
|
+
var expected_type: int = _get_property_type(node, property)
|
|
314
|
+
var raw: Variant = properties[prop_name]
|
|
315
|
+
|
|
316
|
+
# A resource-valued property takes the path of one, which is how a caller names a
|
|
317
|
+
# TileSet, a material or a theme: there is no other way to hand a tool a Resource.
|
|
318
|
+
if expected_type == TYPE_OBJECT and typeof(raw) == TYPE_STRING:
|
|
319
|
+
var path: String = String(raw)
|
|
320
|
+
# The project boundary is enforced here as well as on the server, because only the
|
|
321
|
+
# engine knows that this property is one holding a path: an absolute path and a
|
|
322
|
+
# user:// one both load, and neither names a file this project owns.
|
|
323
|
+
if not (path.begins_with("res://") or path.begins_with("uid://")):
|
|
324
|
+
return "%s takes a res:// or uid:// path, not %s" % [property, path]
|
|
325
|
+
if path.split("/").has(".."):
|
|
326
|
+
return "%s leaves the project: %s" % [property, path]
|
|
327
|
+
if not ResourceLoader.exists(path):
|
|
328
|
+
return "No resource at %s for %s" % [path, property]
|
|
329
|
+
node.set(property, load(path))
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
node.set(property, _parse_value(raw, expected_type))
|
|
333
|
+
return ""
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
func _has_property(node: Node, prop_name: String) -> bool:
|
|
337
|
+
for prop: Dictionary in node.get_property_list():
|
|
338
|
+
if str(prop.get("name", "")) == prop_name:
|
|
339
|
+
return true
|
|
340
|
+
return false
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
func _parse_properties_arg(raw_properties: Variant) -> Dictionary:
|
|
344
|
+
if typeof(raw_properties) == TYPE_DICTIONARY:
|
|
345
|
+
return raw_properties
|
|
346
|
+
if typeof(raw_properties) == TYPE_STRING:
|
|
347
|
+
var text: String = String(raw_properties)
|
|
348
|
+
if text.strip_edges().is_empty():
|
|
349
|
+
return {}
|
|
350
|
+
var parsed: Variant = JSON.parse_string(text)
|
|
351
|
+
if typeof(parsed) == TYPE_DICTIONARY:
|
|
352
|
+
return parsed
|
|
353
|
+
return {}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
func _ensure_parent_dir_for_scene(scene_path: String) -> void:
|
|
357
|
+
var base_dir: String = scene_path.get_base_dir()
|
|
358
|
+
if not DirAccess.dir_exists_absolute(base_dir):
|
|
359
|
+
DirAccess.make_dir_recursive_absolute(base_dir)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
func _set_owner_recursive(node: Node, scene_owner: Node) -> void:
|
|
363
|
+
node.owner = scene_owner
|
|
364
|
+
for child: Node in node.get_children():
|
|
365
|
+
_set_owner_recursive(child, scene_owner)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
func _build_node_tree(
|
|
369
|
+
node: Node, include_properties: bool, depth: int, current_depth: int, node_path: String
|
|
370
|
+
) -> Dictionary:
|
|
371
|
+
var children: Array[Dictionary] = []
|
|
372
|
+
var data: Dictionary = {
|
|
373
|
+
"name": str(node.name), "type": node.get_class(), "path": node_path, "children": children
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if include_properties:
|
|
377
|
+
var props: Dictionary = {}
|
|
378
|
+
for p: Dictionary in node.get_property_list():
|
|
379
|
+
if not (int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
380
|
+
continue
|
|
381
|
+
var pn: String = str(p.get("name", ""))
|
|
382
|
+
if pn.is_empty():
|
|
383
|
+
continue
|
|
384
|
+
props[pn] = _serialize_value(node.get(pn))
|
|
385
|
+
data["properties"] = props
|
|
386
|
+
|
|
387
|
+
if depth >= 0 and current_depth >= depth:
|
|
388
|
+
return data
|
|
389
|
+
|
|
390
|
+
for child: Node in node.get_children():
|
|
391
|
+
var child_path: String = str(child.name) if node_path == "." else node_path + "/" + str(child.name)
|
|
392
|
+
children.append(_build_node_tree(child, include_properties, depth, current_depth + 1, child_path))
|
|
393
|
+
|
|
394
|
+
return data
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
func _collect_nodes_recursive(node: Node, path: String, out_nodes: Array) -> void:
|
|
398
|
+
out_nodes.append({"path": path, "node": node})
|
|
399
|
+
for child: Node in node.get_children():
|
|
400
|
+
var child_path: String = str(child.name) if path == "." else path + "/" + str(child.name)
|
|
401
|
+
_collect_nodes_recursive(child, child_path, out_nodes)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
func create_scene(args: Dictionary) -> Dictionary:
|
|
405
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
406
|
+
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
407
|
+
var root_node_type: String = str(args.get("rootNodeType", "Node"))
|
|
408
|
+
var script_path: String = str(args.get("scriptPath", ""))
|
|
409
|
+
|
|
410
|
+
if scene_path == "res://":
|
|
411
|
+
return {"ok": false, "error": "Missing scenePath"}
|
|
412
|
+
if not scene_path.ends_with(".tscn"):
|
|
413
|
+
scene_path += ".tscn"
|
|
414
|
+
if not ClassDB.class_exists(root_node_type):
|
|
415
|
+
return {"ok": false, "error": "Invalid rootNodeType: " + root_node_type}
|
|
416
|
+
|
|
417
|
+
_ensure_parent_dir_for_scene(scene_path)
|
|
418
|
+
|
|
419
|
+
var root: Node = ClassDB.instantiate(root_node_type)
|
|
420
|
+
if not root:
|
|
421
|
+
return {"ok": false, "error": "Failed to instantiate root node: " + root_node_type}
|
|
422
|
+
root.name = root_node_type
|
|
423
|
+
|
|
424
|
+
if not script_path.is_empty():
|
|
425
|
+
var full_script_path: String = _to_scene_res_path(project_path, script_path)
|
|
426
|
+
var script: Resource = load(full_script_path)
|
|
427
|
+
if not script:
|
|
428
|
+
root.queue_free()
|
|
429
|
+
return {"ok": false, "error": "Failed to load script: " + full_script_path}
|
|
430
|
+
root.set_script(script)
|
|
431
|
+
|
|
432
|
+
var err: Dictionary = _save_scene(root, scene_path)
|
|
433
|
+
if not err.is_empty():
|
|
434
|
+
return err
|
|
435
|
+
|
|
436
|
+
return {"ok": true, "scenePath": scene_path, "rootNodeType": root_node_type}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
func list_scene_nodes(args: Dictionary) -> Dictionary:
|
|
440
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
441
|
+
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
442
|
+
var depth: int = int(args.get("depth", -1))
|
|
443
|
+
var include_properties: bool = bool(args.get("includeProperties", false))
|
|
444
|
+
|
|
445
|
+
var loaded: Array = _load_scene(scene_path)
|
|
446
|
+
var refused: Dictionary = loaded[1]
|
|
447
|
+
if not refused.is_empty():
|
|
448
|
+
return refused
|
|
449
|
+
|
|
450
|
+
var root: Node = loaded[0]
|
|
451
|
+
var tree: Dictionary = _build_node_tree(root, include_properties, depth, 0, ".")
|
|
452
|
+
root.queue_free()
|
|
453
|
+
return {"ok": true, "tree": tree}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
func add_node(args: Dictionary) -> Dictionary:
|
|
457
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
458
|
+
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
459
|
+
var node_type: String = str(args.get("nodeType", ""))
|
|
460
|
+
var node_name: String = str(args.get("nodeName", ""))
|
|
461
|
+
var parent_node_path: String = str(args.get("parentNodePath", "."))
|
|
462
|
+
var properties: Dictionary = _parse_properties_arg(args.get("properties", {}))
|
|
463
|
+
|
|
464
|
+
if node_type.is_empty() or node_name.is_empty():
|
|
465
|
+
return {"ok": false, "error": "Missing nodeType or nodeName"}
|
|
466
|
+
if not ClassDB.class_exists(node_type):
|
|
467
|
+
return {"ok": false, "error": "Invalid nodeType: " + node_type}
|
|
468
|
+
|
|
469
|
+
var loaded: Array = _load_scene(scene_path)
|
|
470
|
+
var refused: Dictionary = loaded[1]
|
|
471
|
+
if not refused.is_empty():
|
|
472
|
+
return refused
|
|
473
|
+
|
|
474
|
+
var root: Node = loaded[0]
|
|
475
|
+
var parent: Node = _find_node(root, parent_node_path)
|
|
476
|
+
if not parent:
|
|
477
|
+
root.queue_free()
|
|
478
|
+
return {"ok": false, "error": "Parent node not found: " + parent_node_path}
|
|
479
|
+
|
|
480
|
+
var new_node: Node = ClassDB.instantiate(node_type)
|
|
481
|
+
if not new_node:
|
|
482
|
+
root.queue_free()
|
|
483
|
+
return {"ok": false, "error": "Failed to instantiate nodeType: " + node_type}
|
|
484
|
+
|
|
485
|
+
new_node.name = node_name
|
|
486
|
+
var refused_property: String = _set_node_properties(new_node, properties)
|
|
487
|
+
if not refused_property.is_empty():
|
|
488
|
+
new_node.queue_free()
|
|
489
|
+
root.queue_free()
|
|
490
|
+
return {"ok": false, "error": refused_property}
|
|
491
|
+
|
|
492
|
+
parent.add_child(new_node)
|
|
493
|
+
_set_owner_recursive(new_node, root)
|
|
494
|
+
|
|
495
|
+
var err: Dictionary = _save_scene(root, scene_path)
|
|
496
|
+
if not err.is_empty():
|
|
497
|
+
return err
|
|
498
|
+
|
|
499
|
+
return {"ok": true, "nodeName": node_name, "nodeType": node_type}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
func delete_node(args: Dictionary) -> Dictionary:
|
|
503
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
504
|
+
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
505
|
+
var node_path: String = str(args.get("nodePath", ""))
|
|
506
|
+
|
|
507
|
+
if node_path.is_empty() or node_path == ".":
|
|
508
|
+
return {"ok": false, "error": "Cannot delete root node"}
|
|
509
|
+
|
|
510
|
+
var loaded: Array = _load_scene(scene_path)
|
|
511
|
+
var refused: Dictionary = loaded[1]
|
|
512
|
+
if not refused.is_empty():
|
|
513
|
+
return refused
|
|
514
|
+
|
|
515
|
+
var root: Node = loaded[0]
|
|
516
|
+
var node: Node = _find_node(root, node_path)
|
|
517
|
+
if not node:
|
|
518
|
+
root.queue_free()
|
|
519
|
+
return {"ok": false, "error": "Node not found: " + node_path}
|
|
520
|
+
|
|
521
|
+
var parent: Node = node.get_parent()
|
|
522
|
+
if not parent:
|
|
523
|
+
root.queue_free()
|
|
524
|
+
return {"ok": false, "error": "Cannot delete root node"}
|
|
525
|
+
|
|
526
|
+
parent.remove_child(node)
|
|
527
|
+
node.queue_free()
|
|
528
|
+
|
|
529
|
+
var err: Dictionary = _save_scene(root, scene_path)
|
|
530
|
+
if not err.is_empty():
|
|
531
|
+
return err
|
|
532
|
+
|
|
533
|
+
return {"ok": true, "deletedNodePath": node_path}
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
func duplicate_node(args: Dictionary) -> Dictionary:
|
|
537
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
538
|
+
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
539
|
+
var node_path: String = str(args.get("nodePath", ""))
|
|
540
|
+
var new_name: String = str(args.get("newName", ""))
|
|
541
|
+
var parent_path: String = str(args.get("parentPath", ""))
|
|
542
|
+
|
|
543
|
+
if node_path.is_empty() or new_name.is_empty():
|
|
544
|
+
return {"ok": false, "error": "Missing nodePath or newName"}
|
|
545
|
+
|
|
546
|
+
var loaded: Array = _load_scene(scene_path)
|
|
547
|
+
var refused: Dictionary = loaded[1]
|
|
548
|
+
if not refused.is_empty():
|
|
549
|
+
return refused
|
|
550
|
+
|
|
551
|
+
var root: Node = loaded[0]
|
|
552
|
+
var source: Node = _find_node(root, node_path)
|
|
553
|
+
if not source:
|
|
554
|
+
root.queue_free()
|
|
555
|
+
return {"ok": false, "error": "Node not found: " + node_path}
|
|
556
|
+
|
|
557
|
+
var target_parent: Node = source.get_parent()
|
|
558
|
+
if not parent_path.is_empty():
|
|
559
|
+
target_parent = _find_node(root, parent_path)
|
|
560
|
+
if not target_parent:
|
|
561
|
+
root.queue_free()
|
|
562
|
+
return {"ok": false, "error": "Parent not found: " + parent_path}
|
|
563
|
+
|
|
564
|
+
var duplicated_node: Node = source.duplicate()
|
|
565
|
+
if not duplicated_node:
|
|
566
|
+
root.queue_free()
|
|
567
|
+
return {"ok": false, "error": "Failed to duplicate node: " + node_path}
|
|
568
|
+
|
|
569
|
+
duplicated_node.name = new_name
|
|
570
|
+
target_parent.add_child(duplicated_node)
|
|
571
|
+
_set_owner_recursive(duplicated_node, root)
|
|
572
|
+
|
|
573
|
+
var err: Dictionary = _save_scene(root, scene_path)
|
|
574
|
+
if not err.is_empty():
|
|
575
|
+
return err
|
|
576
|
+
|
|
577
|
+
return {"ok": true, "nodePath": node_path, "newName": new_name}
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
func reparent_node(args: Dictionary) -> Dictionary:
|
|
581
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
582
|
+
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
583
|
+
var node_path: String = str(args.get("nodePath", ""))
|
|
584
|
+
var new_parent_path: String = str(args.get("newParentPath", ""))
|
|
585
|
+
|
|
586
|
+
if node_path.is_empty() or node_path == ".":
|
|
587
|
+
return {"ok": false, "error": "Cannot reparent root node"}
|
|
588
|
+
if new_parent_path.is_empty():
|
|
589
|
+
return {"ok": false, "error": "Missing newParentPath"}
|
|
590
|
+
|
|
591
|
+
var loaded: Array = _load_scene(scene_path)
|
|
592
|
+
var refused: Dictionary = loaded[1]
|
|
593
|
+
if not refused.is_empty():
|
|
594
|
+
return refused
|
|
595
|
+
|
|
596
|
+
var root: Node = loaded[0]
|
|
597
|
+
var node: Node = _find_node(root, node_path)
|
|
598
|
+
var new_parent: Node = _find_node(root, new_parent_path)
|
|
599
|
+
if not node:
|
|
600
|
+
root.queue_free()
|
|
601
|
+
return {"ok": false, "error": "Node not found: " + node_path}
|
|
602
|
+
if not new_parent:
|
|
603
|
+
root.queue_free()
|
|
604
|
+
return {"ok": false, "error": "New parent not found: " + new_parent_path}
|
|
605
|
+
|
|
606
|
+
var old_parent: Node = node.get_parent()
|
|
607
|
+
if not old_parent:
|
|
608
|
+
root.queue_free()
|
|
609
|
+
return {"ok": false, "error": "Cannot reparent root node"}
|
|
610
|
+
|
|
611
|
+
old_parent.remove_child(node)
|
|
612
|
+
new_parent.add_child(node)
|
|
613
|
+
_set_owner_recursive(node, root)
|
|
614
|
+
|
|
615
|
+
var err: Dictionary = _save_scene(root, scene_path)
|
|
616
|
+
if not err.is_empty():
|
|
617
|
+
return err
|
|
618
|
+
|
|
619
|
+
return {"ok": true, "nodePath": node_path, "newParentPath": new_parent_path}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
func set_node_properties(args: Dictionary) -> Dictionary:
|
|
623
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
624
|
+
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
625
|
+
var node_path: String = str(args.get("nodePath", "."))
|
|
626
|
+
var properties: Dictionary = _parse_properties_arg(args.get("properties", {}))
|
|
627
|
+
|
|
628
|
+
var loaded: Array = _load_scene(scene_path)
|
|
629
|
+
var refused: Dictionary = loaded[1]
|
|
630
|
+
if not refused.is_empty():
|
|
631
|
+
return refused
|
|
632
|
+
|
|
633
|
+
var root: Node = loaded[0]
|
|
634
|
+
var node: Node = _find_node(root, node_path)
|
|
635
|
+
if not node:
|
|
636
|
+
root.queue_free()
|
|
637
|
+
return {"ok": false, "error": "Node not found: " + node_path}
|
|
638
|
+
|
|
639
|
+
var refused_property: String = _set_node_properties(node, properties)
|
|
640
|
+
if not refused_property.is_empty():
|
|
641
|
+
root.queue_free()
|
|
642
|
+
return {"ok": false, "error": refused_property}
|
|
643
|
+
|
|
644
|
+
var err: Dictionary = _save_scene(root, scene_path)
|
|
645
|
+
if not err.is_empty():
|
|
646
|
+
return err
|
|
647
|
+
|
|
648
|
+
return {"ok": true, "nodePath": node_path}
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
func get_node_properties(args: Dictionary) -> Dictionary:
|
|
652
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
653
|
+
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
654
|
+
var node_path: String = str(args.get("nodePath", "."))
|
|
655
|
+
var include_defaults: bool = bool(args.get("includeDefaults", false))
|
|
656
|
+
|
|
657
|
+
var loaded: Array = _load_scene(scene_path)
|
|
658
|
+
var refused: Dictionary = loaded[1]
|
|
659
|
+
if not refused.is_empty():
|
|
660
|
+
return refused
|
|
661
|
+
|
|
662
|
+
var root: Node = loaded[0]
|
|
663
|
+
var node: Node = _find_node(root, node_path)
|
|
664
|
+
if not node:
|
|
665
|
+
root.queue_free()
|
|
666
|
+
return {"ok": false, "error": "Node not found: " + node_path}
|
|
667
|
+
|
|
668
|
+
var defaults: Node = null
|
|
669
|
+
if not include_defaults and ClassDB.class_exists(node.get_class()):
|
|
670
|
+
defaults = ClassDB.instantiate(node.get_class())
|
|
671
|
+
|
|
672
|
+
var props: Dictionary = {}
|
|
673
|
+
for p: Dictionary in node.get_property_list():
|
|
674
|
+
var usage: int = int(p.get("usage", 0))
|
|
675
|
+
if not (usage & PROPERTY_USAGE_STORAGE):
|
|
676
|
+
continue
|
|
677
|
+
var prop_name: String = str(p.get("name", ""))
|
|
678
|
+
if prop_name.is_empty():
|
|
679
|
+
continue
|
|
680
|
+
var current_val: Variant = node.get(prop_name)
|
|
681
|
+
if not include_defaults and defaults:
|
|
682
|
+
var default_val: Variant = defaults.get(prop_name)
|
|
683
|
+
if current_val == default_val:
|
|
684
|
+
continue
|
|
685
|
+
props[prop_name] = _serialize_value(current_val)
|
|
686
|
+
|
|
687
|
+
if defaults:
|
|
688
|
+
defaults.queue_free()
|
|
689
|
+
root.queue_free()
|
|
690
|
+
return {"ok": true, "nodePath": node_path, "properties": props}
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
func save_scene(args: Dictionary) -> Dictionary:
|
|
694
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
695
|
+
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
696
|
+
var new_path_raw: String = str(args.get("newPath", ""))
|
|
697
|
+
var target_path: String = scene_path
|
|
698
|
+
if not new_path_raw.is_empty():
|
|
699
|
+
target_path = _to_scene_res_path(project_path, new_path_raw)
|
|
700
|
+
|
|
701
|
+
var loaded: Array = _load_scene(scene_path)
|
|
702
|
+
var refused: Dictionary = loaded[1]
|
|
703
|
+
if not refused.is_empty():
|
|
704
|
+
return refused
|
|
705
|
+
|
|
706
|
+
_ensure_parent_dir_for_scene(target_path)
|
|
707
|
+
var root: Node = loaded[0]
|
|
708
|
+
var err: Dictionary = _save_scene(root, target_path)
|
|
709
|
+
if not err.is_empty():
|
|
710
|
+
return err
|
|
711
|
+
|
|
712
|
+
return {"ok": true, "scenePath": scene_path, "savedPath": target_path}
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
func connect_signal(args: Dictionary) -> Dictionary:
|
|
716
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
717
|
+
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
718
|
+
var source_node_path: String = str(args.get("sourceNodePath", ""))
|
|
719
|
+
var signal_name: String = str(args.get("signalName", ""))
|
|
720
|
+
var target_node_path: String = str(args.get("targetNodePath", ""))
|
|
721
|
+
var method_name: String = str(args.get("methodName", ""))
|
|
722
|
+
# A connection without CONNECT_PERSIST is a runtime one, and PackedScene.pack drops those on
|
|
723
|
+
# the way out: without this the scene saves unchanged and this answers success over nothing.
|
|
724
|
+
var flags: int = int(args.get("flags", 0)) | Object.CONNECT_PERSIST
|
|
725
|
+
|
|
726
|
+
if (
|
|
727
|
+
source_node_path.is_empty()
|
|
728
|
+
or signal_name.is_empty()
|
|
729
|
+
or target_node_path.is_empty()
|
|
730
|
+
or method_name.is_empty()
|
|
731
|
+
):
|
|
732
|
+
return {"ok": false, "error": "Missing required signal connection arguments"}
|
|
733
|
+
|
|
734
|
+
var loaded: Array = _load_scene(scene_path)
|
|
735
|
+
var refused: Dictionary = loaded[1]
|
|
736
|
+
if not refused.is_empty():
|
|
737
|
+
return refused
|
|
738
|
+
|
|
739
|
+
var root: Node = loaded[0]
|
|
740
|
+
var source: Node = _find_node(root, source_node_path)
|
|
741
|
+
var target: Node = _find_node(root, target_node_path)
|
|
742
|
+
if not source:
|
|
743
|
+
root.queue_free()
|
|
744
|
+
return {"ok": false, "error": "Source node not found: " + source_node_path}
|
|
745
|
+
if not target:
|
|
746
|
+
root.queue_free()
|
|
747
|
+
return {"ok": false, "error": "Target node not found: " + target_node_path}
|
|
748
|
+
if not source.has_signal(signal_name):
|
|
749
|
+
root.queue_free()
|
|
750
|
+
return {"ok": false, "error": "Signal not found on source: " + signal_name}
|
|
751
|
+
|
|
752
|
+
var callable: Callable = Callable(target, method_name)
|
|
753
|
+
if not source.is_connected(signal_name, callable):
|
|
754
|
+
var connect_result: Error = source.connect(signal_name, callable, flags)
|
|
755
|
+
if connect_result != OK:
|
|
756
|
+
root.queue_free()
|
|
757
|
+
return {"ok": false, "error": "Failed to connect signal: " + error_string(connect_result)}
|
|
758
|
+
|
|
759
|
+
var err: Dictionary = _save_scene(root, scene_path)
|
|
760
|
+
if not err.is_empty():
|
|
761
|
+
return err
|
|
762
|
+
|
|
763
|
+
return {
|
|
764
|
+
"ok": true,
|
|
765
|
+
"sourceNodePath": source_node_path,
|
|
766
|
+
"signalName": signal_name,
|
|
767
|
+
"targetNodePath": target_node_path,
|
|
768
|
+
"methodName": method_name,
|
|
769
|
+
"flags": flags
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
func disconnect_signal(args: Dictionary) -> Dictionary:
|
|
774
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
775
|
+
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
776
|
+
var source_node_path: String = str(args.get("sourceNodePath", ""))
|
|
777
|
+
var signal_name: String = str(args.get("signalName", ""))
|
|
778
|
+
var target_node_path: String = str(args.get("targetNodePath", ""))
|
|
779
|
+
var method_name: String = str(args.get("methodName", ""))
|
|
780
|
+
|
|
781
|
+
if (
|
|
782
|
+
source_node_path.is_empty()
|
|
783
|
+
or signal_name.is_empty()
|
|
784
|
+
or target_node_path.is_empty()
|
|
785
|
+
or method_name.is_empty()
|
|
786
|
+
):
|
|
787
|
+
return {"ok": false, "error": "Missing required signal disconnection arguments"}
|
|
788
|
+
|
|
789
|
+
var loaded: Array = _load_scene(scene_path)
|
|
790
|
+
var refused: Dictionary = loaded[1]
|
|
791
|
+
if not refused.is_empty():
|
|
792
|
+
return refused
|
|
793
|
+
|
|
794
|
+
var root: Node = loaded[0]
|
|
795
|
+
var source: Node = _find_node(root, source_node_path)
|
|
796
|
+
var target: Node = _find_node(root, target_node_path)
|
|
797
|
+
if not source:
|
|
798
|
+
root.queue_free()
|
|
799
|
+
return {"ok": false, "error": "Source node not found: " + source_node_path}
|
|
800
|
+
if not target:
|
|
801
|
+
root.queue_free()
|
|
802
|
+
return {"ok": false, "error": "Target node not found: " + target_node_path}
|
|
803
|
+
|
|
804
|
+
var callable: Callable = Callable(target, method_name)
|
|
805
|
+
if source.is_connected(signal_name, callable):
|
|
806
|
+
source.disconnect(signal_name, callable)
|
|
807
|
+
|
|
808
|
+
var err: Dictionary = _save_scene(root, scene_path)
|
|
809
|
+
if not err.is_empty():
|
|
810
|
+
return err
|
|
811
|
+
|
|
812
|
+
return {
|
|
813
|
+
"ok": true,
|
|
814
|
+
"sourceNodePath": source_node_path,
|
|
815
|
+
"signalName": signal_name,
|
|
816
|
+
"targetNodePath": target_node_path,
|
|
817
|
+
"methodName": method_name
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
func list_connections(args: Dictionary) -> Dictionary:
|
|
822
|
+
var project_path: String = str(args.get("projectPath", ""))
|
|
823
|
+
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
824
|
+
var filter_path: String = str(args.get("nodePath", ""))
|
|
825
|
+
|
|
826
|
+
var loaded: Array = _load_scene(scene_path)
|
|
827
|
+
var refused: Dictionary = loaded[1]
|
|
828
|
+
if not refused.is_empty():
|
|
829
|
+
return refused
|
|
830
|
+
|
|
831
|
+
var root: Node = loaded[0]
|
|
832
|
+
var nodes: Array = []
|
|
833
|
+
_collect_nodes_recursive(root, ".", nodes)
|
|
834
|
+
|
|
835
|
+
var connections: Array = []
|
|
836
|
+
for entry: Dictionary in nodes:
|
|
837
|
+
var path: String = str(entry["path"])
|
|
838
|
+
if not filter_path.is_empty() and filter_path != path:
|
|
839
|
+
continue
|
|
840
|
+
var node: Node = entry["node"]
|
|
841
|
+
for signal_info: Dictionary in node.get_signal_list():
|
|
842
|
+
var signal_name: String = str(signal_info.get("name", ""))
|
|
843
|
+
if signal_name.is_empty():
|
|
844
|
+
continue
|
|
845
|
+
for conn: Dictionary in node.get_signal_connection_list(signal_name):
|
|
846
|
+
var callable: Callable = conn.get("callable", Callable())
|
|
847
|
+
var target_obj: Object = callable.get_object()
|
|
848
|
+
var target_path: String = ""
|
|
849
|
+
if target_obj is Node:
|
|
850
|
+
target_path = str(root.get_path_to(target_obj as Node))
|
|
851
|
+
connections.append(
|
|
852
|
+
{
|
|
853
|
+
"sourceNodePath": path,
|
|
854
|
+
"signalName": signal_name,
|
|
855
|
+
"targetNodePath": target_path,
|
|
856
|
+
"methodName": str(callable.get_method()),
|
|
857
|
+
"flags": int(conn.get("flags", 0))
|
|
858
|
+
}
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
root.queue_free()
|
|
862
|
+
return {"ok": true, "connections": connections}
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
## Rescan the project filesystem, and report whether a scan is still running.
|
|
866
|
+
##
|
|
867
|
+
## The editor rescans when its window regains focus, so a script written by anything other
|
|
868
|
+
## than the editor stays invisible until someone clicks on Godot. Until then its
|
|
869
|
+
## `class_name` is missing from the global class list and the language server reports every
|
|
870
|
+
## use of it as an unknown type, which is godotengine/godot#42786.
|
|
871
|
+
##
|
|
872
|
+
## Returns as soon as the scan is queued rather than awaiting it, because the tool executor
|
|
873
|
+
## takes a Dictionary and not a coroutine. Pass `statusOnly` to poll without starting
|
|
874
|
+
## another scan.
|
|
875
|
+
func rescan_filesystem(args: Dictionary) -> Dictionary:
|
|
876
|
+
if not _editor_plugin:
|
|
877
|
+
return {"ok": false, "error": "Editor plugin unavailable"}
|
|
878
|
+
|
|
879
|
+
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
880
|
+
if not bool(args.get("statusOnly", false)):
|
|
881
|
+
filesystem.scan()
|
|
882
|
+
|
|
883
|
+
# Importing is reported separately from scanning, and a class is not registered until
|
|
884
|
+
# both are done, so a caller watching only one of them can look too early.
|
|
885
|
+
return {"ok": true, "scanning": filesystem.is_scanning(), "importing": filesystem.is_importing()}
|