gdharness 1.3.66 → 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/README.md +1 -1
- package/build/cli.js +19209 -17814
- package/build/godot/addons/gdharness_editor/bridge_client.gd +18 -5
- package/build/godot/addons/gdharness_editor/headless_check.gd +52 -0
- package/build/godot/addons/gdharness_editor/picture_saves.gd +48 -0
- package/build/godot/addons/gdharness_editor/picture_saves.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/plugin.gd +84 -3
- package/build/godot/addons/gdharness_editor/scene_file.gd +133 -12
- package/build/godot/addons/gdharness_editor/tool_executor.gd +16 -1
- 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 +51 -17
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +473 -8
- package/build/godot/addons/gdharness_editor/tools/view_tools.gd +121 -0
- package/build/godot/addons/gdharness_editor/tools/view_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/picture_saves.gd +48 -0
- package/build/godot/addons/gdharness_runtime/picture_saves.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -43
- package/build/godot/operations/editor_tools.gd +1 -0
- package/build/godot/operations/gdscript_analysis.gd +17 -0
- package/build/godot/operations/picture_saves.gd +48 -0
- package/build/keeper.js +116 -114
- package/package.json +1 -1
|
@@ -48,6 +48,9 @@ const DAP_ASKED: String = "GDHARNESS_DAP_PORT"
|
|
|
48
48
|
## What a server that opened this editor says about itself, so that being opened by one is stated
|
|
49
49
|
## rather than guessed at from the ports. Kept in step with `OPENED_BY_A_SERVER` in src/launch.ts.
|
|
50
50
|
const OPENED_BY_A_SERVER: String = "GDHARNESS_OPENED_BY_A_SERVER"
|
|
51
|
+
## Set for an editor a server started on its hidden desktop. Kept in step with `DESKTOP_VARIABLE`
|
|
52
|
+
## in src/desktop.ts.
|
|
53
|
+
const DESKTOP_VARIABLE: String = "GDHARNESS_DESKTOP"
|
|
51
54
|
## Put into this editor's own environment as it loads, so every game it plays inherits it and
|
|
52
55
|
## the runtime addon can announce which editor played it. A game of the same project that some
|
|
53
56
|
## other server started has no such variable, or another editor's number, and that is what
|
|
@@ -357,8 +360,10 @@ func _handle_connect() -> void:
|
|
|
357
360
|
# Whether a session opening on the adapter is told this editor's breakpoints or takes
|
|
358
361
|
# them away, so a server can say which before it sets one.
|
|
359
362
|
"syncs_breakpoints": _syncs_breakpoints(),
|
|
360
|
-
# Whether this editor runs with no window, which for one opened by hand
|
|
361
|
-
|
|
363
|
+
# Whether this editor runs with no window anybody sees, which for one opened by hand
|
|
364
|
+
# only
|
|
365
|
+
# it can say: headless, or a window on the hidden desktop a server started it on.
|
|
366
|
+
"hidden": DisplayServer.get_name() == "headless" or _on_the_hidden_desktop(),
|
|
362
367
|
# Whether a server started this editor, which is the only kind that can be started again
|
|
363
368
|
# as itself: the engine hands back none of the arguments it was given, so the arguments
|
|
364
369
|
# have to come from whoever wrote them. See `restart_editor` in play_tools.gd.
|
|
@@ -442,6 +447,12 @@ func _keep_breakpoints_through_sessions() -> void:
|
|
|
442
447
|
)
|
|
443
448
|
|
|
444
449
|
|
|
450
|
+
## Whether a server started this editor on its hidden desktop, where its window draws and never
|
|
451
|
+
## shows.
|
|
452
|
+
static func _on_the_hidden_desktop() -> bool:
|
|
453
|
+
return OS.get_name() == "Windows" and not OS.get_environment(DESKTOP_VARIABLE).is_empty()
|
|
454
|
+
|
|
455
|
+
|
|
445
456
|
## Whether the editor shares its breakpoints with a session as it opens. See the setting.
|
|
446
457
|
func _syncs_breakpoints() -> bool:
|
|
447
458
|
if not Engine.is_editor_hint():
|
|
@@ -562,15 +573,17 @@ func _handle_message(json_string: String) -> void:
|
|
|
562
573
|
|
|
563
574
|
## Says [param tool_name] was not run because the editor is scanning or importing, for the server to
|
|
564
575
|
## ask again once that is over.
|
|
565
|
-
|
|
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:
|
|
566
578
|
_send_message(
|
|
567
579
|
{
|
|
568
580
|
"type": "tool_result",
|
|
569
581
|
"id": request_id,
|
|
570
582
|
"success": false,
|
|
571
583
|
"busy": true,
|
|
572
|
-
"
|
|
573
|
-
"
|
|
584
|
+
"busyWith": doing,
|
|
585
|
+
"progress": mark,
|
|
586
|
+
"error": "The editor is %s, so %s was not run." % [doing, tool_name],
|
|
574
587
|
}
|
|
575
588
|
)
|
|
576
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
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
# How a captured picture is sized and saved, for the game's captures and the editor's alike.
|
|
4
|
+
#
|
|
5
|
+
# This file is the one beside the operations, and `bun run sync:gd` copies it into each addon,
|
|
6
|
+
# because an addon is installed as a directory and cannot preload out of one.
|
|
7
|
+
|
|
8
|
+
## Measured on a downstream game's 1280x720 frame: its interface text and pixel art read as they do
|
|
9
|
+
## in the PNG, at a fifth of the PNG's size.
|
|
10
|
+
const JPEG_QUALITY: float = 0.8
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## The size a capture drawn at [param drawn] is scaled to, from the width and height asked for, 0
|
|
14
|
+
## being not asked. One side alone keeps the picture's proportions, since that is what asking for a
|
|
15
|
+
## smaller picture means: it was ignored unless both came, so the full-size picture was sent back.
|
|
16
|
+
static func scaled_to(drawn: Vector2i, width: int, height: int) -> Vector2i:
|
|
17
|
+
if width > 0 and height > 0:
|
|
18
|
+
return Vector2i(width, height)
|
|
19
|
+
if width > 0 and drawn.x > 0:
|
|
20
|
+
return Vector2i(width, maxi(1, roundi(float(drawn.y) * width / drawn.x)))
|
|
21
|
+
if height > 0 and drawn.y > 0:
|
|
22
|
+
return Vector2i(maxi(1, roundi(float(drawn.x) * height / drawn.y)), height)
|
|
23
|
+
return drawn
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## [param image] saved as a PNG at [param png_path], and as a JPEG beside it, which the server sends
|
|
27
|
+
## instead when it is the smaller: a rendered frame is several times smaller as one, and every
|
|
28
|
+
## picture sent stays in the caller's session. Not for a picture with transparency, which a JPEG
|
|
29
|
+
## would flatten.
|
|
30
|
+
static func saved(image: Image, png_path: String) -> Dictionary:
|
|
31
|
+
var save_error: Error = image.save_png(png_path)
|
|
32
|
+
if save_error != OK:
|
|
33
|
+
return {
|
|
34
|
+
"type": "error",
|
|
35
|
+
"message": "Failed to save screenshot as PNG: " + str(save_error),
|
|
36
|
+
}
|
|
37
|
+
var answer: Dictionary = {
|
|
38
|
+
"type": "screenshot_file",
|
|
39
|
+
"format": "png",
|
|
40
|
+
"width": image.get_width(),
|
|
41
|
+
"height": image.get_height(),
|
|
42
|
+
"path": png_path,
|
|
43
|
+
}
|
|
44
|
+
if image.detect_alpha() == Image.ALPHA_NONE:
|
|
45
|
+
var jpeg_path: String = png_path.get_basename() + ".jpg"
|
|
46
|
+
if image.save_jpg(jpeg_path, JPEG_QUALITY) == OK:
|
|
47
|
+
answer["jpegPath"] = jpeg_path
|
|
48
|
+
return answer
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://c2lleb9sm7gnl
|
|
@@ -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.
|
|
@@ -106,14 +161,39 @@ func _on_disconnected() -> void:
|
|
|
106
161
|
## Whether [param tool_name] waits for the editor to finish scanning or importing. A save during a
|
|
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.
|
|
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.
|
|
109
170
|
func turned_away(tool_name: String) -> bool:
|
|
171
|
+
if _tool_executor != null and _tool_executor.capturing():
|
|
172
|
+
return true
|
|
173
|
+
if not ANSWERED_DURING_A_TASK.has(tool_name) and GdhSceneFile.in_a_task():
|
|
174
|
+
return true
|
|
110
175
|
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
111
176
|
return (
|
|
112
177
|
not ANSWERED_WHILE_SCANNING.has(tool_name)
|
|
113
|
-
and (
|
|
178
|
+
and (GdhSceneFile.scan_unfinished(filesystem) or filesystem.is_importing())
|
|
114
179
|
)
|
|
115
180
|
|
|
116
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
|
+
|
|
117
197
|
func _on_tool_requested(request_id: String, tool_name: String, args: Dictionary) -> void:
|
|
118
198
|
if _tool_executor == null or _client == null:
|
|
119
199
|
return
|
|
@@ -121,10 +201,11 @@ func _on_tool_requested(request_id: String, tool_name: String, args: Dictionary)
|
|
|
121
201
|
# Turned away rather than held, so the server asks again: a call held here could run after the
|
|
122
202
|
# server had given up on it.
|
|
123
203
|
if turned_away(tool_name):
|
|
124
|
-
|
|
204
|
+
var busy: Array[String] = busy_with()
|
|
205
|
+
_client.send_busy(request_id, tool_name, busy[0], busy[1])
|
|
125
206
|
return
|
|
126
207
|
|
|
127
|
-
var result: Dictionary = _tool_executor.execute_tool(tool_name, args)
|
|
208
|
+
var result: Dictionary = await _tool_executor.execute_tool(tool_name, args)
|
|
128
209
|
var success: bool = result.get("ok", false)
|
|
129
210
|
|
|
130
211
|
if success:
|
|
@@ -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)
|
|
@@ -1197,6 +1313,11 @@ func _strays(state: SceneState) -> Array[Array]:
|
|
|
1197
1313
|
continue
|
|
1198
1314
|
if not held and GdhHeadlessCheck.at_default(node, name):
|
|
1199
1315
|
continue
|
|
1316
|
+
# Set in the file to what the node would have anyway, such as an instance naming the
|
|
1317
|
+
# script its own scene gives it. The pack leaves it out, as the editor's does, and the
|
|
1318
|
+
# node loads the same.
|
|
1319
|
+
if held and not stored.has(name) and GdhSame.same(node.get(name), filed[name]):
|
|
1320
|
+
continue
|
|
1200
1321
|
if _engine_kept.has("%d:%s" % [id, name]):
|
|
1201
1322
|
continue
|
|
1202
1323
|
stray.append([node, name, held, filed.get(name)])
|
|
@@ -17,6 +17,7 @@ const GdhClassTools = preload("tools/class_tools.gd")
|
|
|
17
17
|
const GdhSettingsTools = preload("tools/settings_tools.gd")
|
|
18
18
|
const GdhFilesystemTools = preload("tools/filesystem_tools.gd")
|
|
19
19
|
const GdhProfileTools = preload("tools/profile_tools.gd")
|
|
20
|
+
const GdhViewTools = preload("tools/view_tools.gd")
|
|
20
21
|
|
|
21
22
|
var _editor_plugin: EditorPlugin = null
|
|
22
23
|
var _scene_tools: GdhSceneTools = null
|
|
@@ -31,6 +32,7 @@ var _class_tools: GdhClassTools = null
|
|
|
31
32
|
var _settings_tools: GdhSettingsTools = null
|
|
32
33
|
var _filesystem_tools: GdhFilesystemTools = null
|
|
33
34
|
var _profile_tools: GdhProfileTools = null
|
|
35
|
+
var _view_tools: GdhViewTools = null
|
|
34
36
|
var _tool_map: Dictionary = {}
|
|
35
37
|
var _initialized: bool = false
|
|
36
38
|
|
|
@@ -48,6 +50,7 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
|
48
50
|
_class_tools.set_editor_plugin(plugin)
|
|
49
51
|
_filesystem_tools.set_editor_plugin(plugin)
|
|
50
52
|
_profile_tools.set_editor_plugin(plugin)
|
|
53
|
+
_view_tools.set_editor_plugin(plugin)
|
|
51
54
|
|
|
52
55
|
|
|
53
56
|
func _init_tools() -> void:
|
|
@@ -103,6 +106,10 @@ func _init_tools() -> void:
|
|
|
103
106
|
_profile_tools.name = "ProfileTools"
|
|
104
107
|
add_child(_profile_tools)
|
|
105
108
|
|
|
109
|
+
_view_tools = GdhViewTools.new()
|
|
110
|
+
_view_tools.name = "ViewTools"
|
|
111
|
+
add_child(_view_tools)
|
|
112
|
+
|
|
106
113
|
_tool_map = {
|
|
107
114
|
# Scene tools
|
|
108
115
|
"create_scene": [_scene_tools, "create_scene"],
|
|
@@ -111,6 +118,7 @@ func _init_tools() -> void:
|
|
|
111
118
|
"delete_node": [_scene_tools, "delete_node"],
|
|
112
119
|
"duplicate_node": [_scene_tools, "duplicate_node"],
|
|
113
120
|
"reparent_node": [_scene_tools, "reparent_node"],
|
|
121
|
+
"save_branch": [_scene_tools, "save_branch"],
|
|
114
122
|
"set_node_properties": [_scene_tools, "set_node_properties"],
|
|
115
123
|
"get_node_properties": [_scene_tools, "get_node_properties"],
|
|
116
124
|
"bake_navigation": [_scene_tools, "bake_navigation"],
|
|
@@ -148,6 +156,7 @@ func _init_tools() -> void:
|
|
|
148
156
|
# The script profiler on a game the editor plays
|
|
149
157
|
"profile_start": [_profile_tools, "profile_start"],
|
|
150
158
|
"profile_read": [_profile_tools, "profile_read"],
|
|
159
|
+
"capture_editor_view": [_view_tools, "capture_editor_view"],
|
|
151
160
|
# Animation tools
|
|
152
161
|
"read_animations": [_animation_tools, "read_animations"],
|
|
153
162
|
"read_animation_states": [_animation_tools, "read_animation_states"],
|
|
@@ -164,6 +173,11 @@ func _init_tools() -> void:
|
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
|
|
176
|
+
## Whether a capture is waiting for the editor to draw, which another call must not move it from.
|
|
177
|
+
func capturing() -> bool:
|
|
178
|
+
return _view_tools != null and _view_tools.capturing
|
|
179
|
+
|
|
180
|
+
|
|
167
181
|
func execute_tool(tool_name: String, args: Dictionary) -> Dictionary:
|
|
168
182
|
if not _tool_map.has(tool_name):
|
|
169
183
|
return { "ok": false, "error": "Unknown tool: " + tool_name }
|
|
@@ -177,7 +191,8 @@ func execute_tool(tool_name: String, args: Dictionary) -> Dictionary:
|
|
|
177
191
|
|
|
178
192
|
GdhSceneFile.also_saved.clear()
|
|
179
193
|
GdhSceneFile.unmade_note = ""
|
|
180
|
-
|
|
194
|
+
# Awaited for the tools that wait for the editor to draw. The rest answer at once.
|
|
195
|
+
var result: Variant = await node.call(method, args)
|
|
181
196
|
if result is Dictionary:
|
|
182
197
|
var answered: Dictionary = result
|
|
183
198
|
if not GdhSceneFile.also_saved.is_empty():
|
|
@@ -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
|
|