gdharness 1.3.42 → 1.3.43

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 (105) hide show
  1. package/build/cli.js +62 -9
  2. package/build/godot/addons/auto_reload/auto_reload.gd +18 -19
  3. package/build/godot/addons/gdharness_editor/animation_reads.gd +55 -33
  4. package/build/godot/addons/gdharness_editor/bridge_client.gd +53 -58
  5. package/build/godot/addons/gdharness_editor/declared_values.gd +68 -32
  6. package/build/godot/addons/gdharness_editor/file_reads.gd +83 -42
  7. package/build/godot/addons/gdharness_editor/file_walk.gd +12 -5
  8. package/build/godot/addons/gdharness_editor/headless_check.gd +68 -44
  9. package/build/godot/addons/gdharness_editor/plugin.gd +2 -3
  10. package/build/godot/addons/gdharness_editor/property_paths.gd +22 -12
  11. package/build/godot/addons/gdharness_editor/property_values.gd +143 -78
  12. package/build/godot/addons/gdharness_editor/reading.gd +37 -12
  13. package/build/godot/addons/gdharness_editor/same_values.gd +4 -4
  14. package/build/godot/addons/gdharness_editor/scene_file.gd +109 -79
  15. package/build/godot/addons/gdharness_editor/scene_uids.gd +12 -6
  16. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -51
  17. package/build/godot/addons/gdharness_editor/setting_reads.gd +7 -6
  18. package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -6
  19. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +37 -27
  20. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +79 -49
  21. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +70 -40
  22. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +34 -30
  23. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +24 -22
  24. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +49 -29
  25. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +26 -21
  26. package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +40 -40
  27. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +166 -107
  28. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -9
  29. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +68 -41
  30. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +35 -20
  31. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +28 -13
  32. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +12 -5
  33. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +113 -67
  34. package/build/godot/addons/gdharness_editor/tools/script_paths.gd +20 -8
  35. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +11 -8
  36. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +47 -34
  37. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +77 -50
  38. package/build/godot/addons/gdharness_editor/value_shapes.gd +5 -6
  39. package/build/godot/addons/gdharness_runtime/call_errors.gd +1 -2
  40. package/build/godot/addons/gdharness_runtime/declared_values.gd +68 -32
  41. package/build/godot/addons/gdharness_runtime/error_report.gd +1 -2
  42. package/build/godot/addons/gdharness_runtime/reading.gd +37 -12
  43. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +84 -63
  44. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +149 -147
  45. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +64 -42
  46. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +184 -122
  47. package/build/godot/addons/gdharness_runtime/runtime_frames.gd +2 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +24 -20
  49. package/build/godot/addons/gdharness_runtime/runtime_input.gd +134 -92
  50. package/build/godot/addons/gdharness_runtime/runtime_items.gd +29 -13
  51. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +0 -1
  52. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +34 -17
  53. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +17 -13
  54. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +84 -46
  55. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +117 -79
  56. package/build/godot/addons/gdharness_runtime/runtime_says.gd +14 -17
  57. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +23 -16
  58. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +117 -66
  59. package/build/godot/addons/gdharness_runtime/runtime_touch.gd +15 -15
  60. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +44 -27
  61. package/build/godot/addons/gdharness_runtime/runtime_values.gd +108 -73
  62. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +111 -84
  63. package/build/godot/addons/gdharness_runtime/runtime_words.gd +10 -7
  64. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -51
  65. package/build/godot/addons/gdharness_runtime/value_shapes.gd +5 -6
  66. package/build/godot/addons/gdharness_tests/bench.gd +4 -5
  67. package/build/godot/addons/gdharness_tests/event.gd +10 -5
  68. package/build/godot/addons/gdharness_tests/share.gd +38 -18
  69. package/build/godot/addons/gdharness_tests/suites.gd +3 -3
  70. package/build/godot/addons/gdharness_tests/wait.gd +3 -2
  71. package/build/godot/loader/loader.gd +9 -9
  72. package/build/godot/operations/animation_reads.gd +55 -33
  73. package/build/godot/operations/audio_buses.gd +48 -19
  74. package/build/godot/operations/class_cache.gd +16 -11
  75. package/build/godot/operations/classdb_queries.gd +89 -44
  76. package/build/godot/operations/declared_values.gd +68 -32
  77. package/build/godot/operations/dependencies.gd +139 -93
  78. package/build/godot/operations/editor_tools.gd +9 -10
  79. package/build/godot/operations/export_presets.gd +100 -54
  80. package/build/godot/operations/file_reads.gd +83 -42
  81. package/build/godot/operations/file_walk.gd +12 -5
  82. package/build/godot/operations/gdscript_analysis.gd +86 -65
  83. package/build/godot/operations/gdscript_authoring.gd +83 -36
  84. package/build/godot/operations/godot_operations.gd +17 -11
  85. package/build/godot/operations/import_pipeline.gd +110 -59
  86. package/build/godot/operations/input_actions.gd +62 -39
  87. package/build/godot/operations/patterns.gd +3 -1
  88. package/build/godot/operations/plugins.gd +36 -16
  89. package/build/godot/operations/project_config.gd +53 -29
  90. package/build/godot/operations/project_diagnostics.gd +24 -11
  91. package/build/godot/operations/project_file.gd +16 -16
  92. package/build/godot/operations/project_validation.gd +71 -44
  93. package/build/godot/operations/property_paths.gd +22 -12
  94. package/build/godot/operations/property_values.gd +143 -78
  95. package/build/godot/operations/reading.gd +37 -12
  96. package/build/godot/operations/rename_checks.gd +2 -2
  97. package/build/godot/operations/resource_files.gd +11 -9
  98. package/build/godot/operations/same_values.gd +4 -4
  99. package/build/godot/operations/scene_connections.gd +27 -13
  100. package/build/godot/operations/script_check.gd +52 -49
  101. package/build/godot/operations/serialisation.gd +75 -51
  102. package/build/godot/operations/setting_reads.gd +7 -6
  103. package/build/godot/operations/value_shapes.gd +5 -6
  104. package/build/godot/tests/listed.gd +321 -268
  105. package/package.json +1 -1
@@ -1,11 +1,9 @@
1
1
  @tool
2
2
  extends RefCounted
3
-
4
3
  ## What a property declares beyond its type: the values of its enum and the range it is exported
5
4
  ## with, for taking a value by name and for saying when a value written lies outside them.
6
5
 
7
6
  const GdhRead = preload("reading.gd")
8
-
9
7
  ## The section of layer_names in project.godot naming the layers of each layers hint.
10
8
  const LAYER_NAMES: Dictionary[int, String] = {
11
9
  PROPERTY_HINT_LAYERS_2D_PHYSICS: "2d_physics",
@@ -27,7 +25,7 @@ static func layers_in(hint: int, mask: int) -> Array[Dictionary]:
27
25
  for layer: int in range(1, MASK_LAYERS + 1):
28
26
  if mask & (1 << (layer - 1)) == 0:
29
27
  continue
30
- var entry: Dictionary = {"layer": layer}
28
+ var entry: Dictionary = { "layer": layer }
31
29
  var name: String = str(
32
30
  ProjectSettings.get_setting("layer_names/%s/layer_%d" % [LAYER_NAMES[hint], layer], "")
33
31
  )
@@ -119,10 +117,12 @@ static func engine_constants(target: Object, property: String) -> Dictionary:
119
117
  ## names by it, as [true, number], or [false, why] naming the values, with [param holder] naming the
120
118
  ## target in the refusal. [false, ""] for a word that is a number, which converts as one.
121
119
  ##
122
- ## An enum is written by the name of a value as well as its number, since the name is what the script
123
- ## and the inspector show and the number is only where the value happens to sit. Tried as the script
124
- ## spells it, then as the hint does, then as the engine's constant.
125
- static func enum_by_name(target: Object, declared: Dictionary, word: String, holder: String) -> Array:
120
+ ## An enum is written by the name of a value as well as its number, since the name is what the
121
+ ## script and the inspector show and the number is only where the value happens to sit. Tried as the
122
+ ## script spells it, then as the hint does, then as the engine's constant.
123
+ static func enum_by_name(
124
+ target: Object, declared: Dictionary, word: String, holder: String
125
+ ) -> Array:
126
126
  var property: String = str(declared.get("name", ""))
127
127
  var choices: Dictionary = enum_of(target, declared)
128
128
  var chosen: Variant = enum_named(choices, word)
@@ -144,7 +144,9 @@ static func enum_refusal(
144
144
  ) -> String:
145
145
  var ways: String = "by name or by number"
146
146
  if not constants.is_empty():
147
- ways = "by name, by number or by the engine's constant, such as %s" % str(constants.keys()[0])
147
+ ways = (
148
+ "by name, by number or by the engine's constant, such as %s" % str(constants.keys()[0])
149
+ )
148
150
  var said: String = (
149
151
  "%s.%s is an enum, and %s is none of its values: send %s %s"
150
152
  % [holder, property, word, _listed(choices), ways]
@@ -163,7 +165,10 @@ static func enum_argument_refusal(
163
165
  called: String, index: int, named: String, word: String, choices: Dictionary
164
166
  ) -> String:
165
167
  var said: String = (
166
- "%s takes the enum %s as argument %d, and %s is none of its values: send %s by name or by number"
168
+ (
169
+ "%s takes the enum %s as argument %d, and %s is none of its values: send %s by name or "
170
+ + "by number"
171
+ )
167
172
  % [called, named, index + 1, word, _listed(choices)]
168
173
  )
169
174
  var close: PackedStringArray = GdhRead.nearest(word, PackedStringArray(choices.keys()))
@@ -174,7 +179,9 @@ static func enum_argument_refusal(
174
179
 
175
180
  ## Why [param value], given as argument [param index] for the enum [param named], is none of its
176
181
  ## [param choices], or "" when it is one. The engine passes any number on.
177
- static func outside_enum_argument(index: int, named: String, value: Variant, choices: Dictionary) -> String:
182
+ static func outside_enum_argument(
183
+ index: int, named: String, value: Variant, choices: Dictionary
184
+ ) -> String:
178
185
  if typeof(value) != TYPE_INT or choices.values().has(value):
179
186
  return ""
180
187
  return (
@@ -235,7 +242,10 @@ static func outside(target: Object, declared: Dictionary, value: Variant, shown:
235
242
  var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
236
243
  var hint_string: String = str(declared.get("hint_string", ""))
237
244
  var property: String = str(declared.get("name", ""))
238
- if hint == PROPERTY_HINT_ENUM and (typeof(value) == TYPE_STRING or typeof(value) == TYPE_STRING_NAME):
245
+ if (
246
+ hint == PROPERTY_HINT_ENUM
247
+ and (typeof(value) == TYPE_STRING or typeof(value) == TYPE_STRING_NAME)
248
+ ):
239
249
  var names: PackedStringArray = PackedStringArray(enum_values(hint_string).keys())
240
250
  if names.is_empty() or names.has(str(value)):
241
251
  return ""
@@ -308,15 +318,17 @@ static func _listed(choices: Dictionary) -> String:
308
318
  return ", ".join(listed)
309
319
 
310
320
 
311
- ## A range's end as the script wrote it: the engine's hint gives @export_range(0, 10) as 0.0 and 10.0.
321
+ ## A range's end as the script wrote it: the engine's hint gives @export_range(0, 10) as 0.0 and
322
+ ## 10.0.
312
323
  static func _bound(end: float) -> String:
313
324
  return str(int(end)) if end == floorf(end) and absf(end) < 1e15 else str(end)
314
325
 
315
326
 
316
327
  ## Puts back on [param built] what its script set as it was attached and was not among
317
- ## [param wanted], to the defaults it declares, outside the editor. Attaching runs the script's _init
318
- ## there, which the editor never runs for a script that is not a tool, so a resource_name _init chose
319
- ## was saved as if asked for. A property whose default the engine cannot report is left as made.
328
+ ## [param wanted], to the defaults it declares, outside the editor. Attaching runs the script's
329
+ ## _init there, which the editor never runs for a script that is not a tool, so a resource_name
330
+ ## _init chose was saved as if asked for. A property whose default the engine cannot report is left
331
+ ## as made.
320
332
  static func unmade(built: Resource, wanted: Dictionary) -> void:
321
333
  var attached: Variant = built.get_script()
322
334
  if Engine.is_editor_hint() or not attached is Script:
@@ -445,17 +457,25 @@ static func runs_script_code(target: Object, name: String) -> bool:
445
457
  ## the buses are read from the AudioServer, which holds the project's layout; "the engine changed or
446
458
  ## refused the value" left a caller to guess the bus did not exist.
447
459
  static func not_held(
448
- holder: String, engine_class: String, property: String, declared: Dictionary, given: Array, held: String
460
+ holder: String,
461
+ engine_class: String,
462
+ property: String,
463
+ declared: Dictionary,
464
+ given: Array,
465
+ held: String
449
466
  ) -> String:
450
467
  var value: Variant = given[0]
451
- var said: String = "%s.%s was given %s and holds %s after it was set" % [holder, property, given[1], held]
468
+ var said: String = (
469
+ "%s.%s was given %s and holds %s after it was set" % [holder, property, given[1], held]
470
+ )
452
471
  var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL))
453
472
  var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
454
473
  if hint in LAYER_NAMES and typeof(value) == TYPE_INT:
455
474
  return (
456
475
  (
457
- "%s: it is a mask of the %s layers, one bit each from layer 1 (1) to layer %d (%d), so it "
458
- + "takes 0 to %d and the engine keeps only those bits. Nothing was written."
476
+ "%s: it is a mask of the %s layers, one bit each from layer 1 (1) to layer %d "
477
+ + "(%d), so it takes 0 to %d and the engine keeps only those bits. Nothing was "
478
+ + "written."
459
479
  )
460
480
  % [
461
481
  said,
@@ -477,22 +497,28 @@ static func not_held(
477
497
  return said + ": the engine changed or refused the value, so nothing was written."
478
498
  var close: PackedStringArray = GdhRead.nearest(str(value), names)
479
499
  var nearest: String = (
480
- "%s %s" % ["is" if close.size() == 1 else "are", ", ".join(close)] if not close.is_empty() else ""
500
+ "%s %s" % ["is" if close.size() == 1 else "are", ", ".join(close)]
501
+ if not close.is_empty()
502
+ else ""
481
503
  )
482
504
  if bus:
483
505
  # Not "holds Master after it was set": the getter answers the first bus for any name the
484
506
  # layout lacks, and nothing was kept.
485
507
  return (
486
508
  (
487
- "%s.bus takes the name of one of the project's audio buses, and %s is none of them, so it "
488
- + "would play on %s; %s. project_info include audio_buses lists them, and "
489
- + "project_settings add_audio_bus adds one. Nothing was written."
509
+ "%s.bus takes the name of one of the project's audio buses, and %s is none of "
510
+ + "them, so it would play on %s; %s. project_info include audio_buses lists them, "
511
+ + "and project_settings add_audio_bus adds one. Nothing was written."
490
512
  )
491
513
  % [
492
514
  holder,
493
515
  given[1],
494
516
  AudioServer.get_bus_name(0),
495
- "the buses are " + ", ".join(names) if nearest.is_empty() else "the nearest " + nearest
517
+ (
518
+ "the buses are " + ", ".join(names)
519
+ if nearest.is_empty()
520
+ else "the nearest " + nearest
521
+ ),
496
522
  ]
497
523
  )
498
524
  return (
@@ -500,7 +526,11 @@ static func not_held(
500
526
  % [
501
527
  said,
502
528
  given[1],
503
- "it lists " + ", ".join(names) if nearest.is_empty() else "the nearest it lists " + nearest
529
+ (
530
+ "it lists " + ", ".join(names)
531
+ if nearest.is_empty()
532
+ else "the nearest it lists " + nearest
533
+ ),
504
534
  ]
505
535
  )
506
536
 
@@ -513,7 +543,9 @@ static func held_by_children(node: Node, property: String) -> String:
513
543
  var holders: PackedStringArray = []
514
544
  for child: Node in node.get_children():
515
545
  if property in child:
516
- var _added: bool = holders.append("%s (a %s)" % [root.get_path_to(child), child.get_class()])
546
+ var _added: bool = holders.append(
547
+ "%s (a %s)" % [root.get_path_to(child), child.get_class()]
548
+ )
517
549
  if holders.is_empty():
518
550
  return ""
519
551
  if holders.size() == 1:
@@ -523,14 +555,15 @@ static func held_by_children(node: Node, property: String) -> String:
523
555
 
524
556
 
525
557
  ## A sentence for each node path in [param given], by property, that names no node from
526
- ## [param node]. A path is written whatever it names, as the inspector writes one, since the node may
527
- ## be added next; a joint whose node_a named nothing then joined nothing, and no answer said so.
558
+ ## [param node]. A path is written whatever it names, as the inspector writes one, since the node
559
+ ## may be added next; a joint whose node_a named nothing then joined nothing, and no answer said so.
528
560
  static func naming_nothing(node: Node, given: Dictionary) -> Dictionary:
529
561
  var said: Dictionary = {}
530
562
  for property: String in given:
531
563
  if "/" in property or ":" in property:
532
564
  continue
533
- # What the node holds, since a path given as text is held as text until the engine stores it.
565
+ # What the node holds, since a path given as text is held as text until the engine stores
566
+ # it.
534
567
  var path: Variant = node.get(property)
535
568
  if not path is NodePath:
536
569
  continue
@@ -539,8 +572,8 @@ static func naming_nothing(node: Node, given: Dictionary) -> Dictionary:
539
572
  continue
540
573
  said[property] = (
541
574
  (
542
- "%s names no node from %s. It is written, as the editor writes one, since that node may "
543
- + "be added next; until it is, %s finds nothing."
575
+ "%s names no node from %s. It is written, as the editor writes one, since that "
576
+ + "node may be added next; until it is, %s finds nothing."
544
577
  )
545
578
  % [str(named), node.name, property]
546
579
  )
@@ -553,7 +586,10 @@ static func naming_nothing(node: Node, given: Dictionary) -> Dictionary:
553
586
  static func no_uniform(material: ShaderMaterial, property: String) -> String:
554
587
  var uniform: String = property.trim_prefix("shader_parameter/")
555
588
  if material.shader == null:
556
- return "The ShaderMaterial has no shader, so it has no parameter %s yet; set shader first." % uniform
589
+ return (
590
+ "The ShaderMaterial has no shader, so it has no parameter %s yet; set shader first."
591
+ % uniform
592
+ )
557
593
  var shader: String = material.shader.resource_path
558
594
  var named: String = shader if not shader.is_empty() else "Its shader"
559
595
  var uniforms: Array = []
@@ -1,5 +1,4 @@
1
1
  extends Logger
2
-
3
2
  ## The console of a game the editor plays, errors and prints in the order the engine made them,
4
3
  ## written to a file beside the announcement in the lines the engine itself prints.
5
4
  ##
@@ -53,7 +52,7 @@ func _log_message(message: String, _error: bool) -> void:
53
52
 
54
53
 
55
54
  # The engine's own signature for a Logger, eight parameters and not this project's to shape.
56
- # gdlint:ignore = function-arguments-number
55
+ # gdstyle:ignore=quality/max-parameters
57
56
  func _log_error(
58
57
  function: String,
59
58
  file: String,
@@ -124,7 +124,8 @@ static func json_or_null(text: String) -> Variant:
124
124
  ## [param said], the refusal for [param node_path] found nowhere in a scene whose root is called
125
125
  ## [param root_name], with how paths are read when the path starts with that name and the root has
126
126
  ## no child called that. Paths go from the root, so its own name there names a child of it, and a
127
- ## caller who read the root as Main in a tree wrote Main/Body and was told only that nothing is there.
127
+ ## caller who read the root as Main in a tree wrote Main/Body and was told only that nothing is
128
+ ## there.
128
129
  static func from_the_root(said: String, node_path: String, root_name: String) -> String:
129
130
  var first: String = node_path.get_slice("/", 0)
130
131
  if root_name.is_empty() or first != root_name:
@@ -141,7 +142,11 @@ static func from_the_root(said: String, node_path: String, root_name: String) ->
141
142
  ## is empty; the name below it that is missing; and [param children], the names that node has, as
142
143
  ## after a node was moved or renamed. Names up to [constant NAMED_CHILDREN] of them.
143
144
  static func missing_below(
144
- said: String, root_name: String, reached: PackedStringArray, missing: String, children: PackedStringArray
145
+ said: String,
146
+ root_name: String,
147
+ reached: PackedStringArray,
148
+ missing: String,
149
+ children: PackedStringArray
145
150
  ) -> String:
146
151
  var shown: String = ", ".join(children.slice(0, NAMED_CHILDREN))
147
152
  if children.size() > NAMED_CHILDREN:
@@ -167,9 +172,18 @@ static func not_of_kind(
167
172
  ) -> String:
168
173
  var told: String = said + ("" if said.ends_with(".") else ".")
169
174
  if not found.is_empty():
170
- told += ' The node at "%s" is %s, not %s.' % [node_path, with_article(found), with_article(kind)]
175
+ told += (
176
+ ' The node at "%s" is %s, not %s.'
177
+ % [node_path, with_article(found), with_article(kind)]
178
+ )
171
179
  if holders.is_empty():
172
- return told + " The scene holds no %s; scene_node add with nodeType %s puts one in it." % [kind, kind]
180
+ return (
181
+ told
182
+ + (
183
+ " The scene holds no %s; scene_node add with nodeType %s puts one in it."
184
+ % [kind, kind]
185
+ )
186
+ )
173
187
  var quoted: PackedStringArray = []
174
188
  for holder: String in holders.slice(0, NAMED_CHILDREN):
175
189
  var _quoted: bool = quoted.append('"%s"' % holder)
@@ -209,7 +223,10 @@ static func offered(said: String, word: String, names: Array) -> String:
209
223
  var close: PackedStringArray = nearest(word, every)
210
224
  if close.is_empty():
211
225
  return "%s; it has %s" % [said, ", ".join(every)]
212
- return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
226
+ return (
227
+ "%s; the nearest it has %s %s"
228
+ % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
229
+ )
213
230
 
214
231
 
215
232
  ## [param said], the refusal for [param property], which [param target] does not declare, with the
@@ -233,14 +250,20 @@ static func near_property(said: String, target: Object, property: String) -> Str
233
250
  scripted.append(str(declared.get("name", "")))
234
251
  var close: PackedStringArray = nearest(property, PackedStringArray(names))
235
252
  if not close.is_empty():
236
- return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
237
- # With a script that declares nothing readable, as one that does not compile, the name is likelier
238
- # the script's than the class's, so the class's list would point the wrong way.
253
+ return (
254
+ "%s; the nearest it has %s %s"
255
+ % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
256
+ )
257
+ # With a script that declares nothing readable, as one that does not compile, the name is
258
+ # likelier the script's than the class's, so the class's list would point the wrong way.
239
259
  if scripted.is_empty() and target.get_script() != null:
240
260
  return said
241
261
  if scripted.is_empty():
242
262
  return (
243
- "%s; editor_classes info with className %s and includeInherited true lists the ones it has"
263
+ (
264
+ "%s; editor_classes info with className %s and includeInherited true lists the "
265
+ + "ones it has"
266
+ )
244
267
  % [said, target.get_class()]
245
268
  )
246
269
  scripted.sort()
@@ -264,11 +287,13 @@ static func _list_called(target: Object, count: Dictionary, property: String) ->
264
287
  fields = _fields_at(fresh, first)
265
288
  if not fresh is RefCounted:
266
289
  fresh.free()
267
- var each: String = "each one's fields" if fields.is_empty() else "each one's %s" % ", ".join(fields)
290
+ var each: String = (
291
+ "each one's fields" if fields.is_empty() else "each one's %s" % ", ".join(fields)
292
+ )
268
293
  return (
269
294
  (
270
- "it keeps its %s as %s, how many there are, and %s under %s for the first, %s for the second and so on: "
271
- + "give %s before the fields of the entries it makes"
295
+ "it keeps its %s as %s, how many there are, and %s under %s for the first, %s for the "
296
+ + "second and so on: give %s before the fields of the entries it makes"
272
297
  )
273
298
  % [parts[0].to_lower(), counted, each, first, parts[1] + "1/", counted]
274
299
  )
@@ -1,19 +1,17 @@
1
1
  extends RefCounted
2
-
3
2
  ## What the running game tells a screen reader: its accessibility tree, as UI Automation reads it.
4
3
  ##
5
4
  ## The engine builds the tree only when something asks for it, and nothing on gdharness's desktop
6
5
  ## does, so a game's NOTIFICATION_ACCESSIBILITY_UPDATE never came and what its controls expose could
7
6
  ## not be checked. The engine offers no way to read the tree back, so the question is put the way a
8
- ## screen reader puts it: a UI Automation client started by the game lands on the game's own desktop,
9
- ## its first question makes the engine build the tree, and what it reads is what a screen reader is
10
- ## told, names left empty included.
7
+ ## screen reader puts it: a UI Automation client started by the game lands on the game's own
8
+ ## desktop, its first question makes the engine build the tree, and what it reads is what a screen
9
+ ## reader is told, names left empty included.
11
10
 
12
11
  const GdhRead = preload("reading.gd")
13
12
  const GdhSays = preload("runtime_says.gd")
14
-
15
- ## How long the client keeps walking the tree for it to settle: the engine answers the first question
16
- ## with its windows alone and fills them in over the next frames.
13
+ ## How long the client keeps walking the tree for it to settle: the engine answers the first
14
+ ## question with its windows alone and fills them in over the next frames.
17
15
  const SETTLE_MS: int = 8000
18
16
  ## How long a window with nothing in it is walked before the client stops: a game building its tree
19
17
  ## fills the window within a few frames of being asked, and one building none never does.
@@ -33,12 +31,11 @@ const MOST_LEVELS: int = 40
33
31
  ## Where an element holds its node's instance id between matching and placing, removed before the
34
32
  ## answer goes.
35
33
  const NODE_ID: String = "_node_instance"
36
-
37
34
  ## The client, as PowerShell, with $Handles, $Out, $Progress, $SettleMs, $EmptyMs and $MostLevels
38
- ## set before it. Each element answers what UI Automation names its role, its name, its value where it
39
- ## has one, its bounds in the window's pixels, and the actions a screen reader is offered on it. Each
40
- ## stage it reaches is marked in $Progress as it is reached, with the time, so a client stopped at the
41
- ## deadline still says how far it got.
35
+ ## set before it. Each element answers what UI Automation names its role, its name, its value where
36
+ ## it has one, its bounds in the window's pixels, and the actions a screen reader is offered on it.
37
+ ## Each stage it reaches is marked in $Progress as it is reached, with the time, so a client stopped
38
+ ## at the deadline still says how far it got.
42
39
  const WALK: String = r"""
43
40
  $ErrorActionPreference = 'Stop'
44
41
  function Answer($value) {
@@ -200,17 +197,17 @@ func get_accessibility(params: Dictionary) -> Dictionary:
200
197
  if _setting() != "always":
201
198
  also = (
202
199
  (
203
- ' and engineArgs ["--accessibility", "always"], since this project\'s'
204
- + " accessibility/general/accessibility_support is %s and the engine builds the tree"
205
- + " only when asked"
200
+ ' and engineArgs ["--accessibility", "always"], since this project\'s '
201
+ + "accessibility/general/accessibility_support is %s and the engine builds the "
202
+ + "tree only when asked"
206
203
  )
207
204
  % _setting()
208
205
  )
209
206
  return _refused(
210
207
  (
211
208
  (
212
- "A headless run has no window, so a screen reader is told nothing: start the run with"
213
- + " headless: false%s."
209
+ "A headless run has no window, so a screen reader is told nothing: start the "
210
+ + "run with headless: false%s."
214
211
  )
215
212
  % also
216
213
  )
@@ -219,8 +216,8 @@ func get_accessibility(params: Dictionary) -> Dictionary:
219
216
  return _refused(
220
217
  (
221
218
  (
222
- "The tree is read through UI Automation, the interface a Windows screen reader uses, and this"
223
- + " game runs on %s, whose screen readers ask it through another"
219
+ "The tree is read through UI Automation, the interface a Windows screen reader "
220
+ + "uses, and this game runs on %s, whose screen readers ask it through another"
224
221
  )
225
222
  % OS.get_name()
226
223
  )
@@ -239,7 +236,10 @@ func get_accessibility(params: Dictionary) -> Dictionary:
239
236
  var progress: String = out + ".progress"
240
237
  var script: String = (
241
238
  (
242
- "$Handles = @(%s)\n$Out = '%s'\n$Progress = '%s'\n$SettleMs = %d\n$EmptyMs = %d\n$MostLevels = %d\n"
239
+ (
240
+ "$Handles = @(%s)\n$Out = '%s'\n$Progress = '%s'\n$SettleMs = %d\n$EmptyMs = "
241
+ + "%d\n$MostLevels = %d\n"
242
+ )
243
243
  % [
244
244
  ", ".join(handles),
245
245
  out.replace("'", "''"),
@@ -271,9 +271,9 @@ func get_accessibility(params: Dictionary) -> Dictionary:
271
271
  if pid <= 0:
272
272
  return _refused("PowerShell could not be started from %s to read the tree" % shell)
273
273
  # Waited out a frame at a time rather than blocking: the client's questions are answered on the
274
- # game's main thread, which a blocking wait would hold, and neither side would move.
275
- # Two budgets, so a slow start is never charged to the walk: one for PowerShell to begin and load
276
- # UI Automation, and once it has, one for the walks.
274
+ # game's main thread, which a blocking wait would hold, and neither side would move. Two
275
+ # budgets, so a slow start is never charged to the walk: one for PowerShell to begin and load UI
276
+ # Automation, and once it has, one for the walks.
277
277
  var began_at: int = Time.get_ticks_msec()
278
278
  var deadline: int = _deadline(began_at, -1, handles.size())
279
279
  var walking: bool = false
@@ -299,7 +299,9 @@ func get_accessibility(params: Dictionary) -> Dictionary:
299
299
 
300
300
  var text: String = FileAccess.get_file_as_string(out)
301
301
  var took: Dictionary = _took(
302
- FileAccess.get_file_as_string(progress), started_ms, int(Time.get_unix_time_from_system() * 1000.0)
302
+ FileAccess.get_file_as_string(progress),
303
+ started_ms,
304
+ int(Time.get_unix_time_from_system() * 1000.0)
303
305
  )
304
306
  # Not checked: a file that is not there was never written, which the answer below says.
305
307
  var _removed: Error = DirAccess.remove_absolute(out)
@@ -309,7 +311,9 @@ func get_accessibility(params: Dictionary) -> Dictionary:
309
311
  return _refused("UI Automation answered nothing readable: %s" % text.left(500))
310
312
  var answer: Dictionary = read
311
313
  if answer.has("error"):
312
- return _refused("UI Automation could not read the game's windows: %s" % str(answer["error"]))
314
+ return _refused(
315
+ "UI Automation could not read the game's windows: %s" % str(answer["error"])
316
+ )
313
317
 
314
318
  var windows: Array = answer.get("windows", [])
315
319
  var made_by: Dictionary = {}
@@ -333,25 +337,28 @@ func get_accessibility(params: Dictionary) -> Dictionary:
333
337
  _unmark(window)
334
338
  elements += _count(window)
335
339
  unnamed += _unnamed_text(window)
336
- var result: Dictionary = {"windows": windows, "elements": elements}
340
+ var result: Dictionary = { "windows": windows, "elements": elements }
337
341
  if not took.is_empty():
338
342
  result["tookMsBy"] = took
339
343
  if placing.tooltips > 0:
340
344
  result["tooltip_note"] = (
341
- "tooltip is the node's tooltip_text, which the engine puts in the tree, and UI Automation, as"
342
- + " AccessKit fills it in, has no property that carries it (measured on 4.7.2), so a screen"
343
- + " reader on Windows is not told it. Say what a reader needs in accessibility_description."
345
+ "tooltip is the node's tooltip_text, which the engine puts in the tree, and UI "
346
+ + "Automation, as AccessKit fills it in, has no property that carries it (measured on "
347
+ + "4.7.2), so a screen reader on Windows is not told it. Say what a reader needs in "
348
+ + "accessibility_description."
344
349
  )
345
350
  if placing.asked > 0 and placing.placed == 0:
346
351
  # Nothing at all matched: the element ids are not the engine's element RIDs any more, which
347
- # an AccessKit that numbers its elements another way would do, and every node would read null.
352
+ # an AccessKit that numbers its elements another way would do, and every node would read
353
+ # null.
348
354
  result["node_note"] = (
349
- "No element could be matched to the node that made it: UI Automation's runtime ids did not"
350
- + " carry the engine's element ids, so node and part_of are the windows' alone."
355
+ "No element could be matched to the node that made it: UI Automation's runtime ids did "
356
+ + "not carry the engine's element ids, so node and part_of are the windows' alone."
351
357
  )
352
358
  if answer.has("description_unread"):
353
359
  result["description_note"] = (
354
- "Descriptions could not be read, so none is shown: %s" % str(answer["description_unread"])
360
+ "Descriptions could not be read, so none is shown: %s"
361
+ % str(answer["description_unread"])
355
362
  )
356
363
  if elements <= windows.size():
357
364
  var setting: String = _setting()
@@ -359,31 +366,35 @@ func get_accessibility(params: Dictionary) -> Dictionary:
359
366
  # the setting and a screen reader can say the tree is built; otherwise an empty window is
360
367
  # most likely a game that builds none.
361
368
  # 1 is a screen reader running, 0 none and -1 a platform that cannot say.
362
- var read_aloud: bool = setting == "auto" and DisplayServer.accessibility_screen_reader_active() == 1
369
+ var read_aloud: bool = (
370
+ setting == "auto" and DisplayServer.accessibility_screen_reader_active() == 1
371
+ )
363
372
  if setting != "always" and not read_aloud:
364
373
  return _refused(
365
374
  (
366
375
  (
367
- "UI Automation found nothing in the game's windows: the engine builds the tree a screen"
368
- + " reader reads only with accessibility/general/accessibility_support at Always Active,"
369
- + " or at Auto with a screen reader running when it starts, and this project's is %s."
370
- + ' Start the run with engineArgs ["--accessibility", "always"].'
376
+ "UI Automation found nothing in the game's windows: the engine builds the "
377
+ + "tree a screen reader reads only with "
378
+ + "accessibility/general/accessibility_support at Always Active, or at "
379
+ + "Auto with a screen reader running when it starts, and this project's "
380
+ + "is %s. "
381
+ + 'Start the run with engineArgs ["--accessibility", "always"].'
371
382
  )
372
383
  % setting
373
384
  )
374
385
  )
375
386
  result["_note"] = (
376
- "The windows hold nothing a screen reader is told: Controls are in the tree, and Node2D and"
377
- + " Node3D are not unless the game creates elements for them."
387
+ "The windows hold nothing a screen reader is told: Controls are in the tree, and "
388
+ + "Node2D and Node3D are not unless the game creates elements for them."
378
389
  )
379
390
  elif unnamed > 0:
380
391
  # Seen on a game naming its lines with accessibility_update_set_name: the text was there in
381
392
  # the game and empty to the screen reader.
382
393
  result["_note"] = (
383
394
  (
384
- "%d text element(s) on screen have no name, so a screen reader reads them as nothing. Static"
385
- + " text is read from its value, DisplayServer.accessibility_update_set_value, and a name"
386
- + " set on it is not read."
395
+ "%d text element(s) on screen have no name, so a screen reader reads them as "
396
+ + "nothing. Static text is read from its value, "
397
+ + "DisplayServer.accessibility_update_set_value, and a name set on it is not read."
387
398
  )
388
399
  % unnamed
389
400
  )
@@ -498,7 +509,9 @@ static func _cut(element: Dictionary, left: Array[int], omitted: Array[int]) ->
498
509
 
499
510
  ## The project's accessibility mode, as the command line's --accessibility names them.
500
511
  static func _setting() -> String:
501
- match GdhRead.as_int(ProjectSettings.get_setting("accessibility/general/accessibility_support", 0), 0):
512
+ match GdhRead.as_int(
513
+ ProjectSettings.get_setting("accessibility/general/accessibility_support", 0), 0
514
+ ):
502
515
  1:
503
516
  return "always"
504
517
  2:
@@ -576,7 +589,9 @@ static func _match(element: Dictionary, made_by: Dictionary, placing: GdhPlacing
576
589
  ## column of plain containers came out straight under the window. So the node is found where it
577
590
  ## must be: below the shown element's node, itself left out of the tree (a node that is shown would
578
591
  ## have the part under it), drawn around the part, and, for text, holding its words.
579
- static func _attribute(element: Dictionary, holder: Node, placing: GdhPlacing, is_window: bool) -> void:
592
+ static func _attribute(
593
+ element: Dictionary, holder: Node, placing: GdhPlacing, is_window: bool
594
+ ) -> void:
580
595
  var children: Array = element.get("children", [])
581
596
  for child: Variant in children:
582
597
  if not child is Dictionary:
@@ -636,7 +651,9 @@ static func _owners_of(element: Dictionary, holder: Node, placing: GdhPlacing) -
636
651
 
637
652
  ## Where a Control is drawn is read off its screen transform, which for a window that is not
638
653
  ## embedded is in that window's own pixels, as the bounds are.
639
- static func _drawn_around(node: Node, drawn: Rect2, placing: GdhPlacing, found: Array[Node]) -> void:
654
+ static func _drawn_around(
655
+ node: Node, drawn: Rect2, placing: GdhPlacing, found: Array[Node]
656
+ ) -> void:
640
657
  for child: Node in node.get_children(true):
641
658
  # A node that is shown has its parts under it, and so do the nodes below it.
642
659
  if placing.shown.has(child.get_instance_id()):
@@ -678,7 +695,7 @@ static func _text_of(node: Node) -> String:
678
695
  static func _made(parts: Array, made_by: Dictionary) -> Node:
679
696
  var halves: Array[int] = []
680
697
  for part: Variant in parts:
681
- halves.append(GdhRead.as_int(part, 0) & 0xFFFFFFFF)
698
+ halves.append(GdhRead.as_int(part, 0) & 0xffffffff)
682
699
  for at: int in [0, 2]:
683
700
  var id: int = (halves[at] << 32) | halves[at + 1]
684
701
  if made_by.has(id):
@@ -686,21 +703,15 @@ static func _made(parts: Array, made_by: Dictionary) -> Node:
686
703
  return null
687
704
 
688
705
 
689
- ## Counts kept while the elements are placed, and the nodes whose own elements UI Automation shows,
690
- ## by instance id.
691
- class GdhPlacing:
692
- extends RefCounted
693
- var asked: int = 0
694
- var placed: int = 0
695
- var tooltips: int = 0
696
- var shown: Dictionary = {}
697
-
698
-
699
706
  ## Text elements with bounds and no name. The window's announcement element has no bounds and is
700
707
  ## left out with them: it is empty until something is announced.
701
708
  static func _unnamed_text(element: Dictionary) -> int:
702
709
  var total: int = 0
703
- if element.get("role") == "text" and str(element.get("name", "")).is_empty() and element.has("bounds"):
710
+ if (
711
+ element.get("role") == "text"
712
+ and str(element.get("name", "")).is_empty()
713
+ and element.has("bounds")
714
+ ):
704
715
  total += 1
705
716
  var children: Array = element.get("children", [])
706
717
  for child: Variant in children:
@@ -710,8 +721,8 @@ static func _unnamed_text(element: Dictionary) -> int:
710
721
  return total
711
722
 
712
723
 
713
- ## When a client started at [param began_at] is stopped, in ticks: [constant START_MS] after it began
714
- ## while UI Automation is loading ([param loaded_at] -1), and once it was seen loaded at
724
+ ## When a client started at [param began_at] is stopped, in ticks: [constant START_MS] after it
725
+ ## began while UI Automation is loading ([param loaded_at] -1), and once it was seen loaded at
715
726
  ## [param loaded_at], [constant SETTLE_MS] for each of [param windows] and [constant WALK_SLACK_MS]
716
727
  ## after that.
717
728
  static func _deadline(began_at: int, loaded_at: int, windows: int) -> int:
@@ -763,8 +774,8 @@ static func _reached(progress: String, started_ms: int) -> String:
763
774
  )
764
775
  return (
765
776
  (
766
- "PowerShell began the client after %d ms and loaded UI Automation in %d ms, and the windows"
767
- + " were walked %d time%s, the last finding %s element%s"
777
+ "PowerShell began the client after %d ms and loaded UI Automation in %d ms, and the "
778
+ + "windows were walked %d time%s, the last finding %s element%s"
768
779
  )
769
780
  % [began, loaded, walks, "" if walks == 1 else "s", last, "" if last == "1" else "s"]
770
781
  )
@@ -789,4 +800,14 @@ static func _took(progress: String, started_ms: int, done_ms: int) -> Dictionary
789
800
 
790
801
 
791
802
  static func _refused(message: String) -> Dictionary:
792
- return {"type": "error", "message": message}
803
+ return { "type": "error", "message": message }
804
+
805
+
806
+ ## Counts kept while the elements are placed, and the nodes whose own elements UI Automation shows,
807
+ ## by instance id.
808
+ class GdhPlacing:
809
+ extends RefCounted
810
+ var asked: int = 0
811
+ var placed: int = 0
812
+ var tooltips: int = 0
813
+ var shown: Dictionary = {}