gdharness 1.2.30 → 1.2.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/build/cli.js +7197 -6159
  2. package/build/godot/addons/gdharness_editor/bridge_client.gd +14 -0
  3. package/build/godot/addons/gdharness_editor/file_reads.gd +237 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd.uid +1 -0
  5. package/build/godot/addons/gdharness_editor/plugin.gd +27 -0
  6. package/build/godot/addons/gdharness_editor/property_values.gd +80 -54
  7. package/build/godot/addons/gdharness_editor/scene_file.gd +38 -1
  8. package/build/godot/addons/gdharness_editor/serialisation.gd +46 -0
  9. package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
  10. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +22 -0
  11. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +64 -1
  12. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +82 -44
  13. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +178 -0
  14. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +73 -124
  16. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +39 -0
  17. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +285 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_hover.gd.uid +1 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_input.gd +151 -106
  20. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +112 -0
  21. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd.uid +1 -0
  22. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +5 -4
  23. package/build/godot/addons/gdharness_runtime/runtime_values.gd +1 -47
  24. package/build/godot/addons/gdharness_runtime/serialisation.gd +46 -0
  25. package/build/godot/operations/audio_buses.gd +14 -1
  26. package/build/godot/operations/class_cache.gd +146 -35
  27. package/build/godot/operations/classdb_queries.gd +19 -12
  28. package/build/godot/operations/file_reads.gd +237 -0
  29. package/build/godot/operations/gdscript_authoring.gd +136 -19
  30. package/build/godot/operations/godot_operations.gd +24 -0
  31. package/build/godot/operations/project_config.gd +16 -6
  32. package/build/godot/operations/property_values.gd +767 -0
  33. package/build/godot/operations/rename_checks.gd +5 -2
  34. package/build/godot/operations/serialisation.gd +46 -0
  35. package/build/index.js +4896 -3872
  36. package/build/keeper.js +184 -71
  37. package/package.json +1 -1
@@ -1,9 +1,11 @@
1
1
  @tool
2
2
  extends Node
3
3
 
4
- ## The editor's view of the files on disk: scans, the reimports asked for, and where each stands.
4
+ ## The editor's view of the files on disk: scans, the reimports asked for, where each stands, and
5
+ ## files moved under it.
5
6
 
6
7
  const Read = preload("../reading.gd")
8
+ const SceneFile = preload("../scene_file.gd")
7
9
 
8
10
  var _editor_plugin: EditorPlugin = null
9
11
 
@@ -170,6 +172,7 @@ func scan_status(_args: Dictionary) -> Dictionary:
170
172
  "ok": true,
171
173
  "scanning": filesystem.is_scanning(),
172
174
  "importing": filesystem.is_importing(),
175
+ "pending": SceneFile.scan_due,
173
176
  "sinceScanFinishedMs":
174
177
  -1 if _scan_finished_at_msec < 0 else Time.get_ticks_msec() - _scan_finished_at_msec,
175
178
  "reimportsCompleted": _reimports_completed,
@@ -206,6 +209,66 @@ func reimport_files(args: Dictionary) -> Dictionary:
206
209
  }
207
210
 
208
211
 
212
+ ## Brings the editor up to files moved under it, the way its FileSystem dock brings itself up after
213
+ ## a move of its own: each cached resource and each open scene named for where it now is, so what
214
+ ## holds them saves the new path, the editor's file list told of both ends, and each open scene that
215
+ ## moved or that the move rewrote read again from the file. A scene with unsaved edits is left as it
216
+ ## is and named, since reading it again would discard them; its root names the new path either way.
217
+ ##
218
+ ## A moved tab is read again even when its text did not change: the editor keeps each tab's path
219
+ ## twice, on the root and in its own list, and a script can set only the first. The second is what
220
+ ## [method EditorInterface.get_open_scenes] answers, and reading the scene again is what writes it.
221
+ func files_moved(args: Dictionary) -> Dictionary:
222
+ var renames: Dictionary[String, String] = {}
223
+ var given: Variant = args.get("moved", [])
224
+ if given is Array:
225
+ var pairs: Array = given
226
+ for pair: Variant in pairs:
227
+ if pair is Dictionary:
228
+ var named: Dictionary = pair
229
+ renames[str(named.get("from", ""))] = str(named.get("to", ""))
230
+ for from: String in renames:
231
+ if ResourceLoader.has_cached(from):
232
+ var held: Resource = ResourceLoader.get_cached_ref(from)
233
+ if held != null:
234
+ held.take_over_path(renames[from])
235
+ var dirty: Array[String] = []
236
+ for path: String in EditorInterface.get_unsaved_scenes():
237
+ dirty.append(renames.get(path, path))
238
+ var retargeted: Array[String] = []
239
+ var reread: Array[String] = []
240
+ for root: Node in EditorInterface.get_open_scene_roots():
241
+ var path: String = root.scene_file_path
242
+ if renames.has(path):
243
+ root.scene_file_path = renames[path]
244
+ retargeted.append(path)
245
+ reread.append(renames[path])
246
+ var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
247
+ for from: String in renames:
248
+ filesystem.update_file(from)
249
+ filesystem.update_file(renames[from])
250
+ var open: Array[String] = []
251
+ for root: Node in EditorInterface.get_open_scene_roots():
252
+ open.append(root.scene_file_path)
253
+ var changed: Variant = args.get("changed", [])
254
+ if changed is Array:
255
+ var paths: Array = changed
256
+ for path: Variant in paths:
257
+ if open.has(str(path)) and not reread.has(str(path)):
258
+ reread.append(str(path))
259
+ var reloaded: Array[String] = []
260
+ var unsaved: Array[String] = []
261
+ for scene: String in reread:
262
+ if dirty.has(scene):
263
+ unsaved.append(scene)
264
+ else:
265
+ EditorInterface.reload_scene_from_path(scene)
266
+ reloaded.append(scene)
267
+ reloaded.sort()
268
+ unsaved.sort()
269
+ return {"ok": true, "retargetedTabs": retargeted, "reloadedScenes": reloaded, "unsavedScenes": unsaved}
270
+
271
+
209
272
  func _reimport(paths: PackedStringArray) -> void:
210
273
  var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
211
274
  # A scan can begin between the answer and this frame, and its import would refuse this one.
@@ -3,10 +3,10 @@ extends Node
3
3
 
4
4
  ## Resource files, shaders, tilesets and themes, written through the open editor.
5
5
 
6
+ const FileReads = preload("../file_reads.gd")
6
7
  const PropertyValues = preload("../property_values.gd")
7
8
  const Read = preload("../reading.gd")
8
9
  const SceneFile = preload("../scene_file.gd")
9
- const Serialisation = preload("../serialisation.gd")
10
10
 
11
11
  # The minimal shader of a type: its declaration and the one function the engine calls for it.
12
12
  const SHADER_EMPTY: String = """shader_type %s;
@@ -18,9 +18,11 @@ const SHADER_ENTRY: Dictionary[String, String] = {
18
18
  "canvas_item": "fragment", "spatial": "fragment", "particles": "process", "sky": "sky", "fog": "fog"
19
19
  }
20
20
 
21
+ ## How many cells a read of a tile map lists, enough for a level's worth and not a whole world's.
22
+ const CELLS_SHOWN: int = 1000
23
+
21
24
  var _editor_plugin: EditorPlugin = null
22
25
  var _properties: PropertyValues = PropertyValues.new()
23
- var _values: Serialisation = Serialisation.new()
24
26
 
25
27
 
26
28
  func set_editor_plugin(plugin: EditorPlugin) -> void:
@@ -45,7 +47,7 @@ func _written(path: String) -> void:
45
47
  if ResourceLoader.has_cached(path):
46
48
  var _replaced: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_REPLACE)
47
49
  if _editor_plugin:
48
- EditorInterface.get_resource_filesystem().scan()
50
+ SceneFile.scan_later()
49
51
 
50
52
 
51
53
  ## Why a tool that creates [param path] must not write there, or "". Saving over a file that was
@@ -126,8 +128,8 @@ func _save_checked(resource: Resource, path: String, names: Array, previous: Var
126
128
  else:
127
129
  for key: Variant in names:
128
130
  var property: String = str(key)
129
- var read: Variant = loaded.get(property)
130
- if not PropertyValues.same(read, resource.get(property)):
131
+ var read: Variant = _properties.read_at(loaded, property)
132
+ if not PropertyValues.same(read, _properties.read_at(resource, property)):
131
133
  lost = (
132
134
  (
133
135
  "%s.%s would load from the file as %s: it is not a property the file keeps, so "
@@ -236,48 +238,12 @@ func modify_resource(args: Dictionary) -> Dictionary:
236
238
  return {"ok": true, "resourcePath": res_path}
237
239
 
238
240
 
239
- ## What a resource file holds, in the form modify takes, so a value read here can be written back.
240
- ## Read from the file rather than the editor's cached copy, since the file is what the other ops
241
- ## write and what the project loads.
241
+ ## What a resource file holds, read as a headless engine reads it with no editor connected.
242
242
  func read_resource(args: Dictionary) -> Dictionary:
243
243
  var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
244
- var include_defaults: bool = Read.as_bool(args.get("includeDefaults", false))
245
244
  if res_path == "res://":
246
245
  return {"ok": false, "error": "resourcePath is required"}
247
- if not FileAccess.file_exists(res_path):
248
- return {"ok": false, "error": "No resource at " + res_path}
249
- var resource: Resource = ResourceLoader.load(res_path, "", ResourceLoader.CACHE_MODE_IGNORE)
250
- if resource == null:
251
- return {"ok": false, "error": "%s did not load as a resource." % res_path}
252
-
253
- var script: Script = resource.get_script()
254
- var scripted: Dictionary = {}
255
- if script != null:
256
- for declared: Dictionary in script.get_script_property_list():
257
- scripted[str(declared.get("name", ""))] = true
258
- var properties: Dictionary = {}
259
- for p: Dictionary in resource.get_property_list():
260
- var property: String = str(p.get("name", ""))
261
- if property.is_empty() or property == "script":
262
- continue
263
- if not (Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
264
- continue
265
- var value: Variant = resource.get(property)
266
- if not include_defaults:
267
- var default: Variant = (
268
- script.get_property_default_value(property)
269
- if scripted.has(property)
270
- else ClassDB.class_get_property_default_value(resource.get_class(), property)
271
- )
272
- if PropertyValues.same(value, default):
273
- continue
274
- properties[property] = _values.serialize_value(value)
275
- var answer: Dictionary = {
276
- "ok": true, "resourcePath": res_path, "resourceType": resource.get_class(), "properties": properties
277
- }
278
- if script != null:
279
- answer["script"] = script.resource_path
280
- return answer
246
+ return FileReads.resource(res_path, Read.as_bool(args.get("includeDefaults", false)))
281
247
 
282
248
 
283
249
  func create_shader(args: Dictionary) -> Dictionary:
@@ -552,7 +518,13 @@ func _cell(entry: Variant, index: int, tile_set: TileSet) -> Array:
552
518
  var atlas_coords: Vector2i = atlas_at[1]
553
519
  var alternative: int = Read.as_int(cell.get("alternativeTile", 0))
554
520
  if not source.has_tile(atlas_coords):
555
- return [false, "Source %d has no tile at %s" % [source_id, atlas_coords]]
521
+ return [
522
+ false,
523
+ (
524
+ "cells[%d]: source %d has no tile at %s; it has %s. Nothing was placed."
525
+ % [index, source_id, atlas_coords, _tiles_of(source)]
526
+ )
527
+ ]
556
528
  if not source.has_alternative_tile(atlas_coords, alternative):
557
529
  return [
558
530
  false,
@@ -561,6 +533,72 @@ func _cell(entry: Variant, index: int, tile_set: TileSet) -> Array:
561
533
  return [true, at[1], source_id, atlas_coords, alternative]
562
534
 
563
535
 
536
+ ## The atlas coordinates [param source] has tiles at, the first twelve and how many more, or that it
537
+ ## has none: a refusal naming the one missing sent a caller guessing which of the others exist.
538
+ static func _tiles_of(source: TileSetSource) -> String:
539
+ var count: int = source.get_tiles_count()
540
+ if count == 0:
541
+ return "no tiles"
542
+ var named: Array[String] = []
543
+ for index: int in mini(count, 12):
544
+ named.append(str(source.get_tile_id(index)))
545
+ var more: String = "" if count <= 12 else " and %d more" % (count - 12)
546
+ return "tiles at " + ", ".join(PackedStringArray(named)) + more
547
+
548
+
549
+ ## The cells a TileMapLayer or a TileMap holds, as {"cellCount", "cells"} in the form
550
+ ## set_tilemap_cells takes, or {} for any other node. The scene keeps them as packed bytes, which a
551
+ ## read answered with and nobody could read: a probe counted three cells by decoding the base64 by
552
+ ## hand. At most CELLS_SHOWN are listed, the count saying how many there are.
553
+ static func cells_of(node: Node) -> Dictionary:
554
+ var listed: Array[Dictionary] = []
555
+ var count: int = 0
556
+ var single: TileMapLayer = node as TileMapLayer
557
+ var layered: TileMap = node as TileMap
558
+ if single != null:
559
+ var used: Array[Vector2i] = single.get_used_cells()
560
+ count = used.size()
561
+ for at: Vector2i in used.slice(0, CELLS_SHOWN):
562
+ listed.append(
563
+ _cell_entry(
564
+ at,
565
+ single.get_cell_source_id(at),
566
+ single.get_cell_atlas_coords(at),
567
+ single.get_cell_alternative_tile(at)
568
+ )
569
+ )
570
+ elif layered != null:
571
+ for layer: int in layered.get_layers_count():
572
+ var used: Array[Vector2i] = layered.get_used_cells(layer)
573
+ count += used.size()
574
+ for at: Vector2i in used:
575
+ if listed.size() >= CELLS_SHOWN:
576
+ break
577
+ var entry: Dictionary = _cell_entry(
578
+ at,
579
+ layered.get_cell_source_id(layer, at),
580
+ layered.get_cell_atlas_coords(layer, at),
581
+ layered.get_cell_alternative_tile(layer, at)
582
+ )
583
+ entry["layer"] = layer
584
+ listed.append(entry)
585
+ else:
586
+ return {}
587
+ var answer: Dictionary = {"cellCount": count, "cells": listed}
588
+ if count > listed.size():
589
+ answer["cellsNote"] = "The first %d of %d cells are listed." % [listed.size(), count]
590
+ return answer
591
+
592
+
593
+ static func _cell_entry(at: Vector2i, source_id: int, atlas: Vector2i, alternative: int) -> Dictionary:
594
+ return {
595
+ "coords": {"x": at.x, "y": at.y},
596
+ "sourceId": source_id,
597
+ "atlasCoords": {"x": atlas.x, "y": atlas.y},
598
+ "alternativeTile": alternative,
599
+ }
600
+
601
+
564
602
  func _cell_at(node: Node, layer: int, at: Vector2i) -> Array:
565
603
  var layered: TileMap = node as TileMap
566
604
  if layered != null:
@@ -0,0 +1,178 @@
1
+ @tool
2
+ extends RefCounted
3
+
4
+ ## What a new node in a scene is made or copied from, what the scene owns of it, and where among its
5
+ ## siblings a node may go.
6
+
7
+ const Read = preload("../reading.gd")
8
+ const PropertyValues = preload("../property_values.gd")
9
+
10
+
11
+ ## The scene to instance for an add, or null for a node of a class, beside why neither can be made
12
+ ## ("" when one can), as [PackedScene, String].
13
+ static func what_to_add(node_type: String, instance_path: String, scene_path: String) -> Array:
14
+ if node_type.is_empty() == instance_path.is_empty():
15
+ return [
16
+ null,
17
+ "add takes nodeType for a node of a class, or instancePath for a scene, and one of them only."
18
+ ]
19
+ if not instance_path.is_empty():
20
+ var cycle: String = _instancing_cycle(scene_path, instance_path)
21
+ if not cycle.is_empty():
22
+ return [null, cycle]
23
+ if not ResourceLoader.exists(instance_path, "PackedScene"):
24
+ return [null, "No scene at %s to instance." % instance_path]
25
+ var packed: PackedScene = ResourceLoader.load(instance_path, "PackedScene") as PackedScene
26
+ if packed == null or not packed.can_instantiate():
27
+ return [null, "%s does not load as a scene that can be instanced." % instance_path]
28
+ return [packed, ""]
29
+ if node_type.begins_with("res://") or node_type.get_extension() in ["tscn", "scn"]:
30
+ return [
31
+ null,
32
+ (
33
+ "nodeType must be a node class, and %s is a scene: pass it as instancePath to instance it."
34
+ % node_type
35
+ )
36
+ ]
37
+ if not ClassDB.class_exists(node_type) or not ClassDB.is_parent_class(node_type, "Node"):
38
+ return [null, "nodeType must be a node class, and %s is not one." % node_type]
39
+ if not ClassDB.can_instantiate(node_type):
40
+ return [null, "%s cannot be instantiated." % node_type]
41
+ return [null, ""]
42
+
43
+
44
+ ## A new node of [param node_type], or an instance of [param packed] when there is one. The instance
45
+ ## is made in the editor's edit state, so the pack writes it as an instance of its file, and owning
46
+ ## its root leaves its own nodes to the instanced scene.
47
+ static func made(packed: PackedScene, node_type: String) -> Node:
48
+ if packed != null:
49
+ return packed.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)
50
+ var node: Object = ClassDB.instantiate(node_type)
51
+ return node as Node
52
+
53
+
54
+ ## A copy of [param source] and the nodes under it that [param root] owns, each copy recorded in
55
+ ## [param copies] against its source, or null when an instance in it no longer loads.
56
+ static func copied(source: Node, root: Node, copies: Dictionary) -> Node:
57
+ var copy: Node = null
58
+ if not source.scene_file_path.is_empty():
59
+ # An instanced scene made again from its file, so the copy is an instance too. Node.duplicate
60
+ # makes a plain node of the instance's class, and the pack saved it with every property of the
61
+ # instance's root frozen into this scene.
62
+ var packed: PackedScene = ResourceLoader.load(source.scene_file_path, "PackedScene") as PackedScene
63
+ if packed == null:
64
+ return null
65
+ copy = packed.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)
66
+ for declared: Dictionary in source.get_property_list():
67
+ if not (Read.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
68
+ continue
69
+ var property: String = str(declared.get("name", ""))
70
+ var value: Variant = source.get(property)
71
+ if not PropertyValues.same(copy.get(property), value):
72
+ copy.set(property, value)
73
+ for group: StringName in source.get_groups():
74
+ if not copy.is_in_group(group):
75
+ copy.add_to_group(group, true)
76
+ else:
77
+ copy = source.duplicate(Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS)
78
+ for child: Node in copy.get_children():
79
+ copy.remove_child(child)
80
+ child.free()
81
+ copy.name = source.name
82
+ copies[source] = copy
83
+ # Only what this scene owns: an instance's own nodes come with the instance.
84
+ for child: Node in source.get_children():
85
+ if child.owner == root:
86
+ var under: Node = copied(child, root, copies)
87
+ if under == null:
88
+ return null
89
+ copy.add_child(under)
90
+ return copy
91
+
92
+
93
+ ## Gives each copy in [param copies] the persistent connections its source has, to the copy of the
94
+ ## target when the target was copied too. A connection an instance makes inside itself is skipped:
95
+ ## the instance made again from its file already has it.
96
+ static func reconnect(copies: Dictionary, top: Node) -> void:
97
+ for key: Variant in copies:
98
+ var source: Node = key
99
+ var copy: Node = copies[key]
100
+ for info: Dictionary in source.get_signal_list():
101
+ var signal_name: String = str(info.get("name", ""))
102
+ for conn: Dictionary in source.get_signal_connection_list(signal_name):
103
+ var flags: int = Read.as_int(conn.get("flags", 0))
104
+ var callable: Callable = conn.get("callable", Callable())
105
+ var target: Object = callable.get_object()
106
+ if not flags & Object.CONNECT_PERSIST or target == null:
107
+ continue
108
+ var to: Object = copies.get(target, target)
109
+ if to == target and target is Node and (target == top or top.is_ancestor_of(target as Node)):
110
+ continue
111
+ var aimed: Callable = Callable(to, callable.get_method())
112
+ if (
113
+ not copy.is_connected(signal_name, aimed)
114
+ and copy.connect(signal_name, aimed, flags) != OK
115
+ ):
116
+ push_error(
117
+ "gdharness: could not copy the connection %s of %s" % [signal_name, source.name]
118
+ )
119
+
120
+
121
+ ## Owns [param node] and the nodes under it to [param root], except those an instanced scene
122
+ ## inside it owns: giving those to the root made the pack write the instance's own nodes into this
123
+ ## scene as overrides.
124
+ static func own(node: Node, root: Node) -> void:
125
+ node.owner = root
126
+ for child: Node in node.get_children():
127
+ if child.owner == null or child.owner == root:
128
+ own(child, root)
129
+
130
+
131
+ ## Why [param asked] is not a position [param node] can take among [param parent]'s children, or ""
132
+ ## when it is, or when none was asked for. [param node] is null for a node not yet added.
133
+ static func unplaceable(parent: Node, node: Node, asked: Variant) -> String:
134
+ if asked == null:
135
+ return ""
136
+ var others: int = parent.get_child_count() - (1 if node != null and node.get_parent() == parent else 0)
137
+ var at: int = Read.as_int(asked, -1)
138
+ if at < 0 or at > others:
139
+ return (
140
+ "index %s is not a place among the %d other children of %s: give 0 to %d, 0 for first."
141
+ % [
142
+ str(at) if asked is int or asked is float else JSON.stringify(asked),
143
+ others,
144
+ parent.name,
145
+ others
146
+ ]
147
+ )
148
+ return ""
149
+
150
+
151
+ ## Moves [param node] to [param asked] among its siblings, when a place was asked for.
152
+ static func place(node: Node, asked: Variant) -> void:
153
+ if asked != null:
154
+ node.get_parent().move_child(node, Read.as_int(asked))
155
+
156
+
157
+ ## Why [param instanced] cannot be instanced into [param scene_path], or "" when it can: a scene
158
+ ## inside itself, directly or through the scenes it instances, which Godot's own dialog refuses and
159
+ ## which never finishes loading.
160
+ static func _instancing_cycle(scene_path: String, instanced: String) -> String:
161
+ var seen: Dictionary[String, bool] = {}
162
+ var waiting: Array[String] = [instanced]
163
+ while not waiting.is_empty():
164
+ var at: String = waiting.pop_back()
165
+ if at == scene_path:
166
+ return (
167
+ "%s cannot be instanced into %s: it %s, so the scene would hold itself."
168
+ % [instanced, scene_path, "is that scene" if at == instanced else "instances it"]
169
+ )
170
+ if seen.has(at):
171
+ continue
172
+ seen[at] = true
173
+ for dependency: String in ResourceLoader.get_dependencies(at):
174
+ # Slices joined by "::", the UID, the type and the path, some of them empty.
175
+ for part: String in dependency.split("::"):
176
+ if part.begins_with("res://") and part.get_extension() in ["tscn", "scn"]:
177
+ waiting.append(part)
178
+ return ""
@@ -0,0 +1 @@
1
+ uid://bgnjhedq2qj3t