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.
Files changed (25) hide show
  1. package/README.md +1 -1
  2. package/build/cli.js +19209 -17814
  3. package/build/godot/addons/gdharness_editor/bridge_client.gd +18 -5
  4. package/build/godot/addons/gdharness_editor/headless_check.gd +52 -0
  5. package/build/godot/addons/gdharness_editor/picture_saves.gd +48 -0
  6. package/build/godot/addons/gdharness_editor/picture_saves.gd.uid +1 -0
  7. package/build/godot/addons/gdharness_editor/plugin.gd +84 -3
  8. package/build/godot/addons/gdharness_editor/scene_file.gd +133 -12
  9. package/build/godot/addons/gdharness_editor/tool_executor.gd +16 -1
  10. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +3 -1
  11. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +9 -13
  12. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +87 -13
  13. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +20 -4
  14. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +51 -17
  15. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +473 -8
  16. package/build/godot/addons/gdharness_editor/tools/view_tools.gd +121 -0
  17. package/build/godot/addons/gdharness_editor/tools/view_tools.gd.uid +1 -0
  18. package/build/godot/addons/gdharness_runtime/picture_saves.gd +48 -0
  19. package/build/godot/addons/gdharness_runtime/picture_saves.gd.uid +1 -0
  20. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -43
  21. package/build/godot/operations/editor_tools.gd +1 -0
  22. package/build/godot/operations/gdscript_analysis.gd +17 -0
  23. package/build/godot/operations/picture_saves.gd +48 -0
  24. package/build/keeper.js +116 -114
  25. 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 = filesystem.is_scanning() or filesystem.is_importing()
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. `scan()` returns without
75
- # a word while the thread of the scan before is still to be joined, which is a frame or
76
- # more after that scan stops reporting itself, and that scan then signals as it is joined.
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": filesystem.is_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": filesystem.is_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 = filesystem.is_scanning() or filesystem.is_importing()
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": filesystem.is_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 filesystem.is_scanning() or filesystem.is_importing():
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] = [changed]
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 to: String = str(holder.get_path_to(target, unique))
412
- if unique:
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
- # Unbound and then bound again as Node.duplicate does: a callable made
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, aimed)
156
- and copy.connect(signal_name, aimed, flags) != OK
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
- ## Owns [param node] and the nodes under it to [param root], except those an instanced scene
167
- ## inside it owns: giving those to the root made the pack write the instance's own nodes into this
168
- ## scene as overrides.
169
- static func own(node: Node, root: Node) -> void:
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 == null or child.owner == root:
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: