gdharness 1.2.31 → 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 (27) hide show
  1. package/build/cli.js +7212 -6230
  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/scene_file.gd +38 -1
  7. package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
  8. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +22 -0
  9. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +64 -1
  10. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +4 -41
  11. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +178 -0
  12. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd.uid +1 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +54 -120
  14. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +39 -0
  15. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +285 -0
  16. package/build/godot/addons/gdharness_runtime/runtime_hover.gd.uid +1 -0
  17. package/build/godot/addons/gdharness_runtime/runtime_input.gd +151 -106
  18. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +112 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd.uid +1 -0
  20. package/build/godot/operations/file_reads.gd +237 -0
  21. package/build/godot/operations/gdscript_authoring.gd +136 -19
  22. package/build/godot/operations/godot_operations.gd +24 -0
  23. package/build/godot/operations/property_values.gd +767 -0
  24. package/build/godot/operations/rename_checks.gd +5 -2
  25. package/build/index.js +5437 -4469
  26. package/build/keeper.js +184 -71
  27. package/package.json +1 -1
@@ -517,6 +517,20 @@ func _handle_message(json_string: String) -> void:
517
517
  pass
518
518
 
519
519
 
520
+ ## Says [param tool_name] was not run because the editor is scanning or importing, for the server to
521
+ ## ask again once that is over. An older server reads it as an ordinary refusal.
522
+ func send_busy(request_id: String, tool_name: String) -> void:
523
+ _send_message(
524
+ {
525
+ "type": "tool_result",
526
+ "id": request_id,
527
+ "success": false,
528
+ "busy": true,
529
+ "error": "The editor is scanning or importing its files, so %s was not run." % tool_name,
530
+ }
531
+ )
532
+
533
+
520
534
  func send_tool_result(request_id: String, success: bool, result: Variant = null, error: String = "") -> void:
521
535
  var response: Dictionary = {"type": "tool_result", "id": request_id, "success": success}
522
536
 
@@ -0,0 +1,237 @@
1
+ extends RefCounted
2
+
3
+ ## Scenes and resources read from their files, by the editor's tools and by a headless engine with
4
+ ## none connected, so a read answers the same either way.
5
+ ##
6
+ ## A scene is read off its SceneState rather than instantiated. Instantiating outside the editor
7
+ ## runs every script's member initialisers and _init (measured on 4.7.2, with or without -e), so a
8
+ ## read with no editor would have run the game's code on every node; the editor gets placeholders
9
+ ## and runs none. The state holds everything the file says, and the scenes it instances theirs,
10
+ ## without making a node. Loading the file still does what loading does in the game: static
11
+ ## initialisers run, and a resource with a script is made with it.
12
+
13
+ const PropertyValues = preload("property_values.gd")
14
+ const Read = preload("reading.gd")
15
+ const Serialisation = preload("serialisation.gd")
16
+
17
+
18
+ ## The nodes of [param scene_path] as {name, type, path, children}, with instanceOf on a node that
19
+ ## instances another scene and every stored property when [param include_properties] is set, the
20
+ ## file's value or the default where it sets none. [param depth] -1 reads every level.
21
+ static func scene_tree(scene_path: String, depth: int, include_properties: bool) -> Dictionary:
22
+ if not FileAccess.file_exists(scene_path):
23
+ return {"ok": false, "error": "Scene not found: " + scene_path}
24
+ # Loaded without naming the type, so a resource handed in by mistake loads as what it is and the
25
+ # refusal can say so, where asking for a PackedScene printed the loader's error and a backtrace.
26
+ var loaded: Resource = ResourceLoader.load(scene_path, "", ResourceLoader.CACHE_MODE_IGNORE)
27
+ if loaded == null:
28
+ return {"ok": false, "error": "Failed to load %s as a scene" % scene_path}
29
+ if not loaded is PackedScene:
30
+ return {
31
+ "ok": false,
32
+ "error":
33
+ (
34
+ "%s is a %s, not a scene: resource_edit read reads its properties."
35
+ % [scene_path, loaded.get_class()]
36
+ )
37
+ }
38
+ var packed: PackedScene = loaded
39
+ var nodes: Dictionary = {}
40
+ var refused: String = _lay(packed.get_state(), ".", nodes, [scene_path])
41
+ if not refused.is_empty():
42
+ return {"ok": false, "error": refused}
43
+ if not nodes.has("."):
44
+ return {"ok": false, "error": "%s holds no nodes" % scene_path}
45
+ return {"ok": true, "tree": _entry(nodes, ".", include_properties, depth, 0)}
46
+
47
+
48
+ ## Lays the nodes of [param state] into [param nodes] under [param prefix], keyed by their path
49
+ ## from the outermost root, each instanced scene laid where it is instanced and the instancing
50
+ ## file's own entries for it applied over it. Answers a refusal, or "".
51
+ ##
52
+ ## [param within] is the files being laid, outermost first, so a scene that instances one of them
53
+ ## is refused rather than laid forever; the engine refuses to load such a scene, so this is only
54
+ ## reached through a state built some other way.
55
+ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: Array) -> String:
56
+ var base: SceneState = state.get_base_scene_state()
57
+ if base != null and not nodes.has(prefix):
58
+ var refused: String = _lay(base, prefix, nodes, within)
59
+ if not refused.is_empty():
60
+ return refused
61
+ for index: int in state.get_node_count():
62
+ var path: String = _joined(prefix, str(state.get_node_path(index)))
63
+ var fresh: bool = not nodes.has(path)
64
+ if fresh:
65
+ var instance: PackedScene = state.get_node_instance(index)
66
+ if instance != null:
67
+ if within.has(instance.resource_path):
68
+ return "%s instances %s, which holds it" % [within.back(), instance.resource_path]
69
+ var refused: String = _lay(
70
+ instance.get_state(), path, nodes, within + [instance.resource_path]
71
+ )
72
+ if not refused.is_empty():
73
+ return refused
74
+ if not nodes.has(path):
75
+ return "%s holds no nodes" % instance.resource_path
76
+ nodes[path]["instanceOf"] = instance.resource_path
77
+ elif state.is_node_instance_placeholder(index):
78
+ nodes[path] = _made("InstancePlaceholder")
79
+ nodes[path]["instanceOf"] = state.get_node_instance_placeholder(index)
80
+ elif str(state.get_node_type(index)).is_empty():
81
+ # An entry for a node of an instance that no longer has it, which the engine skips too.
82
+ continue
83
+ else:
84
+ nodes[path] = _made(str(state.get_node_type(index)))
85
+ # The name is the state's even for an entry laid already, since an inherited scene names its
86
+ # root itself; a root instanced inside another scene is renamed by that scene once laid.
87
+ nodes[path]["name"] = str(state.get_node_name(index))
88
+ if fresh:
89
+ # The root of what is laid here is placed by whoever instanced it.
90
+ if path != prefix:
91
+ _place(
92
+ nodes,
93
+ _joined(prefix, str(state.get_node_path(index, true))),
94
+ path,
95
+ state.get_node_index(index)
96
+ )
97
+ var held: Dictionary = nodes[path]["properties"]
98
+ for property: int in state.get_node_property_count(index):
99
+ held[str(state.get_node_property_name(index, property))] = state.get_node_property_value(
100
+ index, property
101
+ )
102
+ return ""
103
+
104
+
105
+ static func _made(type: String) -> Dictionary:
106
+ var children: Array[String] = []
107
+ return {"name": "", "type": type, "properties": {}, "children": children}
108
+
109
+
110
+ ## Puts [param path] among [param parent]'s children, at [param at] when the file says where (a node
111
+ ## added inside an instance) and last when it does not.
112
+ static func _place(nodes: Dictionary, parent: String, path: String, at: int) -> void:
113
+ if not nodes.has(parent):
114
+ return
115
+ var children: Array[String] = nodes[parent]["children"]
116
+ if at >= 0 and at < children.size():
117
+ var _placed: int = children.insert(at, path)
118
+ else:
119
+ children.append(path)
120
+
121
+
122
+ ## [param given], a path as a SceneState gives it ("." or "./Body/Tail"), under [param prefix].
123
+ static func _joined(prefix: String, given: String) -> String:
124
+ var relative: String = given.trim_prefix("./")
125
+ if relative == "." or relative.is_empty():
126
+ return prefix
127
+ if prefix == ".":
128
+ return relative
129
+ return prefix + "/" + relative
130
+
131
+
132
+ static func _entry(
133
+ nodes: Dictionary, path: String, include_properties: bool, depth: int, current_depth: int
134
+ ) -> Dictionary:
135
+ var node: Dictionary = nodes[path]
136
+ var children: Array[Dictionary] = []
137
+ var data: Dictionary = {"name": node["name"], "type": node["type"], "path": path, "children": children}
138
+ # The root's file is the scene being read, so only another node's names a second scene.
139
+ if path != "." and node.has("instanceOf"):
140
+ data["instanceOf"] = node["instanceOf"]
141
+ if include_properties:
142
+ var given: Dictionary = node["properties"]
143
+ data["properties"] = _stored(str(node["type"]), given)
144
+ if depth >= 0 and current_depth >= depth:
145
+ return data
146
+ for child: String in node["children"]:
147
+ children.append(_entry(nodes, child, include_properties, depth, current_depth + 1))
148
+ return data
149
+
150
+
151
+ ## Every property a node of [param type] stores, with the value [param given] sets or the default,
152
+ ## read as a node holding those values would answer them.
153
+ ##
154
+ ## The engine's half is read off a bare node of the class with the file's values set on it, which
155
+ ## runs no script: what a node lists depends on its values (a thread group's settings appear only
156
+ ## once it has one) and some setters set others (a Control's anchors preset sets how it grows), so
157
+ ## the class's defaults and the file's entries alone answered neither. The script's half is its
158
+ ## declared defaults under the file's values.
159
+ static func _stored(type: String, given: Dictionary) -> Dictionary:
160
+ var attached: Variant = given.get("script")
161
+ var script: Script = attached if attached is Script else null
162
+ var scripted: Dictionary = {}
163
+ if script != null:
164
+ for declared: Dictionary in script.get_script_property_list():
165
+ var property: String = str(declared.get("name", ""))
166
+ if not property.is_empty() and Read.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
167
+ scripted[property] = script.get_property_default_value(property)
168
+ var values: Dictionary = {}
169
+ if ClassDB.class_exists(type) and ClassDB.can_instantiate(type):
170
+ var bare: Object = ClassDB.instantiate(type)
171
+ for property: Variant in given:
172
+ if property != "script" and not scripted.has(property):
173
+ bare.set(str(property), given[property])
174
+ for declared: Dictionary in bare.get_property_list():
175
+ var property: String = str(declared.get("name", ""))
176
+ if not property.is_empty() and Read.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
177
+ values[property] = bare.get(property)
178
+ if bare is Node:
179
+ bare.free()
180
+ for property: String in scripted:
181
+ values[property] = scripted[property]
182
+ # Given last, which keeps what a bare node cannot hold: the script itself, and a path to a node
183
+ # for a property that holds the node once it is in a tree.
184
+ values.merge(given, true)
185
+ var serialiser: Serialisation = Serialisation.new()
186
+ var answered: Dictionary = {}
187
+ for property: Variant in values:
188
+ answered[property] = serialiser.serialize_value(values[property])
189
+ return answered
190
+
191
+
192
+ ## What a resource file holds, in the form modify takes, so a value read here can be written back.
193
+ ## Read from the file rather than a cached copy, since the file is what the writes change and what
194
+ ## the project loads.
195
+ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
196
+ if not FileAccess.file_exists(res_path):
197
+ return {"ok": false, "error": "No resource at " + res_path}
198
+ var loaded: Resource = ResourceLoader.load(res_path, "", ResourceLoader.CACHE_MODE_IGNORE)
199
+ if loaded == null:
200
+ return {"ok": false, "error": "%s did not load as a resource." % res_path}
201
+ # A scene's own properties are its packed bundle, which says nothing a caller can use.
202
+ if loaded is PackedScene:
203
+ return {
204
+ "ok": false,
205
+ "error":
206
+ "%s is a scene: scene_tree reads its nodes, and scene_node get one node's properties." % res_path
207
+ }
208
+
209
+ var script: Script = loaded.get_script()
210
+ var scripted: Dictionary = {}
211
+ if script != null:
212
+ for declared: Dictionary in script.get_script_property_list():
213
+ scripted[str(declared.get("name", ""))] = true
214
+ var serialiser: Serialisation = Serialisation.new()
215
+ var properties: Dictionary = {}
216
+ for p: Dictionary in loaded.get_property_list():
217
+ var property: String = str(p.get("name", ""))
218
+ if property.is_empty() or property == "script":
219
+ continue
220
+ if not (Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
221
+ continue
222
+ var value: Variant = loaded.get(property)
223
+ if not include_defaults:
224
+ var default: Variant = (
225
+ script.get_property_default_value(property)
226
+ if scripted.has(property)
227
+ else ClassDB.class_get_property_default_value(loaded.get_class(), property)
228
+ )
229
+ if PropertyValues.same(value, default):
230
+ continue
231
+ properties[property] = serialiser.serialize_value(value)
232
+ var answer: Dictionary = {
233
+ "ok": true, "resourcePath": res_path, "resourceType": loaded.get_class(), "properties": properties
234
+ }
235
+ if script != null:
236
+ answer["script"] = script.resource_path
237
+ return answer
@@ -0,0 +1 @@
1
+ uid://c1lf7gjr8v1ux
@@ -8,6 +8,17 @@ extends EditorPlugin
8
8
  const BridgeClient = preload("bridge_client.gd")
9
9
  const ToolExecutor = preload("tool_executor.gd")
10
10
 
11
+ ## The tools answered while the editor scans or imports, because none of them saves a file: the
12
+ ## ones a caller waits on a scan with, and the ones that only read or stop a played game.
13
+ const ANSWERED_WHILE_SCANNING: Array[String] = [
14
+ "scan_status",
15
+ "rescan_filesystem",
16
+ "reimport_files",
17
+ "playing_status",
18
+ "stop_playing",
19
+ "profile_read",
20
+ ]
21
+
11
22
  var _client: BridgeClient
12
23
  var _tool_executor: ToolExecutor
13
24
  var _status_label: Label
@@ -85,10 +96,26 @@ func _on_disconnected() -> void:
85
96
  _status_label.add_theme_color_override("font_color", Color.RED)
86
97
 
87
98
 
99
+ ## Whether [param tool_name] waits for the editor to finish scanning or importing. A save during a
100
+ ## scan updates the file list the scan's thread is writing out (see SceneFile.scan_later), and an
101
+ ## import can run frames of the editor from inside itself, the one handling this call among them.
102
+ func turned_away(tool_name: String) -> bool:
103
+ var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
104
+ return (
105
+ not ANSWERED_WHILE_SCANNING.has(tool_name) and (filesystem.is_scanning() or filesystem.is_importing())
106
+ )
107
+
108
+
88
109
  func _on_tool_requested(request_id: String, tool_name: String, args: Dictionary) -> void:
89
110
  if _tool_executor == null or _client == null:
90
111
  return
91
112
 
113
+ # Turned away rather than held, so the server asks again: a call held here could run after the
114
+ # server had given up on it.
115
+ if turned_away(tool_name):
116
+ _client.send_busy(request_id, tool_name)
117
+ return
118
+
92
119
  var result: Dictionary = _tool_executor.execute_tool(tool_name, args)
93
120
  var success: bool = result.get("ok", false)
94
121
 
@@ -19,6 +19,10 @@ extends RefCounted
19
19
 
20
20
  const PropertyValues = preload("property_values.gd")
21
21
 
22
+ ## Whether a scan asked for through [method scan_later] is still to start, which a caller waiting
23
+ ## for the editor's files to settle has to wait through as well.
24
+ static var scan_due: bool = false
25
+
22
26
  var root: Node = null
23
27
  var path: String = ""
24
28
  var _base_paths: Dictionary = {}
@@ -249,11 +253,44 @@ static func save_packed(packed: PackedScene, scene_path: String) -> Dictionary:
249
253
  static func refresh(scene_path: String) -> void:
250
254
  if ResourceLoader.has_cached(scene_path):
251
255
  var _replaced: Resource = ResourceLoader.load(scene_path, "", ResourceLoader.CACHE_MODE_REPLACE)
252
- EditorInterface.get_resource_filesystem().scan()
256
+ scan_later()
253
257
  if EditorInterface.get_open_scenes().has(scene_path):
254
258
  EditorInterface.reload_scene_from_path(scene_path)
255
259
 
256
260
 
261
+ ## Has the editor scan its files once the call in hand has answered, rather than now.
262
+ ##
263
+ ## A scan's thread ends by writing the editor's file list to its cache, walking the list as it goes,
264
+ ## while each save made in the editor updates an entry of that list on the main thread and frees
265
+ ## what the entry held (4.7.2's [code]_save_filesystem_cache[/code] and
266
+ ## [code]EditorNode::_resource_saved[/code]). A scene saved 250 ms after a resource's scan began was
267
+ ## followed by garbage decoded as text and a dropped connection. Started at the end of the frame,
268
+ ## the scan begins after every save the call makes, and the plugin turns away calls that could save
269
+ ## until it is over.
270
+ static func scan_later() -> void:
271
+ if scan_due:
272
+ return
273
+ scan_due = true
274
+ _start_scan.call_deferred()
275
+
276
+
277
+ ## [method EditorFileSystem.scan] does nothing, and says nothing, while the thread of the scan
278
+ ## before it is still to be joined, which is a frame or two after that scan stops reporting itself,
279
+ ## so a scan that leaves the editor not scanning is asked for once more once that is over. Asked of
280
+ ## a scan that ended within the call, the second costs one more scan.
281
+ static func _start_scan() -> void:
282
+ var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
283
+ var tree: SceneTree = Engine.get_main_loop()
284
+ while filesystem.is_scanning() or filesystem.is_importing():
285
+ await tree.process_frame
286
+ filesystem.scan()
287
+ if not filesystem.is_scanning():
288
+ await tree.process_frame
289
+ await tree.process_frame
290
+ filesystem.scan()
291
+ scan_due = false
292
+
293
+
257
294
  ## Whether the editor holds unsaved changes to [param scene_path], for a read to say that the file
258
295
  ## it answers from is not what the editor shows.
259
296
  static func unsaved(scene_path: String) -> bool:
@@ -95,6 +95,7 @@ func _init_tools() -> void:
95
95
  "rescan_filesystem": [_filesystem_tools, "rescan_filesystem"],
96
96
  "scan_status": [_filesystem_tools, "scan_status"],
97
97
  "reimport_files": [_filesystem_tools, "reimport_files"],
98
+ "files_moved": [_filesystem_tools, "files_moved"],
98
99
  "global_classes": [_class_tools, "global_classes"],
99
100
  "reload_script": [_class_tools, "reload_script"],
100
101
  # Settings, for a caller that asked the editor rather than the file on disk
@@ -117,9 +117,31 @@ func reload_script(args: Dictionary) -> Dictionary:
117
117
  answer["restoredAs"] = error_string(restored)
118
118
  answer["methods"] = _method_names(script)
119
119
  answer["constants"] = _constant_names(script)
120
+ _reread_open_tabs(path, answer)
120
121
  return answer
121
122
 
122
123
 
124
+ ## Has the script editor's tabs reread what changed on disk, as the editor does when its window takes
125
+ ## the focus, which a hidden editor's never does. A tab with unsaved edits is left alone and named:
126
+ ## saving it writes its text over the file, and rereading would ask a question nobody can answer in a
127
+ ## hidden editor's dialog.
128
+ func _reread_open_tabs(path: String, answer: Dictionary) -> void:
129
+ # The engine fixtures reload from a script run outside any editor, where there are no tabs and
130
+ # EditorInterface has none of its methods.
131
+ if not Engine.is_editor_hint():
132
+ return
133
+ var scripts: ScriptEditor = EditorInterface.get_script_editor()
134
+ var unsaved: PackedStringArray = scripts.get_unsaved_files()
135
+ if path in unsaved:
136
+ answer["unsavedInEditor"] = true
137
+ return
138
+ var rereads: Variant = EditorInterface.get_editor_settings().get_setting(
139
+ "text_editor/behavior/files/auto_reload_scripts_on_external_change"
140
+ )
141
+ if unsaved.is_empty() and rereads is bool and rereads:
142
+ scripts.reload_open_files()
143
+
144
+
123
145
  ## [param path] as the files on disk spell it, a step at a time, or as given where a step is not
124
146
  ## there. On a filesystem that ignores case, `res://Core/Rules.gd` opens the file at
125
147
  ## `res://core/rules.gd`, but the editor holds its copy under the spelling on disk, and a load by
@@ -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;
@@ -23,7 +23,6 @@ const CELLS_SHOWN: int = 1000
23
23
 
24
24
  var _editor_plugin: EditorPlugin = null
25
25
  var _properties: PropertyValues = PropertyValues.new()
26
- var _values: Serialisation = Serialisation.new()
27
26
 
28
27
 
29
28
  func set_editor_plugin(plugin: EditorPlugin) -> void:
@@ -48,7 +47,7 @@ func _written(path: String) -> void:
48
47
  if ResourceLoader.has_cached(path):
49
48
  var _replaced: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_REPLACE)
50
49
  if _editor_plugin:
51
- EditorInterface.get_resource_filesystem().scan()
50
+ SceneFile.scan_later()
52
51
 
53
52
 
54
53
  ## Why a tool that creates [param path] must not write there, or "". Saving over a file that was
@@ -239,48 +238,12 @@ func modify_resource(args: Dictionary) -> Dictionary:
239
238
  return {"ok": true, "resourcePath": res_path}
240
239
 
241
240
 
242
- ## What a resource file holds, in the form modify takes, so a value read here can be written back.
243
- ## Read from the file rather than the editor's cached copy, since the file is what the other ops
244
- ## write and what the project loads.
241
+ ## What a resource file holds, read as a headless engine reads it with no editor connected.
245
242
  func read_resource(args: Dictionary) -> Dictionary:
246
243
  var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
247
- var include_defaults: bool = Read.as_bool(args.get("includeDefaults", false))
248
244
  if res_path == "res://":
249
245
  return {"ok": false, "error": "resourcePath is required"}
250
- if not FileAccess.file_exists(res_path):
251
- return {"ok": false, "error": "No resource at " + res_path}
252
- var resource: Resource = ResourceLoader.load(res_path, "", ResourceLoader.CACHE_MODE_IGNORE)
253
- if resource == null:
254
- return {"ok": false, "error": "%s did not load as a resource." % res_path}
255
-
256
- var script: Script = resource.get_script()
257
- var scripted: Dictionary = {}
258
- if script != null:
259
- for declared: Dictionary in script.get_script_property_list():
260
- scripted[str(declared.get("name", ""))] = true
261
- var properties: Dictionary = {}
262
- for p: Dictionary in resource.get_property_list():
263
- var property: String = str(p.get("name", ""))
264
- if property.is_empty() or property == "script":
265
- continue
266
- if not (Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
267
- continue
268
- var value: Variant = resource.get(property)
269
- if not include_defaults:
270
- var default: Variant = (
271
- script.get_property_default_value(property)
272
- if scripted.has(property)
273
- else ClassDB.class_get_property_default_value(resource.get_class(), property)
274
- )
275
- if PropertyValues.same(value, default):
276
- continue
277
- properties[property] = _values.serialize_value(value)
278
- var answer: Dictionary = {
279
- "ok": true, "resourcePath": res_path, "resourceType": resource.get_class(), "properties": properties
280
- }
281
- if script != null:
282
- answer["script"] = script.resource_path
283
- return answer
246
+ return FileReads.resource(res_path, Read.as_bool(args.get("includeDefaults", false)))
284
247
 
285
248
 
286
249
  func create_shader(args: Dictionary) -> Dictionary: