gdharness 1.1.21 → 1.1.23
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/build/cli.js +1470 -726
- package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
- package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
- package/build/godot/addons/gdharness_editor/reading.gd +17 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
- package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
- package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +15 -5
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +89 -16
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +107 -343
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +61 -28
- package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
- package/build/godot/operations/audio_buses.gd +47 -11
- package/build/godot/operations/class_cache.gd +18 -2
- package/build/godot/operations/classdb_queries.gd +28 -17
- package/build/godot/operations/dependencies.gd +128 -17
- package/build/godot/operations/file_walk.gd +20 -0
- package/build/godot/operations/gdscript_analysis.gd +59 -20
- package/build/godot/operations/gdscript_authoring.gd +127 -60
- package/build/godot/operations/godot_operations.gd +24 -5
- package/build/godot/operations/import_pipeline.gd +179 -19
- package/build/godot/operations/logger.gd +2 -2
- package/build/godot/operations/plugins.gd +88 -70
- package/build/godot/operations/project_config.gd +41 -6
- package/build/godot/operations/project_diagnostics.gd +27 -11
- package/build/godot/operations/reading.gd +17 -0
- package/build/godot/operations/resource_files.gd +60 -17
- package/build/godot/operations/serialisation.gd +116 -7
- package/build/index.js +1385 -553
- package/build/keeper.js +4 -0
- package/package.json +1 -1
|
@@ -5,60 +5,16 @@ extends Node
|
|
|
5
5
|
|
|
6
6
|
const Read = preload("../reading.gd")
|
|
7
7
|
const PropertyValues = preload("../property_values.gd")
|
|
8
|
+
const SceneFile = preload("../scene_file.gd")
|
|
8
9
|
const Serialisation = preload("../serialisation.gd")
|
|
9
10
|
|
|
10
11
|
var _editor_plugin: EditorPlugin = null
|
|
11
12
|
var _values: Serialisation = Serialisation.new()
|
|
12
13
|
var _properties: PropertyValues = PropertyValues.new()
|
|
13
14
|
|
|
14
|
-
## How many scans the editor has completed, counted off `sources_changed`, which it emits at the end
|
|
15
|
-
## of every scan once the reimport the scan found is done: the one moment a scan is over. Neither
|
|
16
|
-
## flag it offers says so. A threaded scan stops reporting itself when its thread finishes, and its
|
|
17
|
-
## import runs a frame or more later when the thread is joined; an editor in the background took
|
|
18
|
-
## long enough over that frame that a rescan answered before the import of 211 changed scenes began.
|
|
19
|
-
var _scans_completed: int = 0
|
|
20
|
-
|
|
21
|
-
## When the last scan completed, on the engine's clock, or -1 before the first. The editor writes
|
|
22
|
-
## the class cache a frame after it stops reporting a scan, measured on 4.7.2, so a game started
|
|
23
|
-
## in between reads a file being rewritten; how long ago the scan ended is what tells a caller
|
|
24
|
-
## whether that write can still be coming.
|
|
25
|
-
var _scan_finished_at_msec: int = -1
|
|
26
|
-
|
|
27
|
-
## How many reimports asked for through [method reimport_files] have finished. Counted here rather
|
|
28
|
-
## than off `resources_reimported`, which a scan's own import emits as well.
|
|
29
|
-
var _reimports_completed: int = 0
|
|
30
|
-
|
|
31
15
|
|
|
32
16
|
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
33
17
|
_editor_plugin = plugin
|
|
34
|
-
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
35
|
-
if not filesystem.sources_changed.is_connected(_scan_completed):
|
|
36
|
-
var watched: int = filesystem.sources_changed.connect(_scan_completed)
|
|
37
|
-
if watched != OK:
|
|
38
|
-
push_error("gdharness could not watch for completed scans: %d" % watched)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
func _scan_completed(_sources_exist: bool) -> void:
|
|
42
|
-
_scans_completed += 1
|
|
43
|
-
_scan_finished_at_msec = Time.get_ticks_msec()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
func _refresh_and_reload(scene_path: String) -> void:
|
|
47
|
-
_refresh_filesystem()
|
|
48
|
-
_reload_scene_in_editor(scene_path)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
func _refresh_filesystem() -> void:
|
|
52
|
-
if _editor_plugin:
|
|
53
|
-
EditorInterface.get_resource_filesystem().scan()
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
func _reload_scene_in_editor(scene_path: String) -> void:
|
|
57
|
-
if not _editor_plugin:
|
|
58
|
-
return
|
|
59
|
-
var edited: Node = EditorInterface.get_edited_scene_root()
|
|
60
|
-
if edited and edited.scene_file_path == scene_path:
|
|
61
|
-
EditorInterface.reload_scene_from_path(scene_path)
|
|
62
18
|
|
|
63
19
|
|
|
64
20
|
func _ensure_res_path(path: String) -> String:
|
|
@@ -84,29 +40,10 @@ func _to_scene_res_path(project_path: String, scene_path: String) -> String:
|
|
|
84
40
|
return _ensure_res_path(p)
|
|
85
41
|
|
|
86
42
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if not packed:
|
|
92
|
-
return [null, {"ok": false, "error": "Failed to load: " + scene_path}]
|
|
93
|
-
var root: Node = packed.instantiate()
|
|
94
|
-
if not root:
|
|
95
|
-
return [null, {"ok": false, "error": "Failed to instantiate: " + scene_path}]
|
|
96
|
-
return [root, {}]
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
func _save_scene(scene_root: Node, scene_path: String) -> Dictionary:
|
|
100
|
-
var packed: PackedScene = PackedScene.new()
|
|
101
|
-
if packed.pack(scene_root) != OK:
|
|
102
|
-
scene_root.queue_free()
|
|
103
|
-
return {"ok": false, "error": "Failed to pack scene"}
|
|
104
|
-
if ResourceSaver.save(packed, scene_path) != OK:
|
|
105
|
-
scene_root.queue_free()
|
|
106
|
-
return {"ok": false, "error": "Failed to save scene"}
|
|
107
|
-
scene_root.queue_free()
|
|
108
|
-
_refresh_and_reload(scene_path)
|
|
109
|
-
return {}
|
|
43
|
+
## [param scene_path] opened for a tool, as [scene, refusal]: the refusal is {} when it opened.
|
|
44
|
+
func _open(scene_path: String, for_writing: bool) -> Array:
|
|
45
|
+
var scene: SceneFile = SceneFile.new()
|
|
46
|
+
return [scene, scene.open(scene_path, for_writing)]
|
|
110
47
|
|
|
111
48
|
|
|
112
49
|
func _find_node(root: Node, path: String) -> Node:
|
|
@@ -115,9 +52,156 @@ func _find_node(root: Node, path: String) -> Node:
|
|
|
115
52
|
return root.get_node_or_null(path)
|
|
116
53
|
|
|
117
54
|
|
|
118
|
-
##
|
|
119
|
-
|
|
120
|
-
|
|
55
|
+
## Why the property or signal [param missing] could not be found on [param node], adding the one
|
|
56
|
+
## cause the name alone does not show: a script that does not compile leaves the editor holding a
|
|
57
|
+
## placeholder with no properties or signals at all, and the refusal named the property instead.
|
|
58
|
+
func _missing_on(node: Node, missing: String) -> String:
|
|
59
|
+
var script: Script = node.get_script()
|
|
60
|
+
if script == null or not script.has_source_code():
|
|
61
|
+
return missing
|
|
62
|
+
var probe: GDScript = GDScript.new()
|
|
63
|
+
probe.source_code = script.source_code
|
|
64
|
+
if probe.reload() == OK:
|
|
65
|
+
return missing
|
|
66
|
+
return (
|
|
67
|
+
"%s. Its script, %s, does not compile, so the editor knows none of what it declares."
|
|
68
|
+
% [missing, script.resource_path]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## Whether each of [param names] reads the same off [param saved] as off [param node], answering
|
|
73
|
+
## the first that does not, or "".
|
|
74
|
+
func _kept(scene: SceneFile, saved: Node, node: Node, names: Array) -> String:
|
|
75
|
+
var at: String = scene.path_of(node)
|
|
76
|
+
var copy: Node = saved.get_node_or_null(at)
|
|
77
|
+
if copy == null:
|
|
78
|
+
return "%s is not in the scene as it would be saved." % at
|
|
79
|
+
for key: Variant in names:
|
|
80
|
+
var property: String = str(key)
|
|
81
|
+
var loaded: Variant = copy.get(property)
|
|
82
|
+
if not scene.kept(saved, loaded, node.get(property)):
|
|
83
|
+
return (
|
|
84
|
+
(
|
|
85
|
+
"%s.%s would load from the file as %s: the scene does not keep it, because the property "
|
|
86
|
+
+ "is not one a scene stores or the node belongs to another scene."
|
|
87
|
+
)
|
|
88
|
+
% [at, property, JSON.stringify(_values.serialize_value(loaded))]
|
|
89
|
+
)
|
|
90
|
+
return ""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
## Owns [param node] and the nodes under it to [param root], except those an instanced scene
|
|
94
|
+
## inside it owns: giving those to the root made the pack write the instance's own nodes into this
|
|
95
|
+
## scene as overrides.
|
|
96
|
+
func _copy(source: Node, root: Node, copies: Dictionary) -> Node:
|
|
97
|
+
var copy: Node = null
|
|
98
|
+
if not source.scene_file_path.is_empty():
|
|
99
|
+
# An instanced scene made again from its file, so the copy is an instance too. Node.duplicate
|
|
100
|
+
# makes a plain node of the instance's class, and the pack saved it with every property of the
|
|
101
|
+
# instance's root frozen into this scene.
|
|
102
|
+
var packed: PackedScene = ResourceLoader.load(source.scene_file_path, "PackedScene") as PackedScene
|
|
103
|
+
if packed == null:
|
|
104
|
+
return null
|
|
105
|
+
copy = packed.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)
|
|
106
|
+
for declared: Dictionary in source.get_property_list():
|
|
107
|
+
if not (Read.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
108
|
+
continue
|
|
109
|
+
var property: String = str(declared.get("name", ""))
|
|
110
|
+
var value: Variant = source.get(property)
|
|
111
|
+
if not PropertyValues.same(copy.get(property), value):
|
|
112
|
+
copy.set(property, value)
|
|
113
|
+
for group: StringName in source.get_groups():
|
|
114
|
+
if not copy.is_in_group(group):
|
|
115
|
+
copy.add_to_group(group, true)
|
|
116
|
+
else:
|
|
117
|
+
copy = source.duplicate(Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS)
|
|
118
|
+
for child: Node in copy.get_children():
|
|
119
|
+
copy.remove_child(child)
|
|
120
|
+
child.free()
|
|
121
|
+
copy.name = source.name
|
|
122
|
+
copies[source] = copy
|
|
123
|
+
# Only what this scene owns: an instance's own nodes come with the instance.
|
|
124
|
+
for child: Node in source.get_children():
|
|
125
|
+
if child.owner == root:
|
|
126
|
+
var copied: Node = _copy(child, root, copies)
|
|
127
|
+
if copied == null:
|
|
128
|
+
return null
|
|
129
|
+
copy.add_child(copied)
|
|
130
|
+
return copy
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
## Gives each copy in [param copies] the persistent connections its source has, to the copy of the
|
|
134
|
+
## target when the target was copied too. A connection an instance makes inside itself is skipped:
|
|
135
|
+
## the instance made again from its file already has it.
|
|
136
|
+
func _reconnect(copies: Dictionary, top: Node) -> void:
|
|
137
|
+
for key: Variant in copies:
|
|
138
|
+
var source: Node = key
|
|
139
|
+
var copy: Node = copies[key]
|
|
140
|
+
for info: Dictionary in source.get_signal_list():
|
|
141
|
+
var signal_name: String = str(info.get("name", ""))
|
|
142
|
+
for conn: Dictionary in source.get_signal_connection_list(signal_name):
|
|
143
|
+
var flags: int = Read.as_int(conn.get("flags", 0))
|
|
144
|
+
var callable: Callable = conn.get("callable", Callable())
|
|
145
|
+
var target: Object = callable.get_object()
|
|
146
|
+
if not flags & Object.CONNECT_PERSIST or target == null:
|
|
147
|
+
continue
|
|
148
|
+
var to: Object = copies.get(target, target)
|
|
149
|
+
if to == target and target is Node and (target == top or top.is_ancestor_of(target as Node)):
|
|
150
|
+
continue
|
|
151
|
+
var copied: Callable = Callable(to, callable.get_method())
|
|
152
|
+
if (
|
|
153
|
+
not copy.is_connected(signal_name, copied)
|
|
154
|
+
and copy.connect(signal_name, copied, flags) != OK
|
|
155
|
+
):
|
|
156
|
+
push_error(
|
|
157
|
+
"gdharness: could not copy the connection %s of %s" % [signal_name, source.name]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
func _own(node: Node, root: Node) -> void:
|
|
162
|
+
node.owner = root
|
|
163
|
+
for child: Node in node.get_children():
|
|
164
|
+
if child.owner == null or child.owner == root:
|
|
165
|
+
_own(child, root)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
## [param node]'s stored properties as a read answers them. A property holding a node is answered
|
|
169
|
+
## as the path to it: the copy read here is never in the tree, and an object description named a
|
|
170
|
+
## node that was freed when the call ended.
|
|
171
|
+
func _properties_of(node: Node, include_defaults: bool, set_here: Dictionary) -> Dictionary:
|
|
172
|
+
var script: Script = node.get_script()
|
|
173
|
+
var scripted: Dictionary = {}
|
|
174
|
+
if script != null:
|
|
175
|
+
for declared: Dictionary in script.get_script_property_list():
|
|
176
|
+
scripted[str(declared.get("name", ""))] = true
|
|
177
|
+
var props: Dictionary = {}
|
|
178
|
+
for p: Dictionary in node.get_property_list():
|
|
179
|
+
if not (Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
180
|
+
continue
|
|
181
|
+
var property: String = str(p.get("name", ""))
|
|
182
|
+
if property.is_empty():
|
|
183
|
+
continue
|
|
184
|
+
var value: Variant = node.get(property)
|
|
185
|
+
if not include_defaults and not set_here.has(property):
|
|
186
|
+
var default: Variant = (
|
|
187
|
+
script.get_property_default_value(property)
|
|
188
|
+
if scripted.has(property)
|
|
189
|
+
else ClassDB.class_get_property_default_value(node.get_class(), property)
|
|
190
|
+
)
|
|
191
|
+
if PropertyValues.same(value, default):
|
|
192
|
+
continue
|
|
193
|
+
props[property] = _readable(node, value)
|
|
194
|
+
return props
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
func _readable(node: Node, value: Variant) -> Variant:
|
|
198
|
+
if value is Node:
|
|
199
|
+
var target: Node = value
|
|
200
|
+
return _values.serialize_value(node.get_path_to(target))
|
|
201
|
+
if value is Array:
|
|
202
|
+
var items: Array = value
|
|
203
|
+
return items.map(func(item: Variant) -> Variant: return _readable(node, item))
|
|
204
|
+
return _values.serialize_value(value)
|
|
121
205
|
|
|
122
206
|
|
|
123
207
|
func _parse_properties_arg(raw_properties: Variant) -> Dictionary:
|
|
@@ -133,20 +217,6 @@ func _parse_properties_arg(raw_properties: Variant) -> Dictionary:
|
|
|
133
217
|
return {}
|
|
134
218
|
|
|
135
219
|
|
|
136
|
-
func _ensure_parent_dir_for_scene(scene_path: String) -> void:
|
|
137
|
-
var base_dir: String = scene_path.get_base_dir()
|
|
138
|
-
if not DirAccess.dir_exists_absolute(base_dir):
|
|
139
|
-
var made: Error = DirAccess.make_dir_recursive_absolute(base_dir)
|
|
140
|
-
if made != OK:
|
|
141
|
-
push_error("gdharness: could not create " + base_dir + ": " + error_string(made))
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
func _set_owner_recursive(node: Node, scene_owner: Node) -> void:
|
|
145
|
-
node.owner = scene_owner
|
|
146
|
-
for child: Node in node.get_children():
|
|
147
|
-
_set_owner_recursive(child, scene_owner)
|
|
148
|
-
|
|
149
|
-
|
|
150
220
|
func _build_node_tree(
|
|
151
221
|
node: Node, include_properties: bool, depth: int, current_depth: int, node_path: String
|
|
152
222
|
) -> Dictionary:
|
|
@@ -156,15 +226,7 @@ func _build_node_tree(
|
|
|
156
226
|
}
|
|
157
227
|
|
|
158
228
|
if include_properties:
|
|
159
|
-
|
|
160
|
-
for p: Dictionary in node.get_property_list():
|
|
161
|
-
if not (Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
162
|
-
continue
|
|
163
|
-
var pn: String = str(p.get("name", ""))
|
|
164
|
-
if pn.is_empty():
|
|
165
|
-
continue
|
|
166
|
-
props[pn] = _values.serialize_value(node.get(pn))
|
|
167
|
-
data["properties"] = props
|
|
229
|
+
data["properties"] = _properties_of(node, true, {})
|
|
168
230
|
|
|
169
231
|
if depth >= 0 and current_depth >= depth:
|
|
170
232
|
return data
|
|
@@ -193,27 +255,53 @@ func create_scene(args: Dictionary) -> Dictionary:
|
|
|
193
255
|
return {"ok": false, "error": "Missing scenePath"}
|
|
194
256
|
if not scene_path.ends_with(".tscn"):
|
|
195
257
|
scene_path += ".tscn"
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
258
|
+
# A class that is not a node went into a Node variable, which is a runtime error the caller
|
|
259
|
+
# heard about only as "Invalid tool result".
|
|
260
|
+
if not ClassDB.class_exists(root_node_type) or not ClassDB.is_parent_class(root_node_type, "Node"):
|
|
261
|
+
return {
|
|
262
|
+
"ok": false, "error": "rootNodeType must be a node class, and %s is not one." % root_node_type
|
|
263
|
+
}
|
|
264
|
+
if not ClassDB.can_instantiate(root_node_type):
|
|
265
|
+
return {"ok": false, "error": "%s cannot be instantiated." % root_node_type}
|
|
266
|
+
# Saving over a scene that is there replaced it, nodes and all, and answered as a new scene.
|
|
267
|
+
if FileAccess.file_exists(scene_path):
|
|
268
|
+
return {
|
|
269
|
+
"ok": false,
|
|
270
|
+
"error":
|
|
271
|
+
(
|
|
272
|
+
(
|
|
273
|
+
"%s already exists, and scene_create op=create would replace everything in it. Edit it "
|
|
274
|
+
+ "with scene_node, or create the scene under another path."
|
|
275
|
+
)
|
|
276
|
+
% scene_path
|
|
277
|
+
)
|
|
278
|
+
}
|
|
200
279
|
|
|
201
280
|
var root: Node = ClassDB.instantiate(root_node_type)
|
|
202
|
-
if not root:
|
|
203
|
-
return {"ok": false, "error": "Failed to instantiate root node: " + root_node_type}
|
|
204
281
|
root.name = root_node_type
|
|
205
282
|
|
|
206
283
|
if not script_path.is_empty():
|
|
207
284
|
var full_script_path: String = _to_scene_res_path(project_path, script_path)
|
|
208
|
-
var script:
|
|
209
|
-
if
|
|
210
|
-
root.
|
|
211
|
-
return {"ok": false, "error": "
|
|
285
|
+
var script: Script = load(full_script_path) as Script
|
|
286
|
+
if script == null:
|
|
287
|
+
root.free()
|
|
288
|
+
return {"ok": false, "error": "No script loads from " + full_script_path}
|
|
289
|
+
var base: String = str(script.get_instance_base_type())
|
|
290
|
+
if not ClassDB.is_parent_class(root_node_type, base):
|
|
291
|
+
root.free()
|
|
292
|
+
return {
|
|
293
|
+
"ok": false,
|
|
294
|
+
"error":
|
|
295
|
+
"%s extends %s, so it cannot be on a %s root." % [full_script_path, base, root_node_type]
|
|
296
|
+
}
|
|
212
297
|
root.set_script(script)
|
|
213
298
|
|
|
214
|
-
var
|
|
215
|
-
|
|
216
|
-
|
|
299
|
+
var scene: SceneFile = SceneFile.new()
|
|
300
|
+
scene.root = root
|
|
301
|
+
scene.path = scene_path
|
|
302
|
+
var written: Dictionary = scene.write()
|
|
303
|
+
if not written.is_empty():
|
|
304
|
+
return written
|
|
217
305
|
|
|
218
306
|
return {"ok": true, "scenePath": scene_path, "rootNodeType": root_node_type}
|
|
219
307
|
|
|
@@ -224,15 +312,15 @@ func list_scene_nodes(args: Dictionary) -> Dictionary:
|
|
|
224
312
|
var depth: int = Read.as_int(args.get("depth", -1), -1)
|
|
225
313
|
var include_properties: bool = Read.as_bool(args.get("includeProperties", false))
|
|
226
314
|
|
|
227
|
-
var
|
|
228
|
-
var
|
|
315
|
+
var opened: Array = _open(scene_path, false)
|
|
316
|
+
var scene: SceneFile = opened[0]
|
|
317
|
+
var refused: Dictionary = opened[1]
|
|
229
318
|
if not refused.is_empty():
|
|
230
319
|
return refused
|
|
231
320
|
|
|
232
|
-
var
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return {"ok": true, "tree": tree}
|
|
321
|
+
var tree: Dictionary = _build_node_tree(scene.root, include_properties, depth, 0, ".")
|
|
322
|
+
scene.close()
|
|
323
|
+
return {"ok": true, "tree": tree, "unsavedInEditor": SceneFile.unsaved(scene_path)}
|
|
236
324
|
|
|
237
325
|
|
|
238
326
|
func add_node(args: Dictionary) -> Dictionary:
|
|
@@ -245,40 +333,45 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
245
333
|
|
|
246
334
|
if node_type.is_empty() or node_name.is_empty():
|
|
247
335
|
return {"ok": false, "error": "Missing nodeType or nodeName"}
|
|
248
|
-
if not ClassDB.class_exists(node_type):
|
|
249
|
-
return {"ok": false, "error": "
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
336
|
+
if not ClassDB.class_exists(node_type) or not ClassDB.is_parent_class(node_type, "Node"):
|
|
337
|
+
return {"ok": false, "error": "nodeType must be a node class, and %s is not one." % node_type}
|
|
338
|
+
if not ClassDB.can_instantiate(node_type):
|
|
339
|
+
return {"ok": false, "error": "%s cannot be instantiated." % node_type}
|
|
340
|
+
|
|
341
|
+
var opened: Array = _open(scene_path, true)
|
|
342
|
+
var scene: SceneFile = opened[0]
|
|
343
|
+
var refused: Dictionary = opened[1]
|
|
253
344
|
if not refused.is_empty():
|
|
254
345
|
return refused
|
|
255
346
|
|
|
256
|
-
var root: Node =
|
|
347
|
+
var root: Node = scene.root
|
|
257
348
|
var parent: Node = _find_node(root, parent_node_path)
|
|
258
349
|
if not parent:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
350
|
+
return scene.refuse("Parent node not found: " + parent_node_path)
|
|
351
|
+
var foreign: String = scene.foreign(parent)
|
|
352
|
+
if not foreign.is_empty():
|
|
353
|
+
return scene.refuse(foreign)
|
|
354
|
+
var unusable: String = scene.unusable_name(parent, node_name)
|
|
355
|
+
if not unusable.is_empty():
|
|
356
|
+
return scene.refuse(unusable)
|
|
357
|
+
|
|
358
|
+
# In the tree before its properties, so a property holding a node can find it by path.
|
|
262
359
|
var new_node: Node = ClassDB.instantiate(node_type)
|
|
263
|
-
if not new_node:
|
|
264
|
-
root.queue_free()
|
|
265
|
-
return {"ok": false, "error": "Failed to instantiate nodeType: " + node_type}
|
|
266
|
-
|
|
267
360
|
new_node.name = node_name
|
|
268
|
-
var refused_property: String = _set_node_properties(new_node, properties)
|
|
269
|
-
if not refused_property.is_empty():
|
|
270
|
-
new_node.queue_free()
|
|
271
|
-
root.queue_free()
|
|
272
|
-
return {"ok": false, "error": refused_property}
|
|
273
|
-
|
|
274
361
|
parent.add_child(new_node)
|
|
275
|
-
|
|
362
|
+
_own(new_node, root)
|
|
363
|
+
var refused_property: String = _properties.write_all(new_node, properties)
|
|
364
|
+
if not refused_property.is_empty():
|
|
365
|
+
return scene.refuse(refused_property)
|
|
276
366
|
|
|
277
|
-
var
|
|
278
|
-
|
|
279
|
-
return
|
|
367
|
+
var new_path: String = scene.path_of(new_node)
|
|
368
|
+
var written: Dictionary = scene.write(
|
|
369
|
+
func(saved: Node) -> String: return _kept(scene, saved, new_node, properties.keys())
|
|
370
|
+
)
|
|
371
|
+
if not written.is_empty():
|
|
372
|
+
return written
|
|
280
373
|
|
|
281
|
-
return {"ok": true, "nodeName": node_name, "nodeType": node_type}
|
|
374
|
+
return {"ok": true, "nodeName": node_name, "nodeType": node_type, "nodePath": new_path}
|
|
282
375
|
|
|
283
376
|
|
|
284
377
|
func delete_node(args: Dictionary) -> Dictionary:
|
|
@@ -289,30 +382,33 @@ func delete_node(args: Dictionary) -> Dictionary:
|
|
|
289
382
|
if node_path.is_empty() or node_path == ".":
|
|
290
383
|
return {"ok": false, "error": "Cannot delete root node"}
|
|
291
384
|
|
|
292
|
-
var
|
|
293
|
-
var
|
|
385
|
+
var opened: Array = _open(scene_path, true)
|
|
386
|
+
var scene: SceneFile = opened[0]
|
|
387
|
+
var refused: Dictionary = opened[1]
|
|
294
388
|
if not refused.is_empty():
|
|
295
389
|
return refused
|
|
296
390
|
|
|
297
|
-
var
|
|
298
|
-
var node: Node = _find_node(root, node_path)
|
|
391
|
+
var node: Node = _find_node(scene.root, node_path)
|
|
299
392
|
if not node:
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if not parent:
|
|
305
|
-
root.queue_free()
|
|
306
|
-
return {"ok": false, "error": "Cannot delete root node"}
|
|
393
|
+
return scene.refuse("Node not found: " + node_path)
|
|
394
|
+
var fixed: String = scene.fixed(node)
|
|
395
|
+
if not fixed.is_empty():
|
|
396
|
+
return scene.refuse(fixed)
|
|
307
397
|
|
|
308
|
-
|
|
309
|
-
node.
|
|
398
|
+
var at: String = scene.path_of(node)
|
|
399
|
+
node.get_parent().remove_child(node)
|
|
400
|
+
node.free()
|
|
310
401
|
|
|
311
|
-
var
|
|
312
|
-
|
|
313
|
-
|
|
402
|
+
var written: Dictionary = scene.write(
|
|
403
|
+
func(saved: Node) -> String:
|
|
404
|
+
if saved.get_node_or_null(at) != null:
|
|
405
|
+
return "%s would still be in the scene when it loads." % at
|
|
406
|
+
return ""
|
|
407
|
+
)
|
|
408
|
+
if not written.is_empty():
|
|
409
|
+
return written
|
|
314
410
|
|
|
315
|
-
return {"ok": true, "deletedNodePath":
|
|
411
|
+
return {"ok": true, "deletedNodePath": at}
|
|
316
412
|
|
|
317
413
|
|
|
318
414
|
func duplicate_node(args: Dictionary) -> Dictionary:
|
|
@@ -325,38 +421,55 @@ func duplicate_node(args: Dictionary) -> Dictionary:
|
|
|
325
421
|
if node_path.is_empty() or new_name.is_empty():
|
|
326
422
|
return {"ok": false, "error": "Missing nodePath or newName"}
|
|
327
423
|
|
|
328
|
-
var
|
|
329
|
-
var
|
|
424
|
+
var opened: Array = _open(scene_path, true)
|
|
425
|
+
var scene: SceneFile = opened[0]
|
|
426
|
+
var refused: Dictionary = opened[1]
|
|
330
427
|
if not refused.is_empty():
|
|
331
428
|
return refused
|
|
332
429
|
|
|
333
|
-
var root: Node =
|
|
430
|
+
var root: Node = scene.root
|
|
334
431
|
var source: Node = _find_node(root, node_path)
|
|
335
432
|
if not source:
|
|
336
|
-
|
|
337
|
-
|
|
433
|
+
return scene.refuse("Node not found: " + node_path)
|
|
434
|
+
if source == root:
|
|
435
|
+
return scene.refuse("The root of %s cannot be copied into its own scene." % scene_path)
|
|
436
|
+
var foreign: String = scene.foreign(source)
|
|
437
|
+
if not foreign.is_empty():
|
|
438
|
+
return scene.refuse(foreign)
|
|
338
439
|
|
|
339
440
|
var target_parent: Node = source.get_parent()
|
|
340
441
|
if not parent_path.is_empty():
|
|
341
442
|
target_parent = _find_node(root, parent_path)
|
|
342
443
|
if not target_parent:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
444
|
+
return scene.refuse("Parent not found: " + parent_path)
|
|
445
|
+
foreign = scene.foreign(target_parent)
|
|
446
|
+
if not foreign.is_empty():
|
|
447
|
+
return scene.refuse(foreign)
|
|
448
|
+
var unusable: String = scene.unusable_name(target_parent, new_name)
|
|
449
|
+
if not unusable.is_empty():
|
|
450
|
+
return scene.refuse(unusable)
|
|
451
|
+
|
|
452
|
+
var copies: Dictionary = {}
|
|
453
|
+
var duplicated_node: Node = _copy(source, root, copies)
|
|
347
454
|
if not duplicated_node:
|
|
348
|
-
|
|
349
|
-
return {"ok": false, "error": "Failed to duplicate node: " + node_path}
|
|
455
|
+
return scene.refuse("Failed to duplicate node: " + node_path)
|
|
350
456
|
|
|
351
457
|
duplicated_node.name = new_name
|
|
352
458
|
target_parent.add_child(duplicated_node)
|
|
353
|
-
|
|
459
|
+
_own(duplicated_node, root)
|
|
460
|
+
_reconnect(copies, source)
|
|
354
461
|
|
|
355
|
-
var
|
|
356
|
-
|
|
357
|
-
|
|
462
|
+
var new_path: String = scene.path_of(duplicated_node)
|
|
463
|
+
var written: Dictionary = scene.write(
|
|
464
|
+
func(saved: Node) -> String:
|
|
465
|
+
if saved.get_node_or_null(new_path) == null:
|
|
466
|
+
return "the copy would not be in the scene when it loads."
|
|
467
|
+
return ""
|
|
468
|
+
)
|
|
469
|
+
if not written.is_empty():
|
|
470
|
+
return written
|
|
358
471
|
|
|
359
|
-
return {"ok": true, "nodePath": node_path, "newName": new_name}
|
|
472
|
+
return {"ok": true, "nodePath": node_path, "newName": new_name, "newNodePath": new_path}
|
|
360
473
|
|
|
361
474
|
|
|
362
475
|
func reparent_node(args: Dictionary) -> Dictionary:
|
|
@@ -370,35 +483,53 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
370
483
|
if new_parent_path.is_empty():
|
|
371
484
|
return {"ok": false, "error": "Missing newParentPath"}
|
|
372
485
|
|
|
373
|
-
var
|
|
374
|
-
var
|
|
486
|
+
var opened: Array = _open(scene_path, true)
|
|
487
|
+
var scene: SceneFile = opened[0]
|
|
488
|
+
var refused: Dictionary = opened[1]
|
|
375
489
|
if not refused.is_empty():
|
|
376
490
|
return refused
|
|
377
491
|
|
|
378
|
-
var root: Node =
|
|
492
|
+
var root: Node = scene.root
|
|
379
493
|
var node: Node = _find_node(root, node_path)
|
|
380
494
|
var new_parent: Node = _find_node(root, new_parent_path)
|
|
381
495
|
if not node:
|
|
382
|
-
|
|
383
|
-
return {"ok": false, "error": "Node not found: " + node_path}
|
|
496
|
+
return scene.refuse("Node not found: " + node_path)
|
|
384
497
|
if not new_parent:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
498
|
+
return scene.refuse("New parent not found: " + new_parent_path)
|
|
499
|
+
var fixed: String = scene.fixed(node)
|
|
500
|
+
if not fixed.is_empty():
|
|
501
|
+
return scene.refuse(fixed)
|
|
502
|
+
# Removed first and then refused by add_child, the node and everything under it were saved as
|
|
503
|
+
# gone.
|
|
504
|
+
if new_parent == node or node.is_ancestor_of(new_parent):
|
|
505
|
+
return scene.refuse(
|
|
506
|
+
"%s cannot go under %s, which is itself or inside it." % [node_path, new_parent_path]
|
|
507
|
+
)
|
|
508
|
+
var foreign: String = scene.foreign(new_parent)
|
|
509
|
+
if not foreign.is_empty():
|
|
510
|
+
return scene.refuse(foreign)
|
|
511
|
+
var unusable: String = scene.unusable_name(new_parent, str(node.name), node)
|
|
512
|
+
if not unusable.is_empty():
|
|
513
|
+
return scene.refuse(unusable)
|
|
514
|
+
|
|
515
|
+
var old_path: String = scene.path_of(node)
|
|
516
|
+
node.get_parent().remove_child(node)
|
|
394
517
|
new_parent.add_child(node)
|
|
395
|
-
|
|
518
|
+
_own(node, root)
|
|
396
519
|
|
|
397
|
-
var
|
|
398
|
-
|
|
399
|
-
|
|
520
|
+
var new_path: String = scene.path_of(node)
|
|
521
|
+
var written: Dictionary = scene.write(
|
|
522
|
+
func(saved: Node) -> String:
|
|
523
|
+
if saved.get_node_or_null(new_path) == null:
|
|
524
|
+
return "%s would not be in the scene when it loads." % new_path
|
|
525
|
+
if old_path != new_path and saved.get_node_or_null(old_path) != null:
|
|
526
|
+
return "%s would still be in the scene as well when it loads." % old_path
|
|
527
|
+
return ""
|
|
528
|
+
)
|
|
529
|
+
if not written.is_empty():
|
|
530
|
+
return written
|
|
400
531
|
|
|
401
|
-
return {"ok": true, "nodePath":
|
|
532
|
+
return {"ok": true, "nodePath": old_path, "newParentPath": new_parent_path, "newNodePath": new_path}
|
|
402
533
|
|
|
403
534
|
|
|
404
535
|
func set_node_properties(args: Dictionary) -> Dictionary:
|
|
@@ -407,27 +538,41 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
407
538
|
var node_path: String = str(args.get("nodePath", "."))
|
|
408
539
|
var properties: Dictionary = _parse_properties_arg(args.get("properties", {}))
|
|
409
540
|
|
|
410
|
-
var
|
|
411
|
-
var
|
|
541
|
+
var opened: Array = _open(scene_path, true)
|
|
542
|
+
var scene: SceneFile = opened[0]
|
|
543
|
+
var refused: Dictionary = opened[1]
|
|
412
544
|
if not refused.is_empty():
|
|
413
545
|
return refused
|
|
414
546
|
|
|
415
|
-
var
|
|
416
|
-
var node: Node = _find_node(root, node_path)
|
|
547
|
+
var node: Node = _find_node(scene.root, node_path)
|
|
417
548
|
if not node:
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
549
|
+
return scene.refuse("Node not found: " + node_path)
|
|
550
|
+
var foreign: String = scene.foreign(node)
|
|
551
|
+
if not foreign.is_empty():
|
|
552
|
+
return scene.refuse(foreign)
|
|
553
|
+
if properties.has("name") and node != scene.root:
|
|
554
|
+
var fixed: String = scene.fixed(node)
|
|
555
|
+
if not fixed.is_empty():
|
|
556
|
+
return scene.refuse(fixed)
|
|
557
|
+
var unusable: String = scene.unusable_name(node.get_parent(), str(properties["name"]), node)
|
|
558
|
+
if not unusable.is_empty():
|
|
559
|
+
return scene.refuse(unusable)
|
|
560
|
+
|
|
561
|
+
var refused_property: String = _properties.write_all(node, properties)
|
|
422
562
|
if not refused_property.is_empty():
|
|
423
|
-
|
|
424
|
-
return {"ok": false, "error": refused_property}
|
|
563
|
+
return scene.refuse(_missing_on(node, refused_property))
|
|
425
564
|
|
|
426
|
-
var
|
|
427
|
-
|
|
428
|
-
|
|
565
|
+
var held: Dictionary = {}
|
|
566
|
+
for key: Variant in properties:
|
|
567
|
+
held[str(key)] = _readable(node, node.get(str(key)))
|
|
568
|
+
var new_path: String = scene.path_of(node)
|
|
569
|
+
var written: Dictionary = scene.write(
|
|
570
|
+
func(saved: Node) -> String: return _kept(scene, saved, node, properties.keys())
|
|
571
|
+
)
|
|
572
|
+
if not written.is_empty():
|
|
573
|
+
return written
|
|
429
574
|
|
|
430
|
-
return {"ok": true, "nodePath":
|
|
575
|
+
return {"ok": true, "nodePath": new_path, "properties": held}
|
|
431
576
|
|
|
432
577
|
|
|
433
578
|
func get_node_properties(args: Dictionary) -> Dictionary:
|
|
@@ -436,40 +581,28 @@ func get_node_properties(args: Dictionary) -> Dictionary:
|
|
|
436
581
|
var node_path: String = str(args.get("nodePath", "."))
|
|
437
582
|
var include_defaults: bool = Read.as_bool(args.get("includeDefaults", false))
|
|
438
583
|
|
|
439
|
-
var
|
|
440
|
-
var
|
|
584
|
+
var opened: Array = _open(scene_path, false)
|
|
585
|
+
var scene: SceneFile = opened[0]
|
|
586
|
+
var refused: Dictionary = opened[1]
|
|
441
587
|
if not refused.is_empty():
|
|
442
588
|
return refused
|
|
443
589
|
|
|
444
|
-
var
|
|
445
|
-
var node: Node = _find_node(root, node_path)
|
|
590
|
+
var node: Node = _find_node(scene.root, node_path)
|
|
446
591
|
if not node:
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
continue
|
|
462
|
-
var current_val: Variant = node.get(prop_name)
|
|
463
|
-
if not include_defaults and defaults:
|
|
464
|
-
var default_val: Variant = defaults.get(prop_name)
|
|
465
|
-
if current_val == default_val:
|
|
466
|
-
continue
|
|
467
|
-
props[prop_name] = _values.serialize_value(current_val)
|
|
468
|
-
|
|
469
|
-
if defaults:
|
|
470
|
-
defaults.queue_free()
|
|
471
|
-
root.queue_free()
|
|
472
|
-
return {"ok": true, "nodePath": node_path, "properties": props}
|
|
592
|
+
return scene.refuse("Node not found: " + node_path)
|
|
593
|
+
|
|
594
|
+
# Against the default the node would have in no scene: its script's for what the script
|
|
595
|
+
# declares, the class's for the rest. Compared with a bare instance of the class, every exported
|
|
596
|
+
# variable read as changed; and a value this scene sets back to the default was hidden, so the
|
|
597
|
+
# names this scene's file sets are always answered.
|
|
598
|
+
var props: Dictionary = _properties_of(node, include_defaults, scene.set_here(node))
|
|
599
|
+
scene.close()
|
|
600
|
+
return {
|
|
601
|
+
"ok": true,
|
|
602
|
+
"nodePath": node_path,
|
|
603
|
+
"properties": props,
|
|
604
|
+
"unsavedInEditor": SceneFile.unsaved(scene_path)
|
|
605
|
+
}
|
|
473
606
|
|
|
474
607
|
|
|
475
608
|
func save_scene(args: Dictionary) -> Dictionary:
|
|
@@ -480,18 +613,69 @@ func save_scene(args: Dictionary) -> Dictionary:
|
|
|
480
613
|
if not new_path_raw.is_empty():
|
|
481
614
|
target_path = _to_scene_res_path(project_path, new_path_raw)
|
|
482
615
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
616
|
+
if target_path != scene_path:
|
|
617
|
+
var unsaved: String = SceneFile.unsaved_in_editor(target_path)
|
|
618
|
+
if not unsaved.is_empty():
|
|
619
|
+
return {"ok": false, "error": unsaved}
|
|
620
|
+
|
|
621
|
+
var open_root: Node = null
|
|
622
|
+
for open: Node in EditorInterface.get_open_scene_roots():
|
|
623
|
+
if open.scene_file_path == scene_path:
|
|
624
|
+
open_root = open
|
|
625
|
+
if open_root != null and target_path == scene_path:
|
|
626
|
+
return _save_open_scene(scene_path)
|
|
627
|
+
|
|
628
|
+
# Of the editor's scene when it is open, since that is what a save means, and of the file as it
|
|
629
|
+
# loads otherwise. The editor's scene is packed where it is, as the editor's own save packs it;
|
|
630
|
+
# packing leaves the nodes as they were.
|
|
631
|
+
var written: Dictionary = {}
|
|
632
|
+
if open_root != null:
|
|
633
|
+
var packed: PackedScene = PackedScene.new()
|
|
634
|
+
var packing: Error = packed.pack(open_root)
|
|
635
|
+
if packing != OK:
|
|
636
|
+
return {"ok": false, "error": "Failed to pack %s: %s" % [scene_path, error_string(packing)]}
|
|
637
|
+
written = SceneFile.save_packed(packed, target_path)
|
|
638
|
+
else:
|
|
639
|
+
var scene: SceneFile = SceneFile.new()
|
|
640
|
+
var refused: Dictionary = scene.open(scene_path, false)
|
|
641
|
+
if not refused.is_empty():
|
|
642
|
+
return refused
|
|
643
|
+
scene.path = target_path
|
|
644
|
+
written = scene.write()
|
|
645
|
+
if not written.is_empty():
|
|
646
|
+
return written
|
|
487
647
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
648
|
+
return {
|
|
649
|
+
"ok": true,
|
|
650
|
+
"scenePath": scene_path,
|
|
651
|
+
"savedPath": target_path,
|
|
652
|
+
"from": "editor" if open_root != null else "file"
|
|
653
|
+
}
|
|
493
654
|
|
|
494
|
-
|
|
655
|
+
|
|
656
|
+
## Saves the editor's own copy of [param scene_path] through the editor, which is what its Save
|
|
657
|
+
## does. Loading the file and writing it back, as this did, saved the disk version, and the reload
|
|
658
|
+
## after it discarded every unsaved change in the editor.
|
|
659
|
+
func _save_open_scene(scene_path: String) -> Dictionary:
|
|
660
|
+
var current: Node = EditorInterface.get_edited_scene_root()
|
|
661
|
+
var previous: String = current.scene_file_path if current != null else ""
|
|
662
|
+
if previous != scene_path:
|
|
663
|
+
EditorInterface.open_scene_from_path(scene_path)
|
|
664
|
+
var now: Node = EditorInterface.get_edited_scene_root()
|
|
665
|
+
if now == null or now.scene_file_path != scene_path:
|
|
666
|
+
return {"ok": false, "error": "The editor did not switch to %s to save it." % scene_path}
|
|
667
|
+
var saving: Error = EditorInterface.save_scene()
|
|
668
|
+
if not previous.is_empty() and previous != scene_path:
|
|
669
|
+
EditorInterface.open_scene_from_path(previous)
|
|
670
|
+
if saving != OK:
|
|
671
|
+
return {"ok": false, "error": "The editor failed to save %s: %s" % [scene_path, error_string(saving)]}
|
|
672
|
+
return {
|
|
673
|
+
"ok": true,
|
|
674
|
+
"scenePath": scene_path,
|
|
675
|
+
"savedPath": scene_path,
|
|
676
|
+
"from": "editor",
|
|
677
|
+
"unsavedInEditor": SceneFile.unsaved(scene_path)
|
|
678
|
+
}
|
|
495
679
|
|
|
496
680
|
|
|
497
681
|
func connect_signal(args: Dictionary) -> Dictionary:
|
|
@@ -513,45 +697,94 @@ func connect_signal(args: Dictionary) -> Dictionary:
|
|
|
513
697
|
):
|
|
514
698
|
return {"ok": false, "error": "Missing required signal connection arguments"}
|
|
515
699
|
|
|
516
|
-
var
|
|
517
|
-
var
|
|
700
|
+
var opened: Array = _open(scene_path, true)
|
|
701
|
+
var scene: SceneFile = opened[0]
|
|
702
|
+
var refused: Dictionary = opened[1]
|
|
518
703
|
if not refused.is_empty():
|
|
519
704
|
return refused
|
|
520
705
|
|
|
521
|
-
var
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
return {"ok": false, "error": "Source node not found: " + source_node_path}
|
|
527
|
-
if not target:
|
|
528
|
-
root.queue_free()
|
|
529
|
-
return {"ok": false, "error": "Target node not found: " + target_node_path}
|
|
530
|
-
if not source.has_signal(signal_name):
|
|
531
|
-
root.queue_free()
|
|
532
|
-
return {"ok": false, "error": "Signal not found on source: " + signal_name}
|
|
706
|
+
var ends: Array = _connection_ends(scene, source_node_path, signal_name, target_node_path)
|
|
707
|
+
if ends[0] == null:
|
|
708
|
+
return scene.refuse(str(ends[1]))
|
|
709
|
+
var source: Node = ends[0]
|
|
710
|
+
var target: Node = ends[1]
|
|
533
711
|
|
|
712
|
+
# Reconnected rather than left alone when the flags differ, so the connection saved is the one
|
|
713
|
+
# asked for. Left alone, the answer gave the flags asked for and the file kept the old ones.
|
|
534
714
|
var callable: Callable = Callable(target, method_name)
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
715
|
+
var method_found: bool = target.has_method(method_name)
|
|
716
|
+
if source.is_connected(signal_name, callable):
|
|
717
|
+
source.disconnect(signal_name, callable)
|
|
718
|
+
var connect_result: Error = source.connect(signal_name, callable, flags)
|
|
719
|
+
if connect_result != OK:
|
|
720
|
+
return scene.refuse("Failed to connect signal: " + error_string(connect_result))
|
|
721
|
+
|
|
722
|
+
var from: String = scene.path_of(source)
|
|
723
|
+
var to: String = scene.path_of(target)
|
|
724
|
+
var saved_flags: Array = [-1]
|
|
725
|
+
var written: Dictionary = scene.write(
|
|
726
|
+
func(saved: Node) -> String:
|
|
727
|
+
saved_flags[0] = _connection_flags(saved, from, signal_name, to, method_name)
|
|
728
|
+
if saved_flags[0] < 0:
|
|
729
|
+
return "the connection would not be in the scene when it loads."
|
|
730
|
+
return ""
|
|
731
|
+
)
|
|
732
|
+
if not written.is_empty():
|
|
733
|
+
return written
|
|
544
734
|
|
|
545
735
|
return {
|
|
546
736
|
"ok": true,
|
|
547
|
-
"sourceNodePath":
|
|
737
|
+
"sourceNodePath": from,
|
|
548
738
|
"signalName": signal_name,
|
|
549
|
-
"targetNodePath":
|
|
739
|
+
"targetNodePath": to,
|
|
550
740
|
"methodName": method_name,
|
|
551
|
-
"flags":
|
|
741
|
+
"flags": saved_flags[0],
|
|
742
|
+
# Connected either way, as the editor's own dialog allows, since the method may be written
|
|
743
|
+
# next; a caller told nothing learnt of a misspelt one only when the signal fired.
|
|
744
|
+
"methodFound": method_found
|
|
552
745
|
}
|
|
553
746
|
|
|
554
747
|
|
|
748
|
+
## The two ends of a connection in [param scene], as [source, target], or [null, why not].
|
|
749
|
+
func _connection_ends(
|
|
750
|
+
scene: SceneFile, source_node_path: String, signal_name: String, target_node_path: String
|
|
751
|
+
) -> Array:
|
|
752
|
+
var source: Node = _find_node(scene.root, source_node_path)
|
|
753
|
+
var target: Node = _find_node(scene.root, target_node_path)
|
|
754
|
+
if not source:
|
|
755
|
+
return [null, "Source node not found: " + source_node_path]
|
|
756
|
+
if not target:
|
|
757
|
+
return [null, "Target node not found: " + target_node_path]
|
|
758
|
+
# pack drops a connection with either end inside an instanced scene, and the answer said it was
|
|
759
|
+
# made or removed.
|
|
760
|
+
for end: Node in [source, target]:
|
|
761
|
+
var foreign: String = scene.foreign(end)
|
|
762
|
+
if not foreign.is_empty():
|
|
763
|
+
return [null, foreign]
|
|
764
|
+
if not source.has_signal(signal_name):
|
|
765
|
+
return [null, _missing_on(source, "%s has no signal %s" % [source_node_path, signal_name])]
|
|
766
|
+
return [source, target]
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
## The flags of the persistent connection from [param from]'s [param signal_name] to
|
|
770
|
+
## [param method] on [param to] in the tree under [param root], or -1 when there is none.
|
|
771
|
+
func _connection_flags(root: Node, from: String, signal_name: String, to: String, method: String) -> int:
|
|
772
|
+
var source: Node = root.get_node_or_null(from)
|
|
773
|
+
var target: Node = root.get_node_or_null(to)
|
|
774
|
+
if source == null or target == null or not source.has_signal(signal_name):
|
|
775
|
+
return -1
|
|
776
|
+
for conn: Dictionary in source.get_signal_connection_list(signal_name):
|
|
777
|
+
var flags: int = Read.as_int(conn.get("flags", 0))
|
|
778
|
+
var callable: Callable = conn.get("callable", Callable())
|
|
779
|
+
if (
|
|
780
|
+
flags & Object.CONNECT_PERSIST
|
|
781
|
+
and callable.get_object() == target
|
|
782
|
+
and str(callable.get_method()) == method
|
|
783
|
+
):
|
|
784
|
+
return flags
|
|
785
|
+
return -1
|
|
786
|
+
|
|
787
|
+
|
|
555
788
|
func disconnect_signal(args: Dictionary) -> Dictionary:
|
|
556
789
|
var project_path: String = str(args.get("projectPath", ""))
|
|
557
790
|
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
@@ -568,34 +801,42 @@ func disconnect_signal(args: Dictionary) -> Dictionary:
|
|
|
568
801
|
):
|
|
569
802
|
return {"ok": false, "error": "Missing required signal disconnection arguments"}
|
|
570
803
|
|
|
571
|
-
var
|
|
572
|
-
var
|
|
804
|
+
var opened: Array = _open(scene_path, true)
|
|
805
|
+
var scene: SceneFile = opened[0]
|
|
806
|
+
var refused: Dictionary = opened[1]
|
|
573
807
|
if not refused.is_empty():
|
|
574
808
|
return refused
|
|
575
809
|
|
|
576
|
-
var
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
810
|
+
var ends: Array = _connection_ends(scene, source_node_path, signal_name, target_node_path)
|
|
811
|
+
if ends[0] == null:
|
|
812
|
+
return scene.refuse(str(ends[1]))
|
|
813
|
+
var source: Node = ends[0]
|
|
814
|
+
var target: Node = ends[1]
|
|
815
|
+
|
|
816
|
+
var from: String = scene.path_of(source)
|
|
817
|
+
var to: String = scene.path_of(target)
|
|
818
|
+
# A connection that was never there saved the scene unchanged and answered as removed, so a
|
|
819
|
+
# typo in either name read as success.
|
|
820
|
+
if _connection_flags(scene.root, from, signal_name, to, method_name) < 0:
|
|
821
|
+
return scene.refuse(
|
|
822
|
+
"%s has no connection from %s.%s to %s.%s." % [scene_path, from, signal_name, to, method_name]
|
|
823
|
+
)
|
|
824
|
+
source.disconnect(signal_name, Callable(target, method_name))
|
|
825
|
+
|
|
826
|
+
var written: Dictionary = scene.write(
|
|
827
|
+
func(saved: Node) -> String:
|
|
828
|
+
if _connection_flags(saved, from, signal_name, to, method_name) >= 0:
|
|
829
|
+
return "the connection would still be in the scene when it loads."
|
|
830
|
+
return ""
|
|
831
|
+
)
|
|
832
|
+
if not written.is_empty():
|
|
833
|
+
return written
|
|
593
834
|
|
|
594
835
|
return {
|
|
595
836
|
"ok": true,
|
|
596
|
-
"sourceNodePath":
|
|
837
|
+
"sourceNodePath": from,
|
|
597
838
|
"signalName": signal_name,
|
|
598
|
-
"targetNodePath":
|
|
839
|
+
"targetNodePath": to,
|
|
599
840
|
"methodName": method_name
|
|
600
841
|
}
|
|
601
842
|
|
|
@@ -605,207 +846,60 @@ func list_connections(args: Dictionary) -> Dictionary:
|
|
|
605
846
|
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
606
847
|
var filter_path: String = str(args.get("nodePath", ""))
|
|
607
848
|
|
|
608
|
-
var
|
|
609
|
-
var
|
|
849
|
+
var opened: Array = _open(scene_path, false)
|
|
850
|
+
var scene: SceneFile = opened[0]
|
|
851
|
+
var refused: Dictionary = opened[1]
|
|
610
852
|
if not refused.is_empty():
|
|
611
853
|
return refused
|
|
612
854
|
|
|
613
|
-
var root: Node =
|
|
855
|
+
var root: Node = scene.root
|
|
856
|
+
var filtered: Node = null
|
|
857
|
+
if not filter_path.is_empty():
|
|
858
|
+
filtered = _find_node(root, filter_path)
|
|
859
|
+
if filtered == null:
|
|
860
|
+
return scene.refuse("Node not found: " + filter_path)
|
|
614
861
|
var nodes: Array = []
|
|
615
862
|
_collect_nodes_recursive(root, ".", nodes)
|
|
616
863
|
|
|
864
|
+
# Only persistent connections, which are the ones a scene file holds: a Container connects
|
|
865
|
+
# signals of each child to itself as the child is added, and those were answered as if the file
|
|
866
|
+
# held them. Matched on either end, since the filter is for connections involving the node.
|
|
617
867
|
var connections: Array = []
|
|
618
868
|
for entry: Dictionary in nodes:
|
|
619
869
|
var path: String = str(entry["path"])
|
|
620
|
-
if not filter_path.is_empty() and filter_path != path:
|
|
621
|
-
continue
|
|
622
870
|
var node: Node = entry["node"]
|
|
623
871
|
for signal_info: Dictionary in node.get_signal_list():
|
|
624
872
|
var signal_name: String = str(signal_info.get("name", ""))
|
|
625
873
|
if signal_name.is_empty():
|
|
626
874
|
continue
|
|
627
875
|
for conn: Dictionary in node.get_signal_connection_list(signal_name):
|
|
876
|
+
var flags: int = Read.as_int(conn.get("flags", 0))
|
|
877
|
+
if not flags & Object.CONNECT_PERSIST:
|
|
878
|
+
continue
|
|
628
879
|
var callable: Callable = conn.get("callable", Callable())
|
|
629
880
|
var target_obj: Object = callable.get_object()
|
|
881
|
+
if filtered != null and node != filtered and target_obj != filtered:
|
|
882
|
+
continue
|
|
630
883
|
var target_path: String = ""
|
|
884
|
+
var editable: bool = scene.foreign(node).is_empty()
|
|
631
885
|
if target_obj is Node:
|
|
632
886
|
target_path = str(root.get_path_to(target_obj as Node))
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
887
|
+
editable = editable and scene.foreign(target_obj as Node).is_empty()
|
|
888
|
+
(
|
|
889
|
+
connections
|
|
890
|
+
. append(
|
|
891
|
+
{
|
|
892
|
+
"sourceNodePath": path,
|
|
893
|
+
"signalName": signal_name,
|
|
894
|
+
"targetNodePath": target_path,
|
|
895
|
+
"methodName": str(callable.get_method()),
|
|
896
|
+
"flags": flags,
|
|
897
|
+
# False for a connection an instanced scene makes inside itself, which only that
|
|
898
|
+
# scene can change.
|
|
899
|
+
"editable": editable
|
|
900
|
+
}
|
|
901
|
+
)
|
|
641
902
|
)
|
|
642
903
|
|
|
643
|
-
|
|
644
|
-
return {"ok": true, "connections": connections}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
## Rescan the project filesystem, and report whether a scan is still running.
|
|
648
|
-
##
|
|
649
|
-
## The editor rescans when its window regains focus, so a script written by anything other
|
|
650
|
-
## than the editor stays invisible until someone clicks on Godot. Until then its
|
|
651
|
-
## `class_name` is missing from the global class list and the language server reports every
|
|
652
|
-
## use of it as an unknown type, which is godotengine/godot#42786.
|
|
653
|
-
##
|
|
654
|
-
## Returns as soon as the scan is queued rather than awaiting it, because the tool executor
|
|
655
|
-
## takes a Dictionary and not a coroutine. Pass `statusOnly` to poll without starting
|
|
656
|
-
## another scan.
|
|
657
|
-
func rescan_filesystem(args: Dictionary) -> Dictionary:
|
|
658
|
-
if not _editor_plugin:
|
|
659
|
-
return {"ok": false, "error": "Editor plugin unavailable"}
|
|
660
|
-
|
|
661
|
-
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
662
|
-
# Not over a scan or an import the editor is already running: a scan asked for then starts a
|
|
663
|
-
# second reimport over the first, and the editor logs "Task 'reimport' already exists" and two
|
|
664
|
-
# conditions from its progress dialog. Declined and said, so the caller waits and asks again.
|
|
665
|
-
var busy: bool = filesystem.is_scanning() or filesystem.is_importing()
|
|
666
|
-
var started: bool = false
|
|
667
|
-
var completed_before: int = _scans_completed
|
|
668
|
-
var uids: Dictionary = {"reread": [], "duplicated": []}
|
|
669
|
-
if not Read.as_bool(args.get("statusOnly", false)) and not busy:
|
|
670
|
-
uids = _reread_changed_uids(filesystem)
|
|
671
|
-
filesystem.scan()
|
|
672
|
-
# Whether the scan ran, read off the editor rather than assumed. `scan()` returns without
|
|
673
|
-
# a word while the thread of the scan before is still to be joined, which is a frame or
|
|
674
|
-
# more after that scan stops reporting itself, and that scan then signals as it is joined.
|
|
675
|
-
# Taken as started, the answer came from a scan begun before the files were written:
|
|
676
|
-
# downstream, a rescan after 698 files were rewritten answered finished in 1.5s and the
|
|
677
|
-
# one changed file stayed on its old import. A threaded scan reports itself from inside
|
|
678
|
-
# the call, and one run on this thread has finished and signalled before it returns.
|
|
679
|
-
started = filesystem.is_scanning() or _scans_completed != completed_before
|
|
680
|
-
|
|
681
|
-
# Importing is reported separately from scanning, and a class is not registered until
|
|
682
|
-
# both are done, so a caller watching only one of them can look too early. The two counts are
|
|
683
|
-
# what a caller waits on: the scan it asked for is over once the count has moved past the one
|
|
684
|
-
# from before it was asked.
|
|
685
|
-
return {
|
|
686
|
-
"ok": true,
|
|
687
|
-
"started": started,
|
|
688
|
-
"scanning": filesystem.is_scanning(),
|
|
689
|
-
"importing": filesystem.is_importing(),
|
|
690
|
-
"scansCompletedBefore": completed_before,
|
|
691
|
-
"scansCompleted": _scans_completed,
|
|
692
|
-
"uidsReread": uids["reread"],
|
|
693
|
-
"uidsDuplicatedOnDisk": uids["duplicated"],
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
## Has the editor read again every file whose `.uid` names a UID the editor does not hold for it.
|
|
698
|
-
##
|
|
699
|
-
## The scan re-reads a file when the file changes and not when only the `.uid` beside it does, so a
|
|
700
|
-
## script copied with its `.uid` and then given a fresh one keeps the copied UID in the editor, and
|
|
701
|
-
## every headless engine reading the editor's cache warns of a duplicate. Measured on 4.7.2: a scan
|
|
702
|
-
## after the `.uid` was rewritten left both scripts on one UID in the cache. What the editor holds
|
|
703
|
-
## is read off its UID table, which maps each UID to one path, so a `.uid` whose UID is missing from
|
|
704
|
-
## the table or mapped to another file is one the editor has not taken in.
|
|
705
|
-
##
|
|
706
|
-
## Two files on disk naming one UID are left alone and named: reading them again cannot settle
|
|
707
|
-
## which of them owns it, and doing it on every scan would only move the UID between them.
|
|
708
|
-
func _reread_changed_uids(filesystem: EditorFileSystem) -> Dictionary:
|
|
709
|
-
var sidecars: Array[String] = []
|
|
710
|
-
_uid_files("res://", sidecars)
|
|
711
|
-
var claimed: Dictionary = {}
|
|
712
|
-
for sidecar: String in sidecars:
|
|
713
|
-
var id: int = ResourceUID.text_to_id(FileAccess.get_file_as_string(sidecar).strip_edges())
|
|
714
|
-
var path: String = sidecar.trim_suffix(".uid")
|
|
715
|
-
if id == ResourceUID.INVALID_ID or not FileAccess.file_exists(path):
|
|
716
|
-
continue
|
|
717
|
-
var paths: Array = claimed.get_or_add(id, [])
|
|
718
|
-
paths.append(path)
|
|
719
|
-
var reread: Array[String] = []
|
|
720
|
-
var duplicated: Array[Array] = []
|
|
721
|
-
for id: int in claimed:
|
|
722
|
-
var paths: Array = claimed[id]
|
|
723
|
-
if paths.size() > 1:
|
|
724
|
-
paths.sort()
|
|
725
|
-
duplicated.append(paths)
|
|
726
|
-
continue
|
|
727
|
-
var path: String = str(paths[0])
|
|
728
|
-
if ResourceUID.has_id(id) and ResourceUID.get_id_path(id) == path:
|
|
729
|
-
continue
|
|
730
|
-
filesystem.update_file(path)
|
|
731
|
-
reread.append(path)
|
|
732
|
-
reread.sort()
|
|
733
|
-
duplicated.sort()
|
|
734
|
-
return {"reread": reread, "duplicated": duplicated}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
## Every `.uid` under [param directory], skipping what the editor skips: hidden directories, any
|
|
738
|
-
## holding a `.gdignore`, and any below the project's own that holds a `project.godot` of its own,
|
|
739
|
-
## which the editor passes over as another project. A nested project's UIDs are never in the editor's
|
|
740
|
-
## table, so without that last one its files would be read again on every rescan, and one holding a
|
|
741
|
-
## copy of an outer file's `.uid` named as a duplicate on disk.
|
|
742
|
-
func _uid_files(directory: String, found: Array[String]) -> void:
|
|
743
|
-
var listing: DirAccess = DirAccess.open(directory)
|
|
744
|
-
if listing == null or FileAccess.file_exists(directory.path_join(".gdignore")):
|
|
745
|
-
return
|
|
746
|
-
if directory != "res://" and FileAccess.file_exists(directory.path_join("project.godot")):
|
|
747
|
-
return
|
|
748
|
-
for child: String in listing.get_directories():
|
|
749
|
-
if not child.begins_with("."):
|
|
750
|
-
_uid_files(directory.path_join(child), found)
|
|
751
|
-
for file: String in listing.get_files():
|
|
752
|
-
if file.ends_with(".uid"):
|
|
753
|
-
found.append(directory.path_join(file))
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
## Whether the editor is scanning, and how long ago its last scan finished, without asking for one.
|
|
757
|
-
##
|
|
758
|
-
## For a caller about to start a game: the editor rewrites the class cache a frame after a scan
|
|
759
|
-
## stops reporting, so a game booted during the scan or in that frame reads a file being rewritten.
|
|
760
|
-
## Apart from `rescan_filesystem` because asking must not start or count anything.
|
|
761
|
-
func scan_status(_args: Dictionary) -> Dictionary:
|
|
762
|
-
if not _editor_plugin:
|
|
763
|
-
return {"ok": false, "error": "Editor plugin unavailable"}
|
|
764
|
-
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
765
|
-
return {
|
|
766
|
-
"ok": true,
|
|
767
|
-
"scanning": filesystem.is_scanning(),
|
|
768
|
-
"importing": filesystem.is_importing(),
|
|
769
|
-
"sinceScanFinishedMs":
|
|
770
|
-
-1 if _scan_finished_at_msec < 0 else Time.get_ticks_msec() - _scan_finished_at_msec,
|
|
771
|
-
"reimportsCompleted": _reimports_completed,
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
## Reimport the files named under `paths`, whatever their state, the way the editor's Reimport
|
|
776
|
-
## button does, and report whether the reimport was started.
|
|
777
|
-
##
|
|
778
|
-
## Started on the next frame rather than run here: the editor imports on its main thread, and a
|
|
779
|
-
## reimport of a few hundred scenes outlasts the wait on any one command. The caller waits for
|
|
780
|
-
## `reimportsCompleted` in [method scan_status] to move past `reimportsCompletedBefore`. Declined
|
|
781
|
-
## while the editor scans or imports, since `reimport_files` refuses to run inside an import.
|
|
782
|
-
func reimport_files(args: Dictionary) -> Dictionary:
|
|
783
|
-
if not _editor_plugin:
|
|
784
|
-
return {"ok": false, "error": "Editor plugin unavailable"}
|
|
785
|
-
var paths: Array[String] = []
|
|
786
|
-
var given: Variant = args.get("paths", [])
|
|
787
|
-
if given is Array:
|
|
788
|
-
for path: Variant in given:
|
|
789
|
-
paths.append(str(path))
|
|
790
|
-
if paths.is_empty():
|
|
791
|
-
return {"ok": false, "error": "reimport_files needs paths"}
|
|
792
|
-
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
793
|
-
var busy: bool = filesystem.is_scanning() or filesystem.is_importing()
|
|
794
|
-
if not busy:
|
|
795
|
-
_reimport.call_deferred(PackedStringArray(paths))
|
|
796
|
-
return {
|
|
797
|
-
"ok": true,
|
|
798
|
-
"started": not busy,
|
|
799
|
-
"scanning": filesystem.is_scanning(),
|
|
800
|
-
"importing": filesystem.is_importing(),
|
|
801
|
-
"reimportsCompletedBefore": _reimports_completed,
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
func _reimport(paths: PackedStringArray) -> void:
|
|
806
|
-
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
807
|
-
# A scan can begin between the answer and this frame, and its import would refuse this one.
|
|
808
|
-
while filesystem.is_scanning() or filesystem.is_importing():
|
|
809
|
-
await get_tree().process_frame
|
|
810
|
-
filesystem.reimport_files(paths)
|
|
811
|
-
_reimports_completed += 1
|
|
904
|
+
scene.close()
|
|
905
|
+
return {"ok": true, "connections": connections, "unsavedInEditor": SceneFile.unsaved(scene_path)}
|