gdharness 1.3.67 → 1.3.69
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 +33533 -32349
- package/build/godot/addons/gdharness_editor/bridge_client.gd +5 -3
- package/build/godot/addons/gdharness_editor/file_reads.gd +3 -0
- package/build/godot/addons/gdharness_editor/headless_check.gd +52 -0
- package/build/godot/addons/gdharness_editor/plugin.gd +80 -2
- package/build/godot/addons/gdharness_editor/scene_file.gd +192 -12
- package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +3 -1
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +9 -13
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +87 -13
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +20 -4
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +43 -11
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +520 -9
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +2 -0
- package/build/godot/operations/editor_tools.gd +1 -0
- package/build/godot/operations/file_reads.gd +3 -0
- package/build/keeper.js +116 -114
- package/package.json +1 -1
|
@@ -573,15 +573,17 @@ func _handle_message(json_string: String) -> void:
|
|
|
573
573
|
|
|
574
574
|
## Says [param tool_name] was not run because the editor is scanning or importing, for the server to
|
|
575
575
|
## ask again once that is over.
|
|
576
|
-
|
|
576
|
+
## [param doing] is what the editor is busy with, and [param mark] changes as it gets on with it.
|
|
577
|
+
func send_busy(request_id: String, tool_name: String, doing: String, mark: String) -> void:
|
|
577
578
|
_send_message(
|
|
578
579
|
{
|
|
579
580
|
"type": "tool_result",
|
|
580
581
|
"id": request_id,
|
|
581
582
|
"success": false,
|
|
582
583
|
"busy": true,
|
|
583
|
-
"
|
|
584
|
-
"
|
|
584
|
+
"busyWith": doing,
|
|
585
|
+
"progress": mark,
|
|
586
|
+
"error": "The editor is %s, so %s was not run." % [doing, tool_name],
|
|
585
587
|
}
|
|
586
588
|
)
|
|
587
589
|
|
|
@@ -186,6 +186,7 @@ static func _lay(
|
|
|
186
186
|
elif state.is_node_instance_placeholder(index):
|
|
187
187
|
nodes[path] = _made("InstancePlaceholder")
|
|
188
188
|
nodes[path]["instanceOf"] = state.get_node_instance_placeholder(index)
|
|
189
|
+
nodes[path]["placeholder"] = true
|
|
189
190
|
elif str(state.get_node_type(index)).is_empty():
|
|
190
191
|
# An entry for a node of an instance that no longer has it, which the engine skips
|
|
191
192
|
# too.
|
|
@@ -367,6 +368,8 @@ static func _entry(
|
|
|
367
368
|
data["instanceOf"] = node["instanceOf"]
|
|
368
369
|
if node.has("editableChildren"):
|
|
369
370
|
data["editableChildren"] = node["editableChildren"]
|
|
371
|
+
if node.has("placeholder"):
|
|
372
|
+
data["placeholder"] = true
|
|
370
373
|
# An inherited scene's file holds only what it changes, so the read says what it inherits and
|
|
371
374
|
# which nodes come from there, as the editor greys them.
|
|
372
375
|
if node.has("inherits"):
|
|
@@ -66,6 +66,16 @@ static func through_script(object: Object, names: Dictionary) -> bool:
|
|
|
66
66
|
return false
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
## Sets [param name] on [param object] back to [param value]. A metadata entry put back to nothing
|
|
70
|
+
## is removed. Set to null through its property, Godot keeps the entry holding null, and the saver
|
|
71
|
+
## writes a line that erases what the node's script sets each time the scene loads.
|
|
72
|
+
static func restore(object: Object, name: String, value: Variant) -> void:
|
|
73
|
+
if value == null and name.begins_with("metadata/"):
|
|
74
|
+
object.remove_meta(name.trim_prefix("metadata/"))
|
|
75
|
+
return
|
|
76
|
+
object.set(name, value)
|
|
77
|
+
|
|
78
|
+
|
|
69
79
|
## Whether [param object] still holds [param held] for [param name] once set to [param wanted]
|
|
70
80
|
## because the engine derives it, as it keeps layout_mode at Container under a container: an engine
|
|
71
81
|
## property, so no script code ran for the set, and an editor saves the same.
|
|
@@ -574,3 +584,45 @@ class GdhSaved:
|
|
|
574
584
|
path = at
|
|
575
585
|
before = held
|
|
576
586
|
resave = again
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
## Every script that failed to compile while it is attached, as {file, line, message} in order.
|
|
590
|
+
class GdhParseErrors:
|
|
591
|
+
extends Logger
|
|
592
|
+
|
|
593
|
+
var raised: Array[Dictionary] = []
|
|
594
|
+
|
|
595
|
+
func _log_message(_message: String, _error: bool) -> void:
|
|
596
|
+
pass
|
|
597
|
+
|
|
598
|
+
# The engine's own signature for a Logger, eight parameters and not this project's to shape.
|
|
599
|
+
# gdstyle:ignore=quality/max-parameters
|
|
600
|
+
func _log_error(
|
|
601
|
+
_function: String,
|
|
602
|
+
file: String,
|
|
603
|
+
line: int,
|
|
604
|
+
code: String,
|
|
605
|
+
_rationale: String,
|
|
606
|
+
_editor_notify: bool,
|
|
607
|
+
error_type: int,
|
|
608
|
+
_script_backtraces: Array[ScriptBacktrace],
|
|
609
|
+
) -> void:
|
|
610
|
+
if error_type == Logger.ERROR_TYPE_SCRIPT and code.begins_with("Parse Error: "):
|
|
611
|
+
raised.append(
|
|
612
|
+
{ "file": file, "line": line, "message": code.trim_prefix("Parse Error: ") }
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
## Why a write is refused for what was raised, or "" when nothing was.
|
|
616
|
+
func refusal(scene_path: String) -> String:
|
|
617
|
+
if raised.is_empty():
|
|
618
|
+
return ""
|
|
619
|
+
var first: Dictionary = raised[0]
|
|
620
|
+
return (
|
|
621
|
+
(
|
|
622
|
+
"%s does not compile in a headless engine (line %d: %s), and it loads with %s, "
|
|
623
|
+
+ "so the scene's scripts would not run as they do in the game. A class_name added "
|
|
624
|
+
+ "since the class cache was built is taken in by project_import refresh_classes. "
|
|
625
|
+
+ "Fix the script or refresh the classes, then ask again."
|
|
626
|
+
)
|
|
627
|
+
% [first["file"], first["line"], first["message"], scene_path]
|
|
628
|
+
)
|
|
@@ -7,6 +7,7 @@ extends EditorPlugin
|
|
|
7
7
|
const GdhBridgeClient = preload("bridge_client.gd")
|
|
8
8
|
const GdhToolExecutor = preload("tool_executor.gd")
|
|
9
9
|
const GdhReleaseExport = preload("release_export.gd")
|
|
10
|
+
const GdhSceneFile = preload("scene_file.gd")
|
|
10
11
|
## The tools answered while the editor scans or imports, because none of them saves a file: the
|
|
11
12
|
## ones a caller waits on a scan with, and the ones that only read or stop a played game.
|
|
12
13
|
const ANSWERED_WHILE_SCANNING: Array[String] = [
|
|
@@ -17,11 +18,29 @@ const ANSWERED_WHILE_SCANNING: Array[String] = [
|
|
|
17
18
|
"stop_playing",
|
|
18
19
|
"profile_read",
|
|
19
20
|
]
|
|
21
|
+
## The tools answered inside a task of the editor's progress dialog, because none of them writes
|
|
22
|
+
## anything the editor's task could be in the middle of.
|
|
23
|
+
const ANSWERED_DURING_A_TASK: Array[String] = [
|
|
24
|
+
"scan_status",
|
|
25
|
+
"playing_status",
|
|
26
|
+
"stop_playing",
|
|
27
|
+
"profile_read",
|
|
28
|
+
]
|
|
29
|
+
## What an editor opened to quit once a process has gone carries after `--`, with that pid after it.
|
|
30
|
+
## Kept in step with `UNTIL_MARK` in src/launch.ts.
|
|
31
|
+
const UNTIL_MARK: String = "--gdharness-until="
|
|
32
|
+
## Where the holder of such an editor asks it to save and quit, a file named for its pid. Kept in
|
|
33
|
+
## step with `QUITS_FOLDER` in src/editor-open.ts.
|
|
34
|
+
const QUITS_FOLDER: String = "res://.godot/gdharness-editor-quits/"
|
|
35
|
+
## How often such an editor looks for the request, in seconds.
|
|
36
|
+
const QUIT_LOOK_S: float = 1.0
|
|
20
37
|
|
|
21
38
|
var _client: GdhBridgeClient
|
|
22
39
|
var _tool_executor: GdhToolExecutor
|
|
23
40
|
var _status_label: Label
|
|
24
41
|
var _release_export: GdhReleaseExport
|
|
42
|
+
var _quit_look: Timer
|
|
43
|
+
var _loaded_at: int = 0
|
|
25
44
|
|
|
26
45
|
|
|
27
46
|
func _enter_tree() -> void:
|
|
@@ -43,6 +62,7 @@ func _enter_tree() -> void:
|
|
|
43
62
|
|
|
44
63
|
_setup_status_indicator()
|
|
45
64
|
_client.connect_to_server()
|
|
65
|
+
_look_for_a_quit_request()
|
|
46
66
|
|
|
47
67
|
|
|
48
68
|
func _exit_tree() -> void:
|
|
@@ -71,6 +91,41 @@ func _exit_tree() -> void:
|
|
|
71
91
|
_status_label = null
|
|
72
92
|
|
|
73
93
|
|
|
94
|
+
## Looks for its holder's request once a second, in an editor opened to quit once a process has
|
|
95
|
+
## gone. Godot cannot watch that process itself, since `OS.is_process_running` answers false for any
|
|
96
|
+
## pid it did not start, so a gdharness holder outside the editor watches it and writes the request.
|
|
97
|
+
func _look_for_a_quit_request() -> void:
|
|
98
|
+
var held: bool = false
|
|
99
|
+
for arg: String in OS.get_cmdline_user_args():
|
|
100
|
+
held = held or arg.begins_with(UNTIL_MARK)
|
|
101
|
+
if not held:
|
|
102
|
+
return
|
|
103
|
+
_loaded_at = int(Time.get_unix_time_from_system())
|
|
104
|
+
_quit_look = Timer.new()
|
|
105
|
+
_quit_look.wait_time = QUIT_LOOK_S
|
|
106
|
+
_quit_look.autostart = true
|
|
107
|
+
_listen(_quit_look.timeout, _on_quit_look)
|
|
108
|
+
add_child(_quit_look)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
## Saves and quits on a request written since this addon loaded, as quit_editor does. One older
|
|
112
|
+
## than that was written for an earlier process with this pid. Not while the editor scans, imports
|
|
113
|
+
## or runs a task, which every save waits out.
|
|
114
|
+
func _on_quit_look() -> void:
|
|
115
|
+
var request: String = ProjectSettings.globalize_path(QUITS_FOLDER + str(OS.get_process_id()))
|
|
116
|
+
if not FileAccess.file_exists(request) or FileAccess.get_modified_time(request) < _loaded_at:
|
|
117
|
+
return
|
|
118
|
+
if turned_away("quit_editor"):
|
|
119
|
+
return
|
|
120
|
+
_quit_look.stop()
|
|
121
|
+
var answer: Dictionary = await _tool_executor.execute_tool("quit_editor", {})
|
|
122
|
+
if answer.get("ok", false) != true:
|
|
123
|
+
push_warning(
|
|
124
|
+
"gdharness: the process holding this editor has gone, and it stays open: %s"
|
|
125
|
+
% str(answer.get("error", ""))
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
74
129
|
# A failed connect is a mistake in this plugin rather than anything the editor did, so it goes to
|
|
75
130
|
# the error stream instead of being dropped: a project holding return_value_discarded at error level
|
|
76
131
|
# refuses to compile a script that throws the answer away.
|
|
@@ -107,16 +162,38 @@ func _on_disconnected() -> void:
|
|
|
107
162
|
## scan updates the file list the scan's thread is writing out (see SceneFile.scan_later), and an
|
|
108
163
|
## import can run frames of the editor from inside itself, the one handling this call among them.
|
|
109
164
|
## Every call waits out a capture, which shows a scene for frames before it reads the view.
|
|
165
|
+
##
|
|
166
|
+
## A task in the editor's progress dialog runs frames of the editor from inside itself at every
|
|
167
|
+
## step, so a call handled then runs in the middle of that task. A save there asked for the script
|
|
168
|
+
## classes again while the editor was registering them, and the two registrations walked one list,
|
|
169
|
+
## which crashed the editor at its next scan.
|
|
110
170
|
func turned_away(tool_name: String) -> bool:
|
|
111
171
|
if _tool_executor != null and _tool_executor.capturing():
|
|
112
172
|
return true
|
|
173
|
+
if not ANSWERED_DURING_A_TASK.has(tool_name) and GdhSceneFile.in_a_task():
|
|
174
|
+
return true
|
|
113
175
|
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
114
176
|
return (
|
|
115
177
|
not ANSWERED_WHILE_SCANNING.has(tool_name)
|
|
116
|
-
and (
|
|
178
|
+
and (GdhSceneFile.scan_unfinished(filesystem) or filesystem.is_importing())
|
|
117
179
|
)
|
|
118
180
|
|
|
119
181
|
|
|
182
|
+
## What the editor is doing that turns calls away, and a mark that changes as it gets on with it,
|
|
183
|
+
## as [what, mark]. The server keeps asking while the mark changes: registering forty classes in a
|
|
184
|
+
## hidden editor took over thirty seconds, every one of them a step.
|
|
185
|
+
func busy_with() -> Array[String]:
|
|
186
|
+
if _tool_executor != null and _tool_executor.capturing():
|
|
187
|
+
return ["capturing its view", ""]
|
|
188
|
+
var labels: Array[String] = GdhSceneFile.task_labels()
|
|
189
|
+
if not labels.is_empty():
|
|
190
|
+
return ["running its task \"%s\"" % labels[0].trim_suffix("..."), "\n".join(labels)]
|
|
191
|
+
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
192
|
+
if filesystem.is_importing():
|
|
193
|
+
return ["importing its files", ""]
|
|
194
|
+
return ["scanning its files", str(filesystem.get_scanning_progress())]
|
|
195
|
+
|
|
196
|
+
|
|
120
197
|
func _on_tool_requested(request_id: String, tool_name: String, args: Dictionary) -> void:
|
|
121
198
|
if _tool_executor == null or _client == null:
|
|
122
199
|
return
|
|
@@ -124,7 +201,8 @@ func _on_tool_requested(request_id: String, tool_name: String, args: Dictionary)
|
|
|
124
201
|
# Turned away rather than held, so the server asks again: a call held here could run after the
|
|
125
202
|
# server had given up on it.
|
|
126
203
|
if turned_away(tool_name):
|
|
127
|
-
|
|
204
|
+
var busy: Array[String] = busy_with()
|
|
205
|
+
_client.send_busy(request_id, tool_name, busy[0], busy[1])
|
|
128
206
|
return
|
|
129
207
|
|
|
130
208
|
var result: Dictionary = await _tool_executor.execute_tool(tool_name, args)
|
|
@@ -30,6 +30,7 @@ const PUTTING_BACK_PASSES: int = 4
|
|
|
30
30
|
const GdhRead = preload("reading.gd")
|
|
31
31
|
const GdhSame = preload("same_values.gd")
|
|
32
32
|
const GdhSerialisation = preload("serialisation.gd")
|
|
33
|
+
const GdhSceneFile = preload("scene_file.gd")
|
|
33
34
|
const GdhSceneUids = preload("scene_uids.gd")
|
|
34
35
|
|
|
35
36
|
## Whether a scan asked for through [method scan_later] is still to start, which a caller waiting
|
|
@@ -41,6 +42,10 @@ static var also_saved: Array[Dictionary] = []
|
|
|
41
42
|
## What a write said of the connections it kept in the file though the engine cannot make them,
|
|
42
43
|
## gathered for that call's answer as [member also_saved] is.
|
|
43
44
|
static var unmade_note: String = ""
|
|
45
|
+
## The editor's progress dialog, which Godot keeps for the editor's whole life and exposes no other
|
|
46
|
+
## way. Looked for once, and again only if it has gone.
|
|
47
|
+
static var _progress: Node = null
|
|
48
|
+
static var _progress_sought: bool = false
|
|
44
49
|
|
|
45
50
|
var root: Node = null
|
|
46
51
|
var path: String = ""
|
|
@@ -102,14 +107,25 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
|
|
|
102
107
|
var missing: String = missing_said(path, FileAccess.get_file_as_string(path))
|
|
103
108
|
if not missing.is_empty():
|
|
104
109
|
return { "ok": false, "error": missing }
|
|
110
|
+
# A script the scene's scripts load that fails to compile names no node, so nothing else sees
|
|
111
|
+
# it.
|
|
112
|
+
var failed: GdhHeadlessCheck.GdhParseErrors = GdhHeadlessCheck.GdhParseErrors.new()
|
|
113
|
+
var watching: bool = for_writing and not Engine.is_editor_hint()
|
|
114
|
+
if watching:
|
|
115
|
+
OS.add_logger(failed)
|
|
105
116
|
var packed: PackedScene = (
|
|
106
117
|
ResourceLoader.load(path, "PackedScene", ResourceLoader.CACHE_MODE_IGNORE) as PackedScene
|
|
107
118
|
)
|
|
119
|
+
if packed != null:
|
|
120
|
+
root = packed.instantiate(PackedScene.GEN_EDIT_STATE_MAIN)
|
|
121
|
+
if watching:
|
|
122
|
+
OS.remove_logger(failed)
|
|
108
123
|
if packed == null:
|
|
109
124
|
return { "ok": false, "error": "Failed to load %s as a scene" % path }
|
|
110
|
-
root = packed.instantiate(PackedScene.GEN_EDIT_STATE_MAIN)
|
|
111
125
|
if root == null:
|
|
112
126
|
return { "ok": false, "error": "Failed to instantiate " + path }
|
|
127
|
+
if not Engine.is_editor_hint():
|
|
128
|
+
_unhold_placeholders()
|
|
113
129
|
_state = packed.get_state()
|
|
114
130
|
_base_paths = makers(_state)
|
|
115
131
|
if for_writing:
|
|
@@ -124,11 +140,75 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
|
|
|
124
140
|
if for_writing:
|
|
125
141
|
_text = FileAccess.get_file_as_string(path)
|
|
126
142
|
var unsettled: String = settle()
|
|
143
|
+
if unsettled.is_empty() and for_writing:
|
|
144
|
+
unsettled = failed.refusal(path)
|
|
127
145
|
if not unsettled.is_empty():
|
|
128
146
|
return refuse(("Nothing was written: " if for_writing else "Not read: ") + unsettled)
|
|
129
147
|
return {}
|
|
130
148
|
|
|
131
149
|
|
|
150
|
+
## Puts back each placeholder this scene places as the instance itself, marked, as an editor opens
|
|
151
|
+
## it. An engine with no editor opens one as an InstancePlaceholder even in an edit state, and the
|
|
152
|
+
## scene then packs it as a node of that class with every value it holds, so a save lost the mark.
|
|
153
|
+
## The instance takes the placeholder's place, values, groups, connections and the node values
|
|
154
|
+
## naming it. One whose scene does not instance stays as it was.
|
|
155
|
+
func _unhold_placeholders() -> void:
|
|
156
|
+
var held: Array[InstancePlaceholder] = []
|
|
157
|
+
_placeholders_under(root, held)
|
|
158
|
+
for one: InstancePlaceholder in held:
|
|
159
|
+
var packed: PackedScene = (
|
|
160
|
+
ResourceLoader.load(one.get_instance_path(), "PackedScene") as PackedScene
|
|
161
|
+
)
|
|
162
|
+
var instance: Node = (
|
|
163
|
+
packed.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE) if packed != null else null
|
|
164
|
+
)
|
|
165
|
+
if instance == null:
|
|
166
|
+
continue
|
|
167
|
+
instance.name = one.name
|
|
168
|
+
var values: Dictionary = one.get_stored_values()
|
|
169
|
+
for key: Variant in values:
|
|
170
|
+
instance.set(str(key), values[key])
|
|
171
|
+
# replace_by moves the place and the groups.
|
|
172
|
+
one.replace_by(instance, true)
|
|
173
|
+
instance.owner = root
|
|
174
|
+
instance.scene_file_path = one.get_instance_path()
|
|
175
|
+
instance.set_scene_instance_load_placeholder(true)
|
|
176
|
+
_repointed(root, one, instance)
|
|
177
|
+
one.free()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
## Every InstancePlaceholder below [param top] that this scene places, into [param into].
|
|
181
|
+
func _placeholders_under(top: Node, into: Array[InstancePlaceholder]) -> void:
|
|
182
|
+
for child: Node in top.get_children():
|
|
183
|
+
if child is InstancePlaceholder and child.owner == root:
|
|
184
|
+
into.append(child as InstancePlaceholder)
|
|
185
|
+
else:
|
|
186
|
+
_placeholders_under(child, into)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
## Points every value of a node of this scene that holds [param was], alone or in an array, at
|
|
190
|
+
## [param now].
|
|
191
|
+
func _repointed(top: Node, was: Node, now: Node) -> void:
|
|
192
|
+
if top.owner == root or top == root:
|
|
193
|
+
for declared: Dictionary in top.get_property_list():
|
|
194
|
+
if not GdhRead.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
195
|
+
continue
|
|
196
|
+
var named: String = str(declared["name"])
|
|
197
|
+
var value: Variant = top.get(named)
|
|
198
|
+
if value is Object and value == was:
|
|
199
|
+
top.set(named, now)
|
|
200
|
+
elif value is Array:
|
|
201
|
+
var listed: Array = value
|
|
202
|
+
if listed.has(was):
|
|
203
|
+
var swapped: Array = listed.duplicate()
|
|
204
|
+
for index: int in swapped.size():
|
|
205
|
+
if swapped[index] == was:
|
|
206
|
+
swapped[index] = now
|
|
207
|
+
top.set(named, swapped)
|
|
208
|
+
for child: Node in top.get_children():
|
|
209
|
+
_repointed(child, was, now)
|
|
210
|
+
|
|
211
|
+
|
|
132
212
|
## Why a write to the scene at [param scene_path], whose text is [param text], is refused, or "":
|
|
133
213
|
## an [ext_resource] naming no file the project has, by its uid or by its path. The engine loads the
|
|
134
214
|
## scene without it, so a save drops the entry and every value set from it, and the answer could
|
|
@@ -416,6 +496,64 @@ func copied(copies: Dictionary) -> void:
|
|
|
416
496
|
_copied_from[copy.get_instance_id()] = original.get_instance_id()
|
|
417
497
|
|
|
418
498
|
|
|
499
|
+
## A scene to write at [param branch_path] whose root is [param top], a node of this one about to be
|
|
500
|
+
## taken out of it, keeping what this file stores on each node of [param top] and what the tool
|
|
501
|
+
## asked of them. A connection the engine could not make goes with it when both its ends are in
|
|
502
|
+
## [param top]. One with a single end there is taken out of this scene's and answered, that end
|
|
503
|
+
## given as its path from [param top] under sourceBelow or targetBelow, for [method rejoin_unmade].
|
|
504
|
+
## Answers [scene, those connections].
|
|
505
|
+
func split_off(top: Node, branch_path: String) -> Array:
|
|
506
|
+
var branch: GdhSceneFile = GdhSceneFile.new()
|
|
507
|
+
branch.root = top
|
|
508
|
+
branch.path = branch_path
|
|
509
|
+
branch._text = _text
|
|
510
|
+
branch._reached = _reached.duplicate()
|
|
511
|
+
var nodes: Array[Node] = [top]
|
|
512
|
+
nodes.append_array(top.find_children("*", "", true, false))
|
|
513
|
+
for node: Node in nodes:
|
|
514
|
+
var id: int = node.get_instance_id()
|
|
515
|
+
if _filed.has(id):
|
|
516
|
+
branch._filed[id] = _filed[id]
|
|
517
|
+
if _asked.has(id):
|
|
518
|
+
branch._asked[id] = _asked[id]
|
|
519
|
+
if _before.has(id):
|
|
520
|
+
branch._before[id] = _before[id]
|
|
521
|
+
var crossing: Array[Dictionary] = []
|
|
522
|
+
var still: Array[Dictionary] = []
|
|
523
|
+
for held: Dictionary in _unmade:
|
|
524
|
+
var source_in: bool = _within(held["source"], top)
|
|
525
|
+
var target_in: bool = _within(held["target"], top)
|
|
526
|
+
if source_in and target_in:
|
|
527
|
+
branch._unmade.append(held)
|
|
528
|
+
continue
|
|
529
|
+
if not source_in and not target_in:
|
|
530
|
+
still.append(held)
|
|
531
|
+
continue
|
|
532
|
+
var end: String = "source" if source_in else "target"
|
|
533
|
+
var below: Node = held[end]
|
|
534
|
+
held[end + "Below"] = str(top.get_path_to(below))
|
|
535
|
+
crossing.append(held)
|
|
536
|
+
_unmade = still
|
|
537
|
+
return [branch, crossing]
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
## Puts back each of [param crossing], from [method split_off], among the connections a write keeps,
|
|
541
|
+
## its end below the branch now on the node at that path below [param instance].
|
|
542
|
+
func rejoin_unmade(crossing: Array[Dictionary], instance: Node) -> void:
|
|
543
|
+
for held: Dictionary in crossing:
|
|
544
|
+
for end: String in ["source", "target"]:
|
|
545
|
+
if held.has(end + "Below"):
|
|
546
|
+
held[end] = instance.get_node_or_null(NodePath(str(held[end + "Below"])))
|
|
547
|
+
_unmade.append(held)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
static func _within(held: Variant, top: Node) -> bool:
|
|
551
|
+
if not is_instance_valid(held) or not held is Node:
|
|
552
|
+
return false
|
|
553
|
+
var node: Node = held
|
|
554
|
+
return node == top or top.is_ancestor_of(node)
|
|
555
|
+
|
|
556
|
+
|
|
419
557
|
## Why [param scene_path] cannot be written while the editor has it open, or "".
|
|
420
558
|
static func unsaved_in_editor(scene_path: String) -> String:
|
|
421
559
|
if not Engine.is_editor_hint() or not EditorInterface.get_unsaved_scenes().has(scene_path):
|
|
@@ -1075,23 +1213,63 @@ static func scan_later() -> void:
|
|
|
1075
1213
|
_start_scan.call_deferred()
|
|
1076
1214
|
|
|
1077
1215
|
|
|
1078
|
-
##
|
|
1079
|
-
##
|
|
1080
|
-
##
|
|
1081
|
-
##
|
|
1216
|
+
## It waits out a task of the progress dialog too, whose every step runs a frame of the editor from
|
|
1217
|
+
## inside itself. A scan's end registers the classes it found in such a task (4.7.2's
|
|
1218
|
+
## [code]EditorFileSystem::_notification[/code]). A scan started from that frame ran beside the end
|
|
1219
|
+
## of the one before, which crashed the editor or left it scanning.
|
|
1082
1220
|
static func _start_scan() -> void:
|
|
1083
1221
|
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
1084
1222
|
var tree: SceneTree = Engine.get_main_loop()
|
|
1085
|
-
while filesystem
|
|
1223
|
+
while scan_unfinished(filesystem) or filesystem.is_importing() or in_a_task():
|
|
1086
1224
|
await tree.process_frame
|
|
1087
1225
|
filesystem.scan()
|
|
1088
|
-
if not filesystem.is_scanning():
|
|
1089
|
-
await tree.process_frame
|
|
1090
|
-
await tree.process_frame
|
|
1091
|
-
filesystem.scan()
|
|
1092
1226
|
scan_due = false
|
|
1093
1227
|
|
|
1094
1228
|
|
|
1229
|
+
## Whether the editor scans its files, counting the frames after a scan stops reporting itself and
|
|
1230
|
+
## before the editor handles its end. That end swaps in the new file list and registers the classes
|
|
1231
|
+
## the scan found, from a frame of the editor. A save in those frames registered the classes as
|
|
1232
|
+
## well,
|
|
1233
|
+
## inside its own task, and the editor crashed or logged "Task 'update_scripts_classes' already
|
|
1234
|
+
## exists". [method EditorFileSystem.scan] also does nothing then, without a word. The editor
|
|
1235
|
+
## processes from the start of a scan until it has handled the end (4.7.2's
|
|
1236
|
+
## [code]EditorFileSystem::scan[/code] and [code]_notification[/code]).
|
|
1237
|
+
static func scan_unfinished(filesystem: EditorFileSystem) -> bool:
|
|
1238
|
+
return filesystem.is_scanning() or filesystem.is_processing()
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
## Whether the editor's progress dialog is showing, which it does for as long as a task is open.
|
|
1242
|
+
static func in_a_task() -> bool:
|
|
1243
|
+
if _progress != null and not is_instance_valid(_progress):
|
|
1244
|
+
_progress = null
|
|
1245
|
+
_progress_sought = false
|
|
1246
|
+
if not _progress_sought:
|
|
1247
|
+
_progress_sought = true
|
|
1248
|
+
_progress = _found_progress((Engine.get_main_loop() as SceneTree).root)
|
|
1249
|
+
return _progress is CanvasItem and (_progress as CanvasItem).visible
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
## The labels of the progress dialog's open tasks, each task's title then the step it is on, which
|
|
1253
|
+
## change as the task goes on. Empty when no task is open.
|
|
1254
|
+
static func task_labels() -> Array[String]:
|
|
1255
|
+
var labels: Array[String] = []
|
|
1256
|
+
if not in_a_task():
|
|
1257
|
+
return labels
|
|
1258
|
+
for label: Node in _progress.find_children("*", "Label", true, false):
|
|
1259
|
+
labels.append(str(label.get("text")))
|
|
1260
|
+
return labels
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
static func _found_progress(top: Node) -> Node:
|
|
1264
|
+
var pending: Array[Node] = [top]
|
|
1265
|
+
while not pending.is_empty():
|
|
1266
|
+
var node: Node = pending.pop_back()
|
|
1267
|
+
if node.is_class("ProgressDialog"):
|
|
1268
|
+
return node
|
|
1269
|
+
pending.append_array(node.get_children(true))
|
|
1270
|
+
return null
|
|
1271
|
+
|
|
1272
|
+
|
|
1095
1273
|
## Whether the editor holds unsaved changes to [param scene_path], for a read to say that the file
|
|
1096
1274
|
## it answers from is not what the editor shows.
|
|
1097
1275
|
static func unsaved(scene_path: String) -> bool:
|
|
@@ -1123,7 +1301,7 @@ func _settled(packed: PackedScene) -> Array:
|
|
|
1123
1301
|
var name: String = found[1]
|
|
1124
1302
|
var wanted: Variant = found[3] if found[2] else _unset_value(node, name)
|
|
1125
1303
|
var held: Variant = node.get(name)
|
|
1126
|
-
|
|
1304
|
+
GdhHeadlessCheck.restore(node, name, wanted)
|
|
1127
1305
|
if GdhHeadlessCheck.kept_by_engine(node, name, wanted, held):
|
|
1128
1306
|
_engine_kept["%d:%s" % [node.get_instance_id(), name]] = true
|
|
1129
1307
|
for found: Array in moved:
|
|
@@ -1131,7 +1309,9 @@ func _settled(packed: PackedScene) -> Array:
|
|
|
1131
1309
|
var before: Dictionary = _before[node.get_instance_id()]
|
|
1132
1310
|
var restored: Dictionary = restored_by_write.get_or_add(node.get_instance_id(), {})
|
|
1133
1311
|
for name: String in found[1]:
|
|
1134
|
-
|
|
1312
|
+
GdhHeadlessCheck.restore(
|
|
1313
|
+
node, name, before.get(name, GdhHeadlessCheck.default_of(node, name))
|
|
1314
|
+
)
|
|
1135
1315
|
restored[name] = true
|
|
1136
1316
|
current = PackedScene.new()
|
|
1137
1317
|
var packing: Error = current.pack(root)
|
|
@@ -118,6 +118,7 @@ func _init_tools() -> void:
|
|
|
118
118
|
"delete_node": [_scene_tools, "delete_node"],
|
|
119
119
|
"duplicate_node": [_scene_tools, "duplicate_node"],
|
|
120
120
|
"reparent_node": [_scene_tools, "reparent_node"],
|
|
121
|
+
"save_branch": [_scene_tools, "save_branch"],
|
|
121
122
|
"set_node_properties": [_scene_tools, "set_node_properties"],
|
|
122
123
|
"get_node_properties": [_scene_tools, "get_node_properties"],
|
|
123
124
|
"bake_navigation": [_scene_tools, "bake_navigation"],
|
|
@@ -21,6 +21,8 @@ extends Node
|
|
|
21
21
|
## A file the scan skipped is either missing from the tree or recorded with no class name against
|
|
22
22
|
## it, and both come back as a class the editor does not have, which is the whole of the question.
|
|
23
23
|
|
|
24
|
+
const GdhSceneFile = preload("../scene_file.gd")
|
|
25
|
+
|
|
24
26
|
var _editor_plugin: EditorPlugin = null
|
|
25
27
|
|
|
26
28
|
|
|
@@ -282,7 +284,7 @@ func global_classes(_args: Dictionary) -> Dictionary:
|
|
|
282
284
|
return {
|
|
283
285
|
"ok": true,
|
|
284
286
|
"classes": names,
|
|
285
|
-
"scanning":
|
|
287
|
+
"scanning": GdhSceneFile.scan_unfinished(filesystem),
|
|
286
288
|
"importing": filesystem.is_importing(),
|
|
287
289
|
}
|
|
288
290
|
|
|
@@ -55,7 +55,7 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
|
|
|
55
55
|
# Not over a scan or an import the editor is already running: a scan asked for then starts a
|
|
56
56
|
# second reimport over the first, and the editor logs "Task 'reimport' already exists" and two
|
|
57
57
|
# conditions from its progress dialog. Declined and said, so the caller waits and asks again.
|
|
58
|
-
var busy: bool =
|
|
58
|
+
var busy: bool = GdhSceneFile.scan_unfinished(filesystem) or filesystem.is_importing()
|
|
59
59
|
var started: bool = false
|
|
60
60
|
var completed_before: int = _scans_completed
|
|
61
61
|
var uids: Dictionary = { "reread": [], "duplicated": [] }
|
|
@@ -71,13 +71,9 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
|
|
|
71
71
|
for path: Variant in paths:
|
|
72
72
|
filesystem.update_file(str(path))
|
|
73
73
|
filesystem.scan()
|
|
74
|
-
# Whether the scan ran, read off the editor rather than assumed.
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
# Taken as started, the answer came from a scan begun before the files were written:
|
|
78
|
-
# downstream, a rescan after 698 files were rewritten answered finished in 1.5s and the
|
|
79
|
-
# one changed file stayed on its old import. A threaded scan reports itself from inside
|
|
80
|
-
# the call, and one run on this thread has finished and signalled before it returns.
|
|
74
|
+
# Whether the scan ran, read off the editor rather than assumed. A threaded scan reports
|
|
75
|
+
# itself from inside the call, and one run on this thread has finished and signalled before
|
|
76
|
+
# it returns.
|
|
81
77
|
started = filesystem.is_scanning() or _scans_completed != completed_before
|
|
82
78
|
|
|
83
79
|
# Importing is reported separately from scanning, and a class is not registered until
|
|
@@ -87,7 +83,7 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
|
|
|
87
83
|
return {
|
|
88
84
|
"ok": true,
|
|
89
85
|
"started": started,
|
|
90
|
-
"scanning":
|
|
86
|
+
"scanning": GdhSceneFile.scan_unfinished(filesystem),
|
|
91
87
|
"importing": filesystem.is_importing(),
|
|
92
88
|
"scansCompletedBefore": completed_before,
|
|
93
89
|
"scansCompleted": _scans_completed,
|
|
@@ -179,7 +175,7 @@ func scan_status(_args: Dictionary) -> Dictionary:
|
|
|
179
175
|
"openScenes": open,
|
|
180
176
|
"editedScene": "" if shown == null else shown.scene_file_path,
|
|
181
177
|
"unsavedScenes": unsaved,
|
|
182
|
-
"scanning":
|
|
178
|
+
"scanning": GdhSceneFile.scan_unfinished(filesystem),
|
|
183
179
|
"importing": filesystem.is_importing(),
|
|
184
180
|
"pending": GdhSceneFile.scan_due,
|
|
185
181
|
"sinceScanFinishedMs":
|
|
@@ -210,13 +206,13 @@ func reimport_files(args: Dictionary) -> Dictionary:
|
|
|
210
206
|
if paths.is_empty():
|
|
211
207
|
return { "ok": false, "error": "reimport_files needs paths" }
|
|
212
208
|
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
213
|
-
var busy: bool =
|
|
209
|
+
var busy: bool = GdhSceneFile.scan_unfinished(filesystem) or filesystem.is_importing()
|
|
214
210
|
if not busy:
|
|
215
211
|
_reimport.call_deferred(PackedStringArray(paths))
|
|
216
212
|
return {
|
|
217
213
|
"ok": true,
|
|
218
214
|
"started": not busy,
|
|
219
|
-
"scanning":
|
|
215
|
+
"scanning": GdhSceneFile.scan_unfinished(filesystem),
|
|
220
216
|
"importing": filesystem.is_importing(),
|
|
221
217
|
"reimportsCompletedBefore": _reimports_completed,
|
|
222
218
|
}
|
|
@@ -290,7 +286,7 @@ func files_moved(args: Dictionary) -> Dictionary:
|
|
|
290
286
|
func _reimport(paths: PackedStringArray) -> void:
|
|
291
287
|
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
292
288
|
# A scan can begin between the answer and this frame, and its import would refuse this one.
|
|
293
|
-
while
|
|
289
|
+
while GdhSceneFile.scan_unfinished(filesystem) or filesystem.is_importing():
|
|
294
290
|
await get_tree().process_frame
|
|
295
291
|
filesystem.reimport_files(paths)
|
|
296
292
|
_reimports_completed += 1
|