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.
Files changed (51) hide show
  1. package/build/cli.js +1470 -726
  2. package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
  3. package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
  4. package/build/godot/addons/gdharness_editor/reading.gd +17 -0
  5. package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
  6. package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
  7. package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
  8. package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
  9. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
  10. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
  11. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
  12. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
  13. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
  14. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
  15. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
  16. package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
  17. package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
  18. package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +15 -5
  20. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
  21. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
  22. package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
  23. package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
  24. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +89 -16
  25. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +107 -343
  26. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
  27. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
  28. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
  29. package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
  30. package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
  31. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +61 -28
  32. package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
  33. package/build/godot/operations/audio_buses.gd +47 -11
  34. package/build/godot/operations/class_cache.gd +18 -2
  35. package/build/godot/operations/classdb_queries.gd +28 -17
  36. package/build/godot/operations/dependencies.gd +128 -17
  37. package/build/godot/operations/file_walk.gd +20 -0
  38. package/build/godot/operations/gdscript_analysis.gd +59 -20
  39. package/build/godot/operations/gdscript_authoring.gd +127 -60
  40. package/build/godot/operations/godot_operations.gd +24 -5
  41. package/build/godot/operations/import_pipeline.gd +179 -19
  42. package/build/godot/operations/logger.gd +2 -2
  43. package/build/godot/operations/plugins.gd +88 -70
  44. package/build/godot/operations/project_config.gd +41 -6
  45. package/build/godot/operations/project_diagnostics.gd +27 -11
  46. package/build/godot/operations/reading.gd +17 -0
  47. package/build/godot/operations/resource_files.gd +60 -17
  48. package/build/godot/operations/serialisation.gd +116 -7
  49. package/build/index.js +1385 -553
  50. package/build/keeper.js +4 -0
  51. 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 path: String = str(args.get("scriptPath", ""))
53
- if path.is_empty():
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. The old text goes back
84
- # into the object, so what it holds and the source it says it was built from still agree.
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
- if failed != OK:
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
@@ -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 scene_path.is_empty():
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": EditorInterface.is_playing_scene(),
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
- if EditorInterface.is_playing_scene():
120
- EditorInterface.stop_playing_scene()
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
- if EditorInterface.is_playing_scene():
140
- EditorInterface.stop_playing_scene()
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
  }