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
|
@@ -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:
|
|
@@ -44,6 +44,9 @@ var _homes: Dictionary = {}
|
|
|
44
44
|
## rewrite that is not saved is put back.
|
|
45
45
|
var _outside: Array[Array] = []
|
|
46
46
|
var _scene: GdhSceneFile = null
|
|
47
|
+
## Each unique name a node of this scene had that is unique only inside an instance now, to the
|
|
48
|
+
## node's path from the root, for a %unique path through it to be written as the plain path.
|
|
49
|
+
var _enclosed: Dictionary = {}
|
|
47
50
|
var _moves: Dictionary = {}
|
|
48
51
|
var _back: Dictionary = {}
|
|
49
52
|
var _seen: Dictionary = {}
|
|
@@ -64,7 +67,7 @@ static func moved(given: String, moves: Dictionary) -> String:
|
|
|
64
67
|
## Carries [param moves], from the root of [param scene], into the paths it holds. Called after the
|
|
65
68
|
## move and before the write. Answers how many paths it rewrote.
|
|
66
69
|
func carry(scene: GdhSceneFile, moves: Dictionary) -> int:
|
|
67
|
-
if moves.is_empty():
|
|
70
|
+
if moves.is_empty() and _enclosed.is_empty():
|
|
68
71
|
return 0
|
|
69
72
|
_scene = scene
|
|
70
73
|
_moves = moves
|
|
@@ -110,6 +113,14 @@ func carry(scene: GdhSceneFile, moves: Dictionary) -> int:
|
|
|
110
113
|
return retargeted.size() - before
|
|
111
114
|
|
|
112
115
|
|
|
116
|
+
## Carries into [param scene] a branch saved as a scene of its own and instanced in its place, as
|
|
117
|
+
## [method carry] does: each %unique path through one of [param enclosed], a unique name to its
|
|
118
|
+
## node's path from the root, is written as the plain path. Answers how many paths it rewrote.
|
|
119
|
+
func carry_enclosed(scene: GdhSceneFile, enclosed: Dictionary) -> int:
|
|
120
|
+
_enclosed = enclosed
|
|
121
|
+
return carry(scene, {})
|
|
122
|
+
|
|
123
|
+
|
|
113
124
|
## Opens [param scene_path], carries [param moves] into it, and writes it when that rewrote a path.
|
|
114
125
|
## Answers "" or why it was left as it was.
|
|
115
126
|
func carry_into(scene_path: String, moves: Dictionary) -> String:
|
|
@@ -389,8 +400,12 @@ func _after(path: NodePath, was: String, now: String) -> Array:
|
|
|
389
400
|
var _named: bool = names.append(str(path.get_name(index)))
|
|
390
401
|
var start: String = was
|
|
391
402
|
var unique: bool = names[0].begins_with("%")
|
|
403
|
+
var enclosed: bool = false
|
|
392
404
|
if unique:
|
|
393
405
|
start = _unique_was(names[0].substr(1), now)
|
|
406
|
+
if start.is_empty() and _enclosed.has(names[0].substr(1)):
|
|
407
|
+
start = _enclosed[names[0].substr(1)]
|
|
408
|
+
enclosed = true
|
|
394
409
|
if start.is_empty():
|
|
395
410
|
return [UNCHANGED, ""]
|
|
396
411
|
names = names.slice(1)
|
|
@@ -398,7 +413,7 @@ func _after(path: NodePath, was: String, now: String) -> Array:
|
|
|
398
413
|
if target_was.is_empty():
|
|
399
414
|
return [UNCHANGED, ""]
|
|
400
415
|
var target_now: String = moved(target_was, _moves)
|
|
401
|
-
if target_now == target_was and now == was:
|
|
416
|
+
if target_now == target_was and now == was and not enclosed:
|
|
402
417
|
return [UNCHANGED, ""]
|
|
403
418
|
if target_now.is_empty():
|
|
404
419
|
return [DELETED, ""]
|
|
@@ -408,8 +423,9 @@ func _after(path: NodePath, was: String, now: String) -> Array:
|
|
|
408
423
|
return [UNCHANGED, ""]
|
|
409
424
|
if holder.get_node_or_null(NodePath(path.get_concatenated_names())) == target:
|
|
410
425
|
return [UNCHANGED, ""]
|
|
411
|
-
var
|
|
412
|
-
|
|
426
|
+
var through_unique: bool = unique and not enclosed
|
|
427
|
+
var to: String = str(holder.get_path_to(target, through_unique))
|
|
428
|
+
if through_unique:
|
|
413
429
|
to = _unique_to(start, target, to)
|
|
414
430
|
if path.get_subname_count() > 0:
|
|
415
431
|
to += ":" + path.get_concatenated_subnames()
|
|
@@ -6,6 +6,9 @@ extends RefCounted
|
|
|
6
6
|
const GdhFileWalk = preload("../file_walk.gd")
|
|
7
7
|
const GdhRead = preload("../reading.gd")
|
|
8
8
|
const GdhSame = preload("../same_values.gd")
|
|
9
|
+
## Control's layout modes, which the engine does not expose to scripts.
|
|
10
|
+
const POSITION_MODE: int = 0
|
|
11
|
+
const ANCHORS_MODE: int = 1
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
## The scene to instance for an add, or null for a node of a class, beside why neither can be made
|
|
@@ -142,18 +145,10 @@ static func reconnect(copies: Dictionary, top: Node) -> void:
|
|
|
142
145
|
and (target == top or top.is_ancestor_of(target as Node))
|
|
143
146
|
):
|
|
144
147
|
continue
|
|
145
|
-
|
|
146
|
-
# from the method alone would save the copy without the arguments the
|
|
147
|
-
# original passes.
|
|
148
|
-
var aimed: Callable = Callable(to, callable.get_method())
|
|
149
|
-
var unbound: int = callable.get_unbound_arguments_count()
|
|
150
|
-
if unbound > 0:
|
|
151
|
-
aimed = aimed.unbind(unbound)
|
|
152
|
-
if callable.get_bound_arguments_count() > 0:
|
|
153
|
-
aimed = aimed.bindv(callable.get_bound_arguments())
|
|
148
|
+
var joined: Callable = aimed(callable, to)
|
|
154
149
|
if (
|
|
155
|
-
not copy.is_connected(signal_name,
|
|
156
|
-
and copy.connect(signal_name,
|
|
150
|
+
not copy.is_connected(signal_name, joined)
|
|
151
|
+
and copy.connect(signal_name, joined, flags) != OK
|
|
157
152
|
):
|
|
158
153
|
push_error(
|
|
159
154
|
(
|
|
@@ -163,14 +158,29 @@ static func reconnect(copies: Dictionary, top: Node) -> void:
|
|
|
163
158
|
)
|
|
164
159
|
|
|
165
160
|
|
|
166
|
-
##
|
|
167
|
-
##
|
|
168
|
-
##
|
|
169
|
-
static func
|
|
161
|
+
## [param callable] aimed at [param to], unbound and then bound again as Node.duplicate does: a
|
|
162
|
+
## callable made from the method alone would save the connection without the arguments the original
|
|
163
|
+
## passes.
|
|
164
|
+
static func aimed(callable: Callable, to: Object) -> Callable:
|
|
165
|
+
var aimed_at: Callable = Callable(to, callable.get_method())
|
|
166
|
+
var unbound: int = callable.get_unbound_arguments_count()
|
|
167
|
+
if unbound > 0:
|
|
168
|
+
aimed_at = aimed_at.unbind(unbound)
|
|
169
|
+
if callable.get_bound_arguments_count() > 0:
|
|
170
|
+
aimed_at = aimed_at.bindv(callable.get_bound_arguments())
|
|
171
|
+
return aimed_at
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
## Owns [param node] to [param root], and under it the nodes the root owns already and those of
|
|
175
|
+
## [param placed], the copies a duplicate placed. A node nothing owns is not the scene's: a script's
|
|
176
|
+
## _init made it, which a headless engine runs and the editor does not, and owning it saved it into
|
|
177
|
+
## the scene, where the script makes it again at run time. An instanced scene's own nodes stay its
|
|
178
|
+
## own: giving those to the root made the pack write them into this scene as overrides.
|
|
179
|
+
static func own(node: Node, root: Node, placed: Dictionary = {}) -> void:
|
|
170
180
|
node.owner = root
|
|
171
181
|
for child: Node in node.get_children():
|
|
172
|
-
if child.owner ==
|
|
173
|
-
own(child, root)
|
|
182
|
+
if child.owner == root or placed.has(child):
|
|
183
|
+
own(child, root, placed)
|
|
174
184
|
|
|
175
185
|
|
|
176
186
|
## Why [param asked] is not a position [param node] can take among [param parent]'s children, or ""
|
|
@@ -195,6 +205,30 @@ static func unplaceable(parent: Node, node: Node, asked: Variant) -> String:
|
|
|
195
205
|
return ""
|
|
196
206
|
|
|
197
207
|
|
|
208
|
+
## Whether [param node], an instance of [param packed] just added, would load at its parent's
|
|
209
|
+
## top-left corner rather than where its own scene places it. A Control its scene stores as
|
|
210
|
+
## uncontrolled takes position mode under a Control that is not a Container, so the file stores
|
|
211
|
+
## layout_mode 0 and leaves out the offsets, which are the scene's own. Setting position mode moves
|
|
212
|
+
## a Control to the corner, so the node loads there. Anchors mode with the same anchors places it
|
|
213
|
+
## the same and leaves its offsets as they load.
|
|
214
|
+
static func cornered(node: Node, packed: PackedScene) -> bool:
|
|
215
|
+
var control: Control = node as Control
|
|
216
|
+
if control == null or packed == null:
|
|
217
|
+
return false
|
|
218
|
+
if GdhRead.as_int(control.get("layout_mode")) != POSITION_MODE:
|
|
219
|
+
return false
|
|
220
|
+
if control.offset_left == 0.0 and control.offset_top == 0.0:
|
|
221
|
+
return false
|
|
222
|
+
var state: SceneState = packed.get_state()
|
|
223
|
+
while state != null:
|
|
224
|
+
for property: int in state.get_node_property_count(0):
|
|
225
|
+
if str(state.get_node_property_name(0, property)) == "layout_mode":
|
|
226
|
+
var stored: int = GdhRead.as_int(state.get_node_property_value(0, property))
|
|
227
|
+
return stored != POSITION_MODE and stored != ANCHORS_MODE
|
|
228
|
+
state = state.get_base_scene_state()
|
|
229
|
+
return false
|
|
230
|
+
|
|
231
|
+
|
|
198
232
|
## Moves [param node] to [param asked] among its siblings, when a place was asked for.
|
|
199
233
|
static func place(node: Node, asked: Variant) -> void:
|
|
200
234
|
if asked != null:
|