gdharness 1.3.67 → 1.3.68
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 +33537 -32369
- package/build/godot/addons/gdharness_editor/bridge_client.gd +5 -3
- 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 +128 -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 +469 -7
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +2 -0
- package/build/godot/operations/editor_tools.gd +1 -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
|
|
|
@@ -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,12 +107,21 @@ 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 }
|
|
113
127
|
_state = packed.get_state()
|
|
@@ -124,6 +138,8 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
|
|
|
124
138
|
if for_writing:
|
|
125
139
|
_text = FileAccess.get_file_as_string(path)
|
|
126
140
|
var unsettled: String = settle()
|
|
141
|
+
if unsettled.is_empty() and for_writing:
|
|
142
|
+
unsettled = failed.refusal(path)
|
|
127
143
|
if not unsettled.is_empty():
|
|
128
144
|
return refuse(("Nothing was written: " if for_writing else "Not read: ") + unsettled)
|
|
129
145
|
return {}
|
|
@@ -416,6 +432,64 @@ func copied(copies: Dictionary) -> void:
|
|
|
416
432
|
_copied_from[copy.get_instance_id()] = original.get_instance_id()
|
|
417
433
|
|
|
418
434
|
|
|
435
|
+
## A scene to write at [param branch_path] whose root is [param top], a node of this one about to be
|
|
436
|
+
## taken out of it, keeping what this file stores on each node of [param top] and what the tool
|
|
437
|
+
## asked of them. A connection the engine could not make goes with it when both its ends are in
|
|
438
|
+
## [param top]. One with a single end there is taken out of this scene's and answered, that end
|
|
439
|
+
## given as its path from [param top] under sourceBelow or targetBelow, for [method rejoin_unmade].
|
|
440
|
+
## Answers [scene, those connections].
|
|
441
|
+
func split_off(top: Node, branch_path: String) -> Array:
|
|
442
|
+
var branch: GdhSceneFile = GdhSceneFile.new()
|
|
443
|
+
branch.root = top
|
|
444
|
+
branch.path = branch_path
|
|
445
|
+
branch._text = _text
|
|
446
|
+
branch._reached = _reached.duplicate()
|
|
447
|
+
var nodes: Array[Node] = [top]
|
|
448
|
+
nodes.append_array(top.find_children("*", "", true, false))
|
|
449
|
+
for node: Node in nodes:
|
|
450
|
+
var id: int = node.get_instance_id()
|
|
451
|
+
if _filed.has(id):
|
|
452
|
+
branch._filed[id] = _filed[id]
|
|
453
|
+
if _asked.has(id):
|
|
454
|
+
branch._asked[id] = _asked[id]
|
|
455
|
+
if _before.has(id):
|
|
456
|
+
branch._before[id] = _before[id]
|
|
457
|
+
var crossing: Array[Dictionary] = []
|
|
458
|
+
var still: Array[Dictionary] = []
|
|
459
|
+
for held: Dictionary in _unmade:
|
|
460
|
+
var source_in: bool = _within(held["source"], top)
|
|
461
|
+
var target_in: bool = _within(held["target"], top)
|
|
462
|
+
if source_in and target_in:
|
|
463
|
+
branch._unmade.append(held)
|
|
464
|
+
continue
|
|
465
|
+
if not source_in and not target_in:
|
|
466
|
+
still.append(held)
|
|
467
|
+
continue
|
|
468
|
+
var end: String = "source" if source_in else "target"
|
|
469
|
+
var below: Node = held[end]
|
|
470
|
+
held[end + "Below"] = str(top.get_path_to(below))
|
|
471
|
+
crossing.append(held)
|
|
472
|
+
_unmade = still
|
|
473
|
+
return [branch, crossing]
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
## Puts back each of [param crossing], from [method split_off], among the connections a write keeps,
|
|
477
|
+
## its end below the branch now on the node at that path below [param instance].
|
|
478
|
+
func rejoin_unmade(crossing: Array[Dictionary], instance: Node) -> void:
|
|
479
|
+
for held: Dictionary in crossing:
|
|
480
|
+
for end: String in ["source", "target"]:
|
|
481
|
+
if held.has(end + "Below"):
|
|
482
|
+
held[end] = instance.get_node_or_null(NodePath(str(held[end + "Below"])))
|
|
483
|
+
_unmade.append(held)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
static func _within(held: Variant, top: Node) -> bool:
|
|
487
|
+
if not is_instance_valid(held) or not held is Node:
|
|
488
|
+
return false
|
|
489
|
+
var node: Node = held
|
|
490
|
+
return node == top or top.is_ancestor_of(node)
|
|
491
|
+
|
|
492
|
+
|
|
419
493
|
## Why [param scene_path] cannot be written while the editor has it open, or "".
|
|
420
494
|
static func unsaved_in_editor(scene_path: String) -> String:
|
|
421
495
|
if not Engine.is_editor_hint() or not EditorInterface.get_unsaved_scenes().has(scene_path):
|
|
@@ -1075,23 +1149,63 @@ static func scan_later() -> void:
|
|
|
1075
1149
|
_start_scan.call_deferred()
|
|
1076
1150
|
|
|
1077
1151
|
|
|
1078
|
-
##
|
|
1079
|
-
##
|
|
1080
|
-
##
|
|
1081
|
-
##
|
|
1152
|
+
## It waits out a task of the progress dialog too, whose every step runs a frame of the editor from
|
|
1153
|
+
## inside itself. A scan's end registers the classes it found in such a task (4.7.2's
|
|
1154
|
+
## [code]EditorFileSystem::_notification[/code]). A scan started from that frame ran beside the end
|
|
1155
|
+
## of the one before, which crashed the editor or left it scanning.
|
|
1082
1156
|
static func _start_scan() -> void:
|
|
1083
1157
|
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
1084
1158
|
var tree: SceneTree = Engine.get_main_loop()
|
|
1085
|
-
while filesystem
|
|
1159
|
+
while scan_unfinished(filesystem) or filesystem.is_importing() or in_a_task():
|
|
1086
1160
|
await tree.process_frame
|
|
1087
1161
|
filesystem.scan()
|
|
1088
|
-
if not filesystem.is_scanning():
|
|
1089
|
-
await tree.process_frame
|
|
1090
|
-
await tree.process_frame
|
|
1091
|
-
filesystem.scan()
|
|
1092
1162
|
scan_due = false
|
|
1093
1163
|
|
|
1094
1164
|
|
|
1165
|
+
## Whether the editor scans its files, counting the frames after a scan stops reporting itself and
|
|
1166
|
+
## before the editor handles its end. That end swaps in the new file list and registers the classes
|
|
1167
|
+
## the scan found, from a frame of the editor. A save in those frames registered the classes as
|
|
1168
|
+
## well,
|
|
1169
|
+
## inside its own task, and the editor crashed or logged "Task 'update_scripts_classes' already
|
|
1170
|
+
## exists". [method EditorFileSystem.scan] also does nothing then, without a word. The editor
|
|
1171
|
+
## processes from the start of a scan until it has handled the end (4.7.2's
|
|
1172
|
+
## [code]EditorFileSystem::scan[/code] and [code]_notification[/code]).
|
|
1173
|
+
static func scan_unfinished(filesystem: EditorFileSystem) -> bool:
|
|
1174
|
+
return filesystem.is_scanning() or filesystem.is_processing()
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
## Whether the editor's progress dialog is showing, which it does for as long as a task is open.
|
|
1178
|
+
static func in_a_task() -> bool:
|
|
1179
|
+
if _progress != null and not is_instance_valid(_progress):
|
|
1180
|
+
_progress = null
|
|
1181
|
+
_progress_sought = false
|
|
1182
|
+
if not _progress_sought:
|
|
1183
|
+
_progress_sought = true
|
|
1184
|
+
_progress = _found_progress((Engine.get_main_loop() as SceneTree).root)
|
|
1185
|
+
return _progress is CanvasItem and (_progress as CanvasItem).visible
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
## The labels of the progress dialog's open tasks, each task's title then the step it is on, which
|
|
1189
|
+
## change as the task goes on. Empty when no task is open.
|
|
1190
|
+
static func task_labels() -> Array[String]:
|
|
1191
|
+
var labels: Array[String] = []
|
|
1192
|
+
if not in_a_task():
|
|
1193
|
+
return labels
|
|
1194
|
+
for label: Node in _progress.find_children("*", "Label", true, false):
|
|
1195
|
+
labels.append(str(label.get("text")))
|
|
1196
|
+
return labels
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
static func _found_progress(top: Node) -> Node:
|
|
1200
|
+
var pending: Array[Node] = [top]
|
|
1201
|
+
while not pending.is_empty():
|
|
1202
|
+
var node: Node = pending.pop_back()
|
|
1203
|
+
if node.is_class("ProgressDialog"):
|
|
1204
|
+
return node
|
|
1205
|
+
pending.append_array(node.get_children(true))
|
|
1206
|
+
return null
|
|
1207
|
+
|
|
1208
|
+
|
|
1095
1209
|
## Whether the editor holds unsaved changes to [param scene_path], for a read to say that the file
|
|
1096
1210
|
## it answers from is not what the editor shows.
|
|
1097
1211
|
static func unsaved(scene_path: String) -> bool:
|
|
@@ -1123,7 +1237,7 @@ func _settled(packed: PackedScene) -> Array:
|
|
|
1123
1237
|
var name: String = found[1]
|
|
1124
1238
|
var wanted: Variant = found[3] if found[2] else _unset_value(node, name)
|
|
1125
1239
|
var held: Variant = node.get(name)
|
|
1126
|
-
|
|
1240
|
+
GdhHeadlessCheck.restore(node, name, wanted)
|
|
1127
1241
|
if GdhHeadlessCheck.kept_by_engine(node, name, wanted, held):
|
|
1128
1242
|
_engine_kept["%d:%s" % [node.get_instance_id(), name]] = true
|
|
1129
1243
|
for found: Array in moved:
|
|
@@ -1131,7 +1245,9 @@ func _settled(packed: PackedScene) -> Array:
|
|
|
1131
1245
|
var before: Dictionary = _before[node.get_instance_id()]
|
|
1132
1246
|
var restored: Dictionary = restored_by_write.get_or_add(node.get_instance_id(), {})
|
|
1133
1247
|
for name: String in found[1]:
|
|
1134
|
-
|
|
1248
|
+
GdhHeadlessCheck.restore(
|
|
1249
|
+
node, name, before.get(name, GdhHeadlessCheck.default_of(node, name))
|
|
1250
|
+
)
|
|
1135
1251
|
restored[name] = true
|
|
1136
1252
|
current = PackedScene.new()
|
|
1137
1253
|
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
|
|
@@ -180,19 +180,7 @@ static func follow(
|
|
|
180
180
|
) -> Dictionary:
|
|
181
181
|
var texts: Dictionary = {}
|
|
182
182
|
_read_scenes("res://", texts)
|
|
183
|
-
var kin: Array[String] =
|
|
184
|
-
var grew: bool = true
|
|
185
|
-
while grew:
|
|
186
|
-
grew = false
|
|
187
|
-
for scene_path: String in texts:
|
|
188
|
-
if kin.has(scene_path):
|
|
189
|
-
continue
|
|
190
|
-
var text: String = texts[scene_path]
|
|
191
|
-
for one: String in kin:
|
|
192
|
-
if text.contains(' path="%s"' % one):
|
|
193
|
-
kin.append(scene_path)
|
|
194
|
-
grew = true
|
|
195
|
-
break
|
|
183
|
+
var kin: Array[String] = _kin_of(changed, texts)
|
|
196
184
|
var followed: Array[String] = []
|
|
197
185
|
var not_followed: Array[Dictionary] = []
|
|
198
186
|
for scene_path: String in kin:
|
|
@@ -247,6 +235,92 @@ static func follow(
|
|
|
247
235
|
return answer
|
|
248
236
|
|
|
249
237
|
|
|
238
|
+
## Marks the instance a branch at [param at] in [param changed] became editable in every other
|
|
239
|
+
## .tscn that instances or inherits [param changed] at any depth, wherever that scene keeps values
|
|
240
|
+
## on a node inside it or adds one below it. Those still load, and the next save of that scene
|
|
241
|
+
## dropped them, since the packer skips every node under an instance whose children are not
|
|
242
|
+
## editable. Answers followed, the scenes marked, and notFollowed, each left with why, each only
|
|
243
|
+
## when it names any.
|
|
244
|
+
static func enclose(changed: String, at: String) -> Dictionary:
|
|
245
|
+
var texts: Dictionary = {}
|
|
246
|
+
_read_scenes("res://", texts)
|
|
247
|
+
var followed: Array[String] = []
|
|
248
|
+
var not_followed: Array[Dictionary] = []
|
|
249
|
+
for scene_path: String in _kin_of(changed, texts):
|
|
250
|
+
if scene_path == changed:
|
|
251
|
+
continue
|
|
252
|
+
var moves: Dictionary = {}
|
|
253
|
+
if _inherits(scene_path, changed, texts):
|
|
254
|
+
moves[at] = at
|
|
255
|
+
var read: Dictionary = GdhFileReads.scene_tree(scene_path, -1, false, false)
|
|
256
|
+
if not read.get("ok", false):
|
|
257
|
+
not_followed.append({ "scenePath": scene_path, "reason": str(read.get("error", "")) })
|
|
258
|
+
continue
|
|
259
|
+
var tree: Dictionary = read["tree"]
|
|
260
|
+
_instances_of(tree, changed, at, at, moves)
|
|
261
|
+
var text: String = texts[scene_path]
|
|
262
|
+
var marks: PackedStringArray = []
|
|
263
|
+
for path: String in moves:
|
|
264
|
+
var mark: String = '[editable path="%s"]' % path
|
|
265
|
+
if not text.contains(mark) and _holds_below(text, path):
|
|
266
|
+
var _marked: bool = marks.append(mark)
|
|
267
|
+
if marks.is_empty():
|
|
268
|
+
continue
|
|
269
|
+
var unsaved: String = GdhSceneFile.unsaved_in_editor(scene_path)
|
|
270
|
+
if not unsaved.is_empty():
|
|
271
|
+
not_followed.append({ "scenePath": scene_path, "reason": unsaved })
|
|
272
|
+
continue
|
|
273
|
+
var file: FileAccess = FileAccess.open(scene_path, FileAccess.WRITE)
|
|
274
|
+
if (file == null
|
|
275
|
+
or not file.store_string(text.rstrip("\n") + "\n\n" + "\n".join(marks) + "\n")):
|
|
276
|
+
not_followed.append(
|
|
277
|
+
{ "scenePath": scene_path, "reason": error_string(FileAccess.get_open_error()) }
|
|
278
|
+
)
|
|
279
|
+
continue
|
|
280
|
+
file.close()
|
|
281
|
+
GdhSceneFile.refresh(scene_path)
|
|
282
|
+
followed.append(scene_path)
|
|
283
|
+
var answer: Dictionary = {}
|
|
284
|
+
if not followed.is_empty():
|
|
285
|
+
answer["followed"] = followed
|
|
286
|
+
if not not_followed.is_empty():
|
|
287
|
+
answer["notFollowed"] = not_followed
|
|
288
|
+
return answer
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
## Whether [param text], a scene's file, has a node entry below [param path], one setting values on
|
|
292
|
+
## a node there or adding one.
|
|
293
|
+
static func _holds_below(text: String, path: String) -> bool:
|
|
294
|
+
for line: String in text.split("\n"):
|
|
295
|
+
if not line.begins_with("[node "):
|
|
296
|
+
continue
|
|
297
|
+
var parent: String = _attribute(line, "parent")
|
|
298
|
+
if parent.is_empty():
|
|
299
|
+
continue
|
|
300
|
+
var full: String = _under(parent, _attribute(line, "name"))
|
|
301
|
+
if full.begins_with(path + "/"):
|
|
302
|
+
return true
|
|
303
|
+
return false
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
## [param changed] and every scene in [param texts] that instances or inherits it at any depth.
|
|
307
|
+
static func _kin_of(changed: String, texts: Dictionary) -> Array[String]:
|
|
308
|
+
var kin: Array[String] = [changed]
|
|
309
|
+
var grew: bool = true
|
|
310
|
+
while grew:
|
|
311
|
+
grew = false
|
|
312
|
+
for scene_path: String in texts:
|
|
313
|
+
if kin.has(scene_path):
|
|
314
|
+
continue
|
|
315
|
+
var text: String = texts[scene_path]
|
|
316
|
+
for one: String in kin:
|
|
317
|
+
if text.contains(' path="%s"' % one):
|
|
318
|
+
kin.append(scene_path)
|
|
319
|
+
grew = true
|
|
320
|
+
break
|
|
321
|
+
return kin
|
|
322
|
+
|
|
323
|
+
|
|
250
324
|
## The text of every .tscn under [param directory] into [param texts], keyed by res:// path, leaving
|
|
251
325
|
## out the engine's own folder and hidden ones.
|
|
252
326
|
static func _read_scenes(directory: String, texts: Dictionary) -> void:
|