gdharness 1.1.21 → 1.1.23
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 +1470 -726
- package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
- package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
- package/build/godot/addons/gdharness_editor/reading.gd +17 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
- package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
- package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +15 -5
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +89 -16
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +107 -343
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +61 -28
- package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
- package/build/godot/operations/audio_buses.gd +47 -11
- package/build/godot/operations/class_cache.gd +18 -2
- package/build/godot/operations/classdb_queries.gd +28 -17
- package/build/godot/operations/dependencies.gd +128 -17
- package/build/godot/operations/file_walk.gd +20 -0
- package/build/godot/operations/gdscript_analysis.gd +59 -20
- package/build/godot/operations/gdscript_authoring.gd +127 -60
- package/build/godot/operations/godot_operations.gd +24 -5
- package/build/godot/operations/import_pipeline.gd +179 -19
- package/build/godot/operations/logger.gd +2 -2
- package/build/godot/operations/plugins.gd +88 -70
- package/build/godot/operations/project_config.gd +41 -6
- package/build/godot/operations/project_diagnostics.gd +27 -11
- package/build/godot/operations/reading.gd +17 -0
- package/build/godot/operations/resource_files.gd +60 -17
- package/build/godot/operations/serialisation.gd +116 -7
- package/build/index.js +1385 -553
- package/build/keeper.js +4 -0
- package/package.json +1 -1
|
@@ -49,9 +49,10 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
|
49
49
|
## returned OK and compiled nothing is exactly the failure worth catching, and the members it now
|
|
50
50
|
## has are the only thing that can tell those apart.
|
|
51
51
|
func reload_script(args: Dictionary) -> Dictionary:
|
|
52
|
-
var
|
|
53
|
-
if
|
|
52
|
+
var asked: String = str(args.get("scriptPath", ""))
|
|
53
|
+
if asked.is_empty():
|
|
54
54
|
return {"ok": false, "error": "scriptPath is required"}
|
|
55
|
+
var path: String = _spelled_as_on_disk(asked)
|
|
55
56
|
if not ResourceLoader.exists(path):
|
|
56
57
|
return {"ok": false, "error": "No script at " + path}
|
|
57
58
|
|
|
@@ -80,21 +81,58 @@ func reload_script(args: Dictionary) -> Dictionary:
|
|
|
80
81
|
|
|
81
82
|
# A reload that fails is still answered as a call that ran, because the readings are what a
|
|
82
83
|
# caller needs then: a failure travels back as its message alone, and a project met one and
|
|
83
|
-
# could not tell whether the copy had been damaged by it or before it.
|
|
84
|
-
#
|
|
84
|
+
# could not tell whether the copy had been damaged by it or before it.
|
|
85
|
+
#
|
|
86
|
+
# And the old text is compiled again. A failed reload leaves the object unusable while its member
|
|
87
|
+
# lists read as before, measured on 4.7.2: it can make no instance, and an instance it already made
|
|
88
|
+
# has lost its methods, and the old text put back uncompiled leaves it that way. Compiling the text
|
|
89
|
+
# it was built from puts it back, and whether that worked is said, since the text may no longer
|
|
90
|
+
# compile against what it names.
|
|
85
91
|
var failed: Error = script.reload(true)
|
|
86
|
-
|
|
87
|
-
script.source_code = previous
|
|
88
|
-
return {
|
|
92
|
+
var answer: Dictionary = {
|
|
89
93
|
"ok": true,
|
|
90
94
|
"script": path,
|
|
91
95
|
"failed": failed,
|
|
92
96
|
"failedAs": error_string(failed) if failed != OK else "",
|
|
93
97
|
"heldBefore": held,
|
|
94
|
-
"methods": _method_names(script),
|
|
95
98
|
"heldConstantsBefore": held_constants,
|
|
96
|
-
"constants": _constant_names(script),
|
|
97
99
|
}
|
|
100
|
+
if failed != OK:
|
|
101
|
+
script.source_code = previous
|
|
102
|
+
var restored: Error = script.reload(true)
|
|
103
|
+
answer["restored"] = restored == OK
|
|
104
|
+
if restored != OK:
|
|
105
|
+
answer["restoredAs"] = error_string(restored)
|
|
106
|
+
answer["methods"] = _method_names(script)
|
|
107
|
+
answer["constants"] = _constant_names(script)
|
|
108
|
+
return answer
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
## [param path] as the files on disk spell it, a step at a time, or as given where a step is not
|
|
112
|
+
## there. On a filesystem that ignores case, `res://Core/Rules.gd` opens the file at
|
|
113
|
+
## `res://core/rules.gd`, but the editor holds its copy under the spelling on disk, and a load by
|
|
114
|
+
## another spelling misses that copy and compiles a new one: reloading that answers with a fresh
|
|
115
|
+
## compile's members and leaves the copy the editor holds as it was.
|
|
116
|
+
static func _spelled_as_on_disk(path: String) -> String:
|
|
117
|
+
var local: String = ProjectSettings.localize_path(path)
|
|
118
|
+
if not local.begins_with("res://"):
|
|
119
|
+
return local
|
|
120
|
+
var spelled: String = "res://"
|
|
121
|
+
for step: String in local.trim_prefix("res://").split("/", false):
|
|
122
|
+
var listing: DirAccess = DirAccess.open(spelled)
|
|
123
|
+
if listing == null:
|
|
124
|
+
return local
|
|
125
|
+
var found: String = ""
|
|
126
|
+
for entry: String in listing.get_directories() + listing.get_files():
|
|
127
|
+
if entry == step:
|
|
128
|
+
found = entry
|
|
129
|
+
break
|
|
130
|
+
if found.is_empty() and entry.nocasecmp_to(step) == 0:
|
|
131
|
+
found = entry
|
|
132
|
+
if found.is_empty():
|
|
133
|
+
return local
|
|
134
|
+
spelled = spelled.path_join(found)
|
|
135
|
+
return spelled
|
|
98
136
|
|
|
99
137
|
|
|
100
138
|
func _method_names(script: GDScript) -> Array[String]:
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends Node
|
|
3
|
+
|
|
4
|
+
## The editor's view of the files on disk: scans, the reimports asked for, and where each stands.
|
|
5
|
+
|
|
6
|
+
const Read = preload("../reading.gd")
|
|
7
|
+
|
|
8
|
+
var _editor_plugin: EditorPlugin = null
|
|
9
|
+
|
|
10
|
+
## How many scans the editor has completed, counted off `sources_changed`, which it emits at the end
|
|
11
|
+
## of every scan once the reimport the scan found is done: the one moment a scan is over. Neither
|
|
12
|
+
## flag it offers says so. A threaded scan stops reporting itself when its thread finishes, and its
|
|
13
|
+
## import runs a frame or more later when the thread is joined; an editor in the background took
|
|
14
|
+
## long enough over that frame that a rescan answered before the import of 211 changed scenes began.
|
|
15
|
+
var _scans_completed: int = 0
|
|
16
|
+
|
|
17
|
+
## When the last scan completed, on the engine's clock, or -1 before the first. The editor writes
|
|
18
|
+
## the class cache a frame after it stops reporting a scan, measured on 4.7.2, so a game started
|
|
19
|
+
## in between reads a file being rewritten; how long ago the scan ended is what tells a caller
|
|
20
|
+
## whether that write can still be coming.
|
|
21
|
+
var _scan_finished_at_msec: int = -1
|
|
22
|
+
|
|
23
|
+
## How many reimports asked for through [method reimport_files] have finished. Counted here rather
|
|
24
|
+
## than off `resources_reimported`, which a scan's own import emits as well.
|
|
25
|
+
var _reimports_completed: int = 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
29
|
+
_editor_plugin = plugin
|
|
30
|
+
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
31
|
+
if not filesystem.sources_changed.is_connected(_scan_completed):
|
|
32
|
+
var watched: int = filesystem.sources_changed.connect(_scan_completed)
|
|
33
|
+
if watched != OK:
|
|
34
|
+
push_error("gdharness could not watch for completed scans: %d" % watched)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
func _scan_completed(_sources_exist: bool) -> void:
|
|
38
|
+
_scans_completed += 1
|
|
39
|
+
_scan_finished_at_msec = Time.get_ticks_msec()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Rescan the project filesystem, and report whether a scan is still running.
|
|
43
|
+
##
|
|
44
|
+
## The editor rescans when its window regains focus, so a script written by anything other
|
|
45
|
+
## than the editor stays invisible until someone clicks on Godot. Until then its
|
|
46
|
+
## `class_name` is missing from the global class list and the language server reports every
|
|
47
|
+
## use of it as an unknown type, which is godotengine/godot#42786.
|
|
48
|
+
##
|
|
49
|
+
## Returns as soon as the scan is queued rather than awaiting it, because the tool executor
|
|
50
|
+
## takes a Dictionary and not a coroutine. Pass `statusOnly` to poll without starting
|
|
51
|
+
## another scan.
|
|
52
|
+
func rescan_filesystem(args: Dictionary) -> Dictionary:
|
|
53
|
+
if not _editor_plugin:
|
|
54
|
+
return {"ok": false, "error": "Editor plugin unavailable"}
|
|
55
|
+
|
|
56
|
+
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
57
|
+
# Not over a scan or an import the editor is already running: a scan asked for then starts a
|
|
58
|
+
# second reimport over the first, and the editor logs "Task 'reimport' already exists" and two
|
|
59
|
+
# conditions from its progress dialog. Declined and said, so the caller waits and asks again.
|
|
60
|
+
var busy: bool = filesystem.is_scanning() or filesystem.is_importing()
|
|
61
|
+
var started: bool = false
|
|
62
|
+
var completed_before: int = _scans_completed
|
|
63
|
+
var uids: Dictionary = {"reread": [], "duplicated": []}
|
|
64
|
+
if not Read.as_bool(args.get("statusOnly", false)) and not busy:
|
|
65
|
+
uids = _reread_changed_uids(filesystem)
|
|
66
|
+
filesystem.scan()
|
|
67
|
+
# Whether the scan ran, read off the editor rather than assumed. `scan()` returns without
|
|
68
|
+
# a word while the thread of the scan before is still to be joined, which is a frame or
|
|
69
|
+
# more after that scan stops reporting itself, and that scan then signals as it is joined.
|
|
70
|
+
# Taken as started, the answer came from a scan begun before the files were written:
|
|
71
|
+
# downstream, a rescan after 698 files were rewritten answered finished in 1.5s and the
|
|
72
|
+
# one changed file stayed on its old import. A threaded scan reports itself from inside
|
|
73
|
+
# the call, and one run on this thread has finished and signalled before it returns.
|
|
74
|
+
started = filesystem.is_scanning() or _scans_completed != completed_before
|
|
75
|
+
|
|
76
|
+
# Importing is reported separately from scanning, and a class is not registered until
|
|
77
|
+
# both are done, so a caller watching only one of them can look too early. The two counts are
|
|
78
|
+
# what a caller waits on: the scan it asked for is over once the count has moved past the one
|
|
79
|
+
# from before it was asked.
|
|
80
|
+
return {
|
|
81
|
+
"ok": true,
|
|
82
|
+
"started": started,
|
|
83
|
+
"scanning": filesystem.is_scanning(),
|
|
84
|
+
"importing": filesystem.is_importing(),
|
|
85
|
+
"scansCompletedBefore": completed_before,
|
|
86
|
+
"scansCompleted": _scans_completed,
|
|
87
|
+
"uidsReread": uids["reread"],
|
|
88
|
+
"uidsDuplicatedOnDisk": uids["duplicated"],
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
## Has the editor read again every file whose `.uid` names a UID the editor does not hold for it.
|
|
93
|
+
##
|
|
94
|
+
## The scan re-reads a file when the file changes and not when only the `.uid` beside it does, so a
|
|
95
|
+
## script copied with its `.uid` and then given a fresh one keeps the copied UID in the editor, and
|
|
96
|
+
## every headless engine reading the editor's cache warns of a duplicate. Measured on 4.7.2: a scan
|
|
97
|
+
## after the `.uid` was rewritten left both scripts on one UID in the cache. What the editor holds
|
|
98
|
+
## is read off its UID table, which maps each UID to one path, so a `.uid` whose UID is missing from
|
|
99
|
+
## the table or mapped to another file is one the editor has not taken in.
|
|
100
|
+
##
|
|
101
|
+
## Two files on disk naming one UID are left alone and named: reading them again cannot settle
|
|
102
|
+
## which of them owns it, and doing it on every scan would only move the UID between them.
|
|
103
|
+
func _reread_changed_uids(filesystem: EditorFileSystem) -> Dictionary:
|
|
104
|
+
var sidecars: Array[String] = []
|
|
105
|
+
_uid_files("res://", sidecars)
|
|
106
|
+
var claimed: Dictionary = {}
|
|
107
|
+
for sidecar: String in sidecars:
|
|
108
|
+
var id: int = ResourceUID.text_to_id(FileAccess.get_file_as_string(sidecar).strip_edges())
|
|
109
|
+
var path: String = sidecar.trim_suffix(".uid")
|
|
110
|
+
if id == ResourceUID.INVALID_ID or not FileAccess.file_exists(path):
|
|
111
|
+
continue
|
|
112
|
+
var paths: Array = claimed.get_or_add(id, [])
|
|
113
|
+
paths.append(path)
|
|
114
|
+
var reread: Array[String] = []
|
|
115
|
+
var duplicated: Array[Array] = []
|
|
116
|
+
for id: int in claimed:
|
|
117
|
+
var paths: Array = claimed[id]
|
|
118
|
+
if paths.size() > 1:
|
|
119
|
+
paths.sort()
|
|
120
|
+
duplicated.append(paths)
|
|
121
|
+
continue
|
|
122
|
+
var path: String = str(paths[0])
|
|
123
|
+
if ResourceUID.has_id(id) and ResourceUID.get_id_path(id) == path:
|
|
124
|
+
continue
|
|
125
|
+
filesystem.update_file(path)
|
|
126
|
+
reread.append(path)
|
|
127
|
+
reread.sort()
|
|
128
|
+
duplicated.sort()
|
|
129
|
+
return {"reread": reread, "duplicated": duplicated}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
## Every `.uid` under [param directory], skipping what the editor skips: hidden directories, any
|
|
133
|
+
## holding a `.gdignore`, and any below the project's own that holds a `project.godot` of its own,
|
|
134
|
+
## which the editor passes over as another project. A nested project's UIDs are never in the editor's
|
|
135
|
+
## table, so without that last one its files would be read again on every rescan, and one holding a
|
|
136
|
+
## copy of an outer file's `.uid` named as a duplicate on disk.
|
|
137
|
+
func _uid_files(directory: String, found: Array[String]) -> void:
|
|
138
|
+
var listing: DirAccess = DirAccess.open(directory)
|
|
139
|
+
if listing == null or FileAccess.file_exists(directory.path_join(".gdignore")):
|
|
140
|
+
return
|
|
141
|
+
if directory != "res://" and FileAccess.file_exists(directory.path_join("project.godot")):
|
|
142
|
+
return
|
|
143
|
+
for child: String in listing.get_directories():
|
|
144
|
+
if not child.begins_with("."):
|
|
145
|
+
_uid_files(directory.path_join(child), found)
|
|
146
|
+
for file: String in listing.get_files():
|
|
147
|
+
if file.ends_with(".uid"):
|
|
148
|
+
found.append(directory.path_join(file))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Whether the editor is scanning, and how long ago its last scan finished, without asking for one.
|
|
152
|
+
##
|
|
153
|
+
## For a caller about to start a game: the editor rewrites the class cache a frame after a scan
|
|
154
|
+
## stops reporting, so a game booted during the scan or in that frame reads a file being rewritten.
|
|
155
|
+
## Apart from `rescan_filesystem` because asking must not start or count anything.
|
|
156
|
+
func scan_status(_args: Dictionary) -> Dictionary:
|
|
157
|
+
if not _editor_plugin:
|
|
158
|
+
return {"ok": false, "error": "Editor plugin unavailable"}
|
|
159
|
+
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
160
|
+
return {
|
|
161
|
+
"ok": true,
|
|
162
|
+
"scanning": filesystem.is_scanning(),
|
|
163
|
+
"importing": filesystem.is_importing(),
|
|
164
|
+
"sinceScanFinishedMs":
|
|
165
|
+
-1 if _scan_finished_at_msec < 0 else Time.get_ticks_msec() - _scan_finished_at_msec,
|
|
166
|
+
"reimportsCompleted": _reimports_completed,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
## Reimport the files named under `paths`, whatever their state, the way the editor's Reimport
|
|
171
|
+
## button does, and report whether the reimport was started.
|
|
172
|
+
##
|
|
173
|
+
## Started on the next frame rather than run here: the editor imports on its main thread, and a
|
|
174
|
+
## reimport of a few hundred scenes outlasts the wait on any one command. The caller waits for
|
|
175
|
+
## `reimportsCompleted` in [method scan_status] to move past `reimportsCompletedBefore`. Declined
|
|
176
|
+
## while the editor scans or imports, since `reimport_files` refuses to run inside an import.
|
|
177
|
+
func reimport_files(args: Dictionary) -> Dictionary:
|
|
178
|
+
if not _editor_plugin:
|
|
179
|
+
return {"ok": false, "error": "Editor plugin unavailable"}
|
|
180
|
+
var paths: Array[String] = []
|
|
181
|
+
var given: Variant = args.get("paths", [])
|
|
182
|
+
if given is Array:
|
|
183
|
+
for path: Variant in given:
|
|
184
|
+
paths.append(str(path))
|
|
185
|
+
if paths.is_empty():
|
|
186
|
+
return {"ok": false, "error": "reimport_files needs paths"}
|
|
187
|
+
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
188
|
+
var busy: bool = filesystem.is_scanning() or filesystem.is_importing()
|
|
189
|
+
if not busy:
|
|
190
|
+
_reimport.call_deferred(PackedStringArray(paths))
|
|
191
|
+
return {
|
|
192
|
+
"ok": true,
|
|
193
|
+
"started": not busy,
|
|
194
|
+
"scanning": filesystem.is_scanning(),
|
|
195
|
+
"importing": filesystem.is_importing(),
|
|
196
|
+
"reimportsCompletedBefore": _reimports_completed,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
func _reimport(paths: PackedStringArray) -> void:
|
|
201
|
+
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
202
|
+
# A scan can begin between the answer and this frame, and its import would refuse this one.
|
|
203
|
+
while filesystem.is_scanning() or filesystem.is_importing():
|
|
204
|
+
await get_tree().process_frame
|
|
205
|
+
filesystem.reimport_files(paths)
|
|
206
|
+
_reimports_completed += 1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://brmbmxql6hidu
|
|
@@ -19,32 +19,75 @@ const LOOPBACK: String = "127.0.0.1"
|
|
|
19
19
|
|
|
20
20
|
var _editor_plugin: EditorPlugin = null
|
|
21
21
|
|
|
22
|
+
## Which play this is, counted up for every one the editor starts, whoever starts it. What a server
|
|
23
|
+
## writes about a play is kept on disk after it, and the editor saying it is playing does not say
|
|
24
|
+
## which play: a server picking a play back up read the console of an earlier one as this one's.
|
|
25
|
+
var _play: int = 0
|
|
26
|
+
var _was_playing: bool = false
|
|
27
|
+
## A play asked for here that the editor has not begun yet, as an editor still scanning answers.
|
|
28
|
+
## Counted when it was asked for, so its beginning later is not counted again as a play of its own.
|
|
29
|
+
var _awaited: bool = false
|
|
30
|
+
|
|
22
31
|
|
|
23
32
|
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
24
33
|
_editor_plugin = plugin
|
|
25
34
|
|
|
26
35
|
|
|
36
|
+
func _process(_delta: float) -> void:
|
|
37
|
+
var playing: bool = EditorInterface.is_playing_scene()
|
|
38
|
+
if playing and not _was_playing:
|
|
39
|
+
if _awaited:
|
|
40
|
+
_awaited = false
|
|
41
|
+
else:
|
|
42
|
+
_play += 1
|
|
43
|
+
_was_playing = playing
|
|
44
|
+
|
|
45
|
+
|
|
27
46
|
func play_scene(args: Dictionary) -> Dictionary:
|
|
28
47
|
var scene_path: String = str(args.get("scenePath", ""))
|
|
29
48
|
|
|
49
|
+
var full_path: String = ""
|
|
50
|
+
if scene_path.is_empty():
|
|
51
|
+
# The editor puts up a dialog for a main scene that is not there and plays nothing, and the
|
|
52
|
+
# play was answered as started.
|
|
53
|
+
var main: String = str(ProjectSettings.get_setting("application/run/main_scene", ""))
|
|
54
|
+
if main.is_empty():
|
|
55
|
+
return {"ok": false, "error": "The project has no main scene to play."}
|
|
56
|
+
if not ResourceLoader.exists(main, "PackedScene"):
|
|
57
|
+
return {
|
|
58
|
+
"ok": false,
|
|
59
|
+
"error":
|
|
60
|
+
(
|
|
61
|
+
"The main scene, %s, is not there, so the editor would ask for another rather than play."
|
|
62
|
+
% main
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
else:
|
|
66
|
+
full_path = scene_path if scene_path.begins_with("res://") else "res://" + scene_path
|
|
67
|
+
if not ResourceLoader.exists(full_path, "PackedScene"):
|
|
68
|
+
return {"ok": false, "error": "No scene at " + full_path}
|
|
69
|
+
|
|
30
70
|
if EditorInterface.is_playing_scene():
|
|
31
71
|
EditorInterface.stop_playing_scene()
|
|
32
72
|
|
|
33
73
|
var debugger: int = _a_port_for_the_debugger()
|
|
34
74
|
|
|
35
|
-
if
|
|
75
|
+
if full_path.is_empty():
|
|
36
76
|
EditorInterface.play_main_scene()
|
|
37
77
|
else:
|
|
38
|
-
var full_path: String = scene_path if scene_path.begins_with("res://") else "res://" + scene_path
|
|
39
|
-
if not ResourceLoader.exists(full_path, "PackedScene"):
|
|
40
|
-
return {"ok": false, "error": "No scene at " + full_path}
|
|
41
78
|
EditorInterface.play_custom_scene(full_path)
|
|
42
79
|
|
|
80
|
+
_play += 1
|
|
81
|
+
_was_playing = EditorInterface.is_playing_scene()
|
|
82
|
+
_awaited = not _was_playing
|
|
83
|
+
|
|
43
84
|
return {
|
|
44
85
|
"ok": true,
|
|
45
|
-
"playing":
|
|
86
|
+
"playing": _was_playing,
|
|
46
87
|
"scenePath": EditorInterface.get_playing_scene(),
|
|
47
|
-
"debugPort": debugger
|
|
88
|
+
"debugPort": debugger,
|
|
89
|
+
"play": _play,
|
|
90
|
+
"editorPid": OS.get_process_id(),
|
|
48
91
|
}
|
|
49
92
|
|
|
50
93
|
|
|
@@ -116,10 +159,9 @@ func restart_editor(_args: Dictionary) -> Dictionary:
|
|
|
116
159
|
)
|
|
117
160
|
}
|
|
118
161
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
EditorInterface.save_all_scenes()
|
|
162
|
+
var unsaved: Dictionary = _save_everything()
|
|
163
|
+
if not unsaved.is_empty():
|
|
164
|
+
return unsaved
|
|
123
165
|
|
|
124
166
|
# Deferred so this answer is on its way out before the editor goes.
|
|
125
167
|
EditorInterface.restart_editor.call_deferred(true)
|
|
@@ -127,6 +169,38 @@ func restart_editor(_args: Dictionary) -> Dictionary:
|
|
|
127
169
|
return {"ok": true, "restarting": true, "saved": true}
|
|
128
170
|
|
|
129
171
|
|
|
172
|
+
## Saves every open scene, answering a refusal naming what is still unsaved, or {}.
|
|
173
|
+
##
|
|
174
|
+
## Read back rather than assumed: save_all_scenes answers nothing and passes over a scene that has
|
|
175
|
+
## never been saved, which has no path to go to, so an untitled scene was lost with the editor while
|
|
176
|
+
## the answer said everything was saved.
|
|
177
|
+
static func _save_everything() -> Dictionary:
|
|
178
|
+
if EditorInterface.is_playing_scene():
|
|
179
|
+
EditorInterface.stop_playing_scene()
|
|
180
|
+
EditorInterface.save_all_scenes()
|
|
181
|
+
var left: PackedStringArray = EditorInterface.get_unsaved_scenes()
|
|
182
|
+
if left.is_empty():
|
|
183
|
+
return {}
|
|
184
|
+
var named: Array[String] = []
|
|
185
|
+
for scene: String in left:
|
|
186
|
+
named.append(scene if not scene.is_empty() else "a scene that has never been saved")
|
|
187
|
+
return {
|
|
188
|
+
"ok": false,
|
|
189
|
+
"error":
|
|
190
|
+
(
|
|
191
|
+
(
|
|
192
|
+
"%s still %s unsaved changes after saving, and ending the editor would lose them. Save "
|
|
193
|
+
+ "or close %s in the editor first."
|
|
194
|
+
)
|
|
195
|
+
% [
|
|
196
|
+
", ".join(PackedStringArray(named)),
|
|
197
|
+
"has" if named.size() == 1 else "have",
|
|
198
|
+
"it" if named.size() == 1 else "them"
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
130
204
|
## Ends this editor, having saved, so that whoever opened it can open it again.
|
|
131
205
|
##
|
|
132
206
|
## The editor's half of restarting one a server started. Godot's own restart cannot carry the ports
|
|
@@ -136,10 +210,9 @@ func restart_editor(_args: Dictionary) -> Dictionary:
|
|
|
136
210
|
## Saved first, exactly as the restart above saves. The editor is going either way, and unsaved
|
|
137
211
|
## scenes are not this tool's to lose.
|
|
138
212
|
func quit_editor(_args: Dictionary) -> Dictionary:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
EditorInterface.save_all_scenes()
|
|
213
|
+
var unsaved: Dictionary = _save_everything()
|
|
214
|
+
if not unsaved.is_empty():
|
|
215
|
+
return unsaved
|
|
143
216
|
|
|
144
217
|
# Deferred so this answer is on its way out before the editor goes.
|
|
145
218
|
_leave.call_deferred()
|
|
@@ -168,5 +241,7 @@ func playing_status(_args: Dictionary) -> Dictionary:
|
|
|
168
241
|
"ok": true,
|
|
169
242
|
"playing": playing,
|
|
170
243
|
"scenePath": EditorInterface.get_playing_scene() if playing else "",
|
|
171
|
-
"debugPort": debugger
|
|
244
|
+
"debugPort": debugger,
|
|
245
|
+
"play": _play,
|
|
246
|
+
"editorPid": OS.get_process_id(),
|
|
172
247
|
}
|