gdharness 1.3.8 → 1.3.10

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 (78) hide show
  1. package/build/cli.js +4668 -3229
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
  7. package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
  8. package/build/godot/addons/gdharness_editor/reading.gd +112 -6
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
  10. package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
  11. package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
  13. package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
  14. package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
  17. package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
  18. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  20. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  23. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
  25. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  26. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
  27. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  28. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
  29. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
  30. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  31. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
  32. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  33. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  35. package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
  36. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  37. package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
  38. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  39. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
  40. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
  41. package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
  42. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  43. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  44. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
  45. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
  46. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  47. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
  49. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  50. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  51. package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
  52. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  53. package/build/godot/operations/animation_reads.gd +115 -4
  54. package/build/godot/operations/audio_buses.gd +93 -7
  55. package/build/godot/operations/classdb_queries.gd +119 -7
  56. package/build/godot/operations/declared_values.gd +272 -0
  57. package/build/godot/operations/dependencies.gd +154 -41
  58. package/build/godot/operations/editor_tools.gd +29 -13
  59. package/build/godot/operations/export_presets.gd +602 -4
  60. package/build/godot/operations/file_reads.gd +115 -18
  61. package/build/godot/operations/gdscript_analysis.gd +14 -0
  62. package/build/godot/operations/gdscript_authoring.gd +12 -2
  63. package/build/godot/operations/godot_operations.gd +18 -2
  64. package/build/godot/operations/import_pipeline.gd +20 -221
  65. package/build/godot/operations/input_actions.gd +127 -51
  66. package/build/godot/operations/project_config.gd +113 -68
  67. package/build/godot/operations/project_diagnostics.gd +4 -0
  68. package/build/godot/operations/project_validation.gd +266 -0
  69. package/build/godot/operations/property_paths.gd +89 -16
  70. package/build/godot/operations/property_values.gd +180 -121
  71. package/build/godot/operations/reading.gd +112 -6
  72. package/build/godot/operations/scene_connections.gd +222 -0
  73. package/build/godot/operations/serialisation.gd +19 -29
  74. package/build/godot/operations/setting_reads.gd +121 -0
  75. package/build/godot/operations/value_shapes.gd +128 -1
  76. package/build/godot/tests/listed.gd +20 -13
  77. package/build/keeper.js +1402 -707
  78. package/package.json +1 -1
@@ -8,6 +8,7 @@ extends RefCounted
8
8
  ## only true of a running game: reaching a node by path, and fitting an argument to the type the
9
9
  ## method it is going to declares.
10
10
 
11
+ const GdhDeclared = preload("declared_values.gd")
11
12
  const GdhRead = preload("reading.gd")
12
13
  const GdhSerialisation = preload("serialisation.gd")
13
14
  const GdhShapes = preload("value_shapes.gd")
@@ -31,6 +32,9 @@ const SINGLETONS: Array[String] = [
31
32
  ## The most frames a press is held in one call, as many as a frames wait takes.
32
33
  const MOST_HELD: int = 600
33
34
 
35
+ ## Read by [method _key_names], once a game.
36
+ static var _named_keys: PackedStringArray = []
37
+
34
38
  var _shared: GdhSerialisation = GdhSerialisation.new()
35
39
 
36
40
 
@@ -77,9 +81,18 @@ static func node_at(root: Node, path: String) -> Dictionary:
77
81
 
78
82
  ## "Node not found" for [param path], with the deepest node of it the game has and the children that
79
83
  ## node has instead, as a scene's refusal names them: a mistyped step was otherwise found by reading
80
- ## the whole tree.
84
+ ## the whole tree. A path written from the current scene rather than from the root, as a scene's
85
+ ## tools take one, is given the path that reaches it.
81
86
  static func _not_found(root: Node, path: String) -> String:
82
87
  var said: String = "Node not found: " + path
88
+ var scene: Node = root.get_tree().current_scene if root.is_inside_tree() else null
89
+ if not path.begins_with("/") and scene != null:
90
+ var meant: Node = scene.get_node_or_null(path)
91
+ if meant != null:
92
+ return (
93
+ "%s. Paths here go from the root, /root, not from the current scene, so this one is %s."
94
+ % [said, str(meant.get_path())]
95
+ )
83
96
  var parts: PackedStringArray = path.split("/", false)
84
97
  var at: Node = root
85
98
  var step: int = 0
@@ -160,12 +173,7 @@ static func glyph_of(keycode: Key, shifted: bool) -> int:
160
173
  ## engine's own names, printable keys and the special ones above [constant KEY_SPECIAL], since the
161
174
  ## game takes any name OS.find_keycode_from_string does.
162
175
  static func unknown_key(name: String) -> String:
163
- var names: PackedStringArray = []
164
- for code: int in range(32, 127) + range(KEY_SPECIAL + 1, KEY_SPECIAL + 0x100):
165
- var spelled: String = OS.get_keycode_string(code as Key)
166
- if not spelled.is_empty() and OS.find_keycode_from_string(spelled) == code:
167
- var _named: bool = names.append(spelled)
168
- var close: PackedStringArray = GdhRead.nearest(name, names)
176
+ var close: PackedStringArray = GdhRead.nearest(name, _key_names())
169
177
  var said: String = "No key is called %s." % name
170
178
  if close.size() == 1:
171
179
  said += " The nearest key name is %s." % close[0]
@@ -178,6 +186,151 @@ static func unknown_key(name: String) -> String:
178
186
  )
179
187
 
180
188
 
189
+ ## Every key name the engine takes, read once a game. The engine spells a code it has no name for as
190
+ ## the character it would be, and past the last code point that is an error it prints: 136 of them
191
+ ## for the special range, two seconds of console on a fast machine, more than a call's whole wait on
192
+ ## a loaded one, and each in the game's log as an error the game never made. So they are asked with
193
+ ## the engine's error printing off, which no other line of the main thread runs beside.
194
+ static func _key_names() -> PackedStringArray:
195
+ if not _named_keys.is_empty():
196
+ return _named_keys
197
+ var printing: bool = Engine.print_error_messages
198
+ Engine.print_error_messages = false
199
+ for code: int in range(32, 127) + range(KEY_SPECIAL + 1, KEY_SPECIAL + 0x100):
200
+ var spelled: String = OS.get_keycode_string(code as Key)
201
+ if not spelled.is_empty() and OS.find_keycode_from_string(spelled) == code:
202
+ var _named: bool = _named_keys.append(spelled)
203
+ Engine.print_error_messages = printing
204
+ return _named_keys
205
+
206
+
207
+ ## The refusal for [param action], which the input map lacks, with the nearest of the project's own
208
+ ## actions, or else all of them. The engine's ui_ ones are in every project, and offered for any name
209
+ ## they only buried the project's ("dance" is near ui_cancel), so they are offered for a ui_ name only.
210
+ static func no_action(action: String) -> String:
211
+ var engine: PackedStringArray = []
212
+ var own: PackedStringArray = []
213
+ for named: StringName in InputMap.get_actions():
214
+ if str(named).begins_with("ui_"):
215
+ var _listed: bool = engine.append(str(named))
216
+ else:
217
+ var _owned: bool = own.append(str(named))
218
+ own.sort()
219
+ var said: String = "Action not found: %s" % action
220
+ var close: PackedStringArray = GdhRead.nearest(action, engine if action.begins_with("ui_") else own)
221
+ if not close.is_empty():
222
+ return "%s; the nearest %s %s." % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
223
+ if own.is_empty():
224
+ return "%s; the project defines no actions of its own, and the engine's are the ui_ ones." % said
225
+ return "%s; the project's own actions are %s, beside the engine's ui_ ones." % [said, ", ".join(own)]
226
+
227
+
228
+ ## The actions of the input map that [param event] stands for, as Input reads it for
229
+ ## is_action_pressed: a key or a button bound to an action presses that action too, so a game
230
+ ## counting the action counted the key, and the answer had said only that a key went in.
231
+ static func actions_of(event: InputEvent) -> Array[String]:
232
+ var named: Array[String] = []
233
+ # A motion matches every action on its axis, either way and at any distance, which would name
234
+ # ui_left for a stick held right; Input presses only those its direction reaches past their
235
+ # deadzone.
236
+ var moved: bool = event is InputEventJoypadMotion
237
+ for action: StringName in InputMap.get_actions():
238
+ if event.is_action_pressed(action) if moved else InputMap.event_is_action(event, action):
239
+ named.append(str(action))
240
+ named.sort()
241
+ return named
242
+
243
+
244
+ ## The path of the Control holding the keyboard focus in [param host]'s viewport, or "" when none
245
+ ## does. Read before an event goes in, since pressing a focused button can move the focus.
246
+ static func focus_path(host: Node) -> String:
247
+ var holding: Control = host.get_viewport().gui_get_focus_owner()
248
+ return "" if holding == null else str(holding.get_path())
249
+
250
+
251
+ ## [param said] with [param focused], the Control that had the focus, under focused. A key, an action
252
+ ## or a pad's event reaches that Control before the game's _unhandled_input: Space on a focused Button
253
+ ## pressed it and the game's own handler counted the key as well, and a stick pushed down moved a
254
+ ## focused list's selection and never reached the game, with nothing in the answer saying why.
255
+ ##
256
+ ## Where the focus is once the event has been handled goes under focused_after when it moved, null
257
+ ## when nothing holds it then: ui_down on a focused button hands the focus to its neighbour, and an
258
+ ## answer naming only the button that took the press read as the focus never having moved.
259
+ static func focus_noted(said: Dictionary, focused: String, host: Node) -> Dictionary:
260
+ if not focused.is_empty():
261
+ said["focused"] = focused
262
+ var now: String = focus_path(host)
263
+ if now.is_empty() and not focused.is_empty():
264
+ said["focused_after"] = null
265
+ elif now != focused:
266
+ said["focused_after"] = now
267
+ return said
268
+
269
+
270
+ ## Presses the buttons under [param root] whose Shortcut [param action] matches, as the player's key
271
+ ## bound to it would, and answers their paths under "shortcuts" and the one that took the press
272
+ ## under "shortcut_pressed". A Viewport hands a shortcut only keys, joypad buttons and
273
+ ## InputEventShortcut, so an action pressed on its own never reached a Button whose Shortcut holds
274
+ ## that action, which is how a game gives a button a binding the player can change.
275
+ ##
276
+ ## A key goes to the shortcuts last in the tree first and stops at the first button that takes it,
277
+ ## while an InputEventShortcut reaches only the buttons holding that one Shortcut. So each Shortcut
278
+ ## is sent in that order, a frame apart, until a button is pressed; a button still decides by its
279
+ ## disabled, its visibility and its shortcut context, as it does for the key.
280
+ static func press_shortcuts(root: Node, action: String) -> Dictionary:
281
+ var pressing: InputEventAction = InputEventAction.new()
282
+ pressing.action = action
283
+ pressing.pressed = true
284
+ var buttons: Array[BaseButton] = []
285
+ for node: Node in root.find_children("*", "BaseButton", true, false):
286
+ var button: BaseButton = node
287
+ if button.shortcut != null and button.shortcut.matches_event(pressing):
288
+ buttons.append(button)
289
+ var paths: Array[String] = []
290
+ for button: BaseButton in buttons:
291
+ paths.append(str(button.get_path()))
292
+ buttons.reverse()
293
+ var taken: Array[String] = []
294
+ var heard: Array[Callable] = []
295
+ for button: BaseButton in buttons:
296
+ var path: String = str(button.get_path())
297
+ var hearing: Callable = func() -> void: taken.append(path)
298
+ heard.append(hearing)
299
+ var _listening: int = button.pressed.connect(hearing)
300
+ var sent: Dictionary = {}
301
+ for button: BaseButton in buttons:
302
+ if not taken.is_empty():
303
+ break
304
+ if sent.has(button.shortcut):
305
+ continue
306
+ sent[button.shortcut] = true
307
+ var through: InputEventShortcut = InputEventShortcut.new()
308
+ through.shortcut = button.shortcut
309
+ Input.parse_input_event(through)
310
+ await root.get_tree().process_frame
311
+ for at: int in buttons.size():
312
+ if is_instance_valid(buttons[at]):
313
+ buttons[at].pressed.disconnect(heard[at])
314
+ var pressed: Variant = null
315
+ if not taken.is_empty():
316
+ pressed = taken[0]
317
+ return {"shortcuts": paths, "shortcut_pressed": pressed}
318
+
319
+
320
+ ## The methods [param object] answers to, as the engine lists them: for a script itself, the static
321
+ ## functions it declares before the methods every script has. The engine's list for a script holds
322
+ ## only those, so a static function called on its class was read as taking nothing.
323
+ static func methods_of(object: Object) -> Array[Dictionary]:
324
+ var listed: Array[Dictionary] = []
325
+ if object is Script:
326
+ var script: Script = object
327
+ for entry: Dictionary in script.get_script_method_list():
328
+ if GdhRead.as_int(entry.get("flags", 0)) & METHOD_FLAG_STATIC:
329
+ listed.append(entry)
330
+ listed.append_array(object.get_method_list())
331
+ return listed
332
+
333
+
181
334
  ## What [param path] names: a node under [param root], as [method node_at] reads it, or an engine
182
335
  ## singleton by its name. Answers the object under "object", and under "node" the node that objects
183
336
  ## named in a value are found from, which for a singleton is [param root].
@@ -203,6 +356,32 @@ static func target_at(root: Node, path: String) -> Dictionary:
203
356
  return {"object": standing["node"], "node": standing["node"]}
204
357
 
205
358
 
359
+ ## The script [param path] names for a call of one of its static functions, as [method target_at]
360
+ ## answers a node, with [param root] as the node objects named in its arguments are found from: a
361
+ ## script by its res:// path, or the global class of that name where no node is at [param path].
362
+ ## Answers {} when it names a node or no class, and a message for a res:// path holding no script.
363
+ ## A class no node carries could not be called at all, and each way of naming it read as a node or
364
+ ## a method misspelt.
365
+ static func class_at(root: Node, path: String) -> Dictionary:
366
+ var script_path: String = path
367
+ if not path.begins_with("res://"):
368
+ if not path.is_valid_ascii_identifier() or not node_at(root, path).has("message"):
369
+ return {}
370
+ script_path = ""
371
+ for declared: Dictionary in ProjectSettings.get_global_class_list():
372
+ if str(declared.get("class", "")) == path:
373
+ script_path = str(declared.get("path", ""))
374
+ if script_path.is_empty():
375
+ return {}
376
+ var loaded: Resource = load(script_path) if ResourceLoader.exists(script_path) else null
377
+ if not loaded is Script:
378
+ return {
379
+ "type": "error",
380
+ "message": "No script is at %s to call a static function of." % script_path,
381
+ }
382
+ return {"object": loaded, "node": root}
383
+
384
+
206
385
  ## Whether two values can be compared without the comparison itself failing.
207
386
  static func comparable(one: Variant, other: Variant) -> bool:
208
387
  return GdhSerialisation.comparable(one, other)
@@ -253,7 +432,7 @@ static func remedy(value: Variant, type: int) -> String:
253
432
  ## The declared type of one parameter of a method, or TYPE_NIL when the method or the parameter
254
433
  ## is not there, which reads as "no opinion".
255
434
  func parameter_type(object: Object, method: String, index: int) -> int:
256
- for entry: Dictionary in object.get_method_list():
435
+ for entry: Dictionary in methods_of(object):
257
436
  if entry.get("name", "") != method:
258
437
  continue
259
438
  var params: Array = entry.get("args", [])
@@ -264,10 +443,59 @@ func parameter_type(object: Object, method: String, index: int) -> int:
264
443
  return TYPE_NIL
265
444
 
266
445
 
446
+ ## The values of the enum argument [param index] of [param method] declares, by name, as
447
+ ## `{"enum": "Mood", "values": {...}}`, or {} when it declares none. The engine gives such an argument
448
+ ## the type int and the enum's name, as res://main.gd.Mood or Node.ProcessMode.
449
+ func parameter_enum(object: Object, method: String, index: int) -> Dictionary:
450
+ for entry: Dictionary in methods_of(object):
451
+ if entry.get("name", "") != method:
452
+ continue
453
+ var params: Array = entry.get("args", [])
454
+ if index < 0 or index >= params.size():
455
+ return {}
456
+ var parameter: Dictionary = params[index]
457
+ if GdhRead.as_int(parameter.get("usage", 0)) & PROPERTY_USAGE_CLASS_IS_ENUM == 0:
458
+ return {}
459
+ var qualified: String = str(parameter.get("class_name", ""))
460
+ var enum_name: String = qualified.get_slice(".", qualified.get_slice_count(".") - 1)
461
+ var values: Dictionary = GdhDeclared.enum_declared_as(qualified)
462
+ if values.is_empty():
463
+ values = _enum_along(object if object is Script else object.get_script(), enum_name)
464
+ if values.is_empty():
465
+ return {}
466
+ return {"enum": enum_name, "values": values}
467
+ return {}
468
+
469
+
470
+ ## The enum [param named] declared by [param script] or a script it extends, or {}. A script built in
471
+ ## a scene or made at run time names its enums by an id nothing loads, gdscript://-92233….gd.Mood
472
+ ## measured on 4.7.2, and a method of the object called declares its enums along this chain.
473
+ static func _enum_along(script: Variant, named: String) -> Dictionary:
474
+ if not script is Script:
475
+ return {}
476
+ var current: Script = script
477
+ while current != null:
478
+ var found: Variant = current.get_script_constant_map().get(named)
479
+ if found is Dictionary:
480
+ var values: Dictionary = found
481
+ return values
482
+ current = current.get_base_script()
483
+ return {}
484
+
485
+
486
+ ## An empty list typed as what the packed array [param type] holds, for its elements to be fitted
487
+ ## against one by one, or null for any other type.
488
+ static func packed_template(type: int) -> Variant:
489
+ if not GdhSerialisation.LIST_TYPES.has(type):
490
+ return null
491
+ var element: int = GdhShapes.PACKED_ELEMENTS[type]
492
+ return Array([], element, &"", null)
493
+
494
+
267
495
  ## The class a method declares for argument [param index], or "" when it names none: an untyped
268
496
  ## parameter, a Variant, or a type that is not an object.
269
497
  func parameter_class(object: Object, method: String, index: int) -> String:
270
- for entry: Dictionary in object.get_method_list():
498
+ for entry: Dictionary in methods_of(object):
271
499
  if entry.get("name", "") != method:
272
500
  continue
273
501
  var params: Array = entry.get("args", [])
@@ -413,7 +641,7 @@ func fitted(value: Variant, type: int) -> Variant:
413
641
  ## An empty list or map typed the way argument [param index] of [param method] declares, or null
414
642
  ## when that argument is not a typed container. What [method typed_like] builds against.
415
643
  func parameter_container(object: Object, method: String, index: int) -> Variant:
416
- for entry: Dictionary in object.get_method_list():
644
+ for entry: Dictionary in methods_of(object):
417
645
  if entry.get("name", "") != method:
418
646
  continue
419
647
  var params: Array = entry.get("args", [])
@@ -555,6 +783,18 @@ func _element(item: Variant, builtin: int, declared: String, script: Script, res
555
783
  if named.has("message"):
556
784
  return {"message": str(named["message"]) + "."}
557
785
  return {"value": named["object"]}
786
+ # Fitted alone, 4.5 into an Array[int] went in as 4, as a single value is refused for.
787
+ if cuts(item, builtin):
788
+ return {
789
+ "message":
790
+ (
791
+ "is %s, which %s cannot hold without changing it."
792
+ % [JSON.stringify(serialize(item), "", false), cut_by(builtin)]
793
+ )
794
+ }
795
+ var rounded: String = inexact(item, builtin)
796
+ if not rounded.is_empty():
797
+ return {"message": "is too large to be sure of: %s." % rounded}
558
798
  var fitted_item: Variant = fitted(item, builtin)
559
799
  if not acceptable(fitted_item, builtin):
560
800
  # The value as it was sent rather than its engine type: JSON numbers arrive as floats, so a 7