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.
- package/build/cli.js +62 -9
- package/build/godot/addons/auto_reload/auto_reload.gd +18 -19
- package/build/godot/addons/gdharness_editor/animation_reads.gd +55 -33
- package/build/godot/addons/gdharness_editor/bridge_client.gd +53 -58
- package/build/godot/addons/gdharness_editor/declared_values.gd +68 -32
- package/build/godot/addons/gdharness_editor/file_reads.gd +83 -42
- package/build/godot/addons/gdharness_editor/file_walk.gd +12 -5
- package/build/godot/addons/gdharness_editor/headless_check.gd +68 -44
- package/build/godot/addons/gdharness_editor/plugin.gd +2 -3
- package/build/godot/addons/gdharness_editor/property_paths.gd +22 -12
- package/build/godot/addons/gdharness_editor/property_values.gd +143 -78
- package/build/godot/addons/gdharness_editor/reading.gd +37 -12
- package/build/godot/addons/gdharness_editor/same_values.gd +4 -4
- package/build/godot/addons/gdharness_editor/scene_file.gd +109 -79
- package/build/godot/addons/gdharness_editor/scene_uids.gd +12 -6
- package/build/godot/addons/gdharness_editor/serialisation.gd +75 -51
- package/build/godot/addons/gdharness_editor/setting_reads.gd +7 -6
- package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -6
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +37 -27
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +79 -49
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +70 -40
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +34 -30
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +24 -22
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +49 -29
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +26 -21
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +40 -40
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +166 -107
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -9
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +68 -41
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +35 -20
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +28 -13
- package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +12 -5
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +113 -67
- package/build/godot/addons/gdharness_editor/tools/script_paths.gd +20 -8
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +11 -8
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +47 -34
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +77 -50
- package/build/godot/addons/gdharness_editor/value_shapes.gd +5 -6
- package/build/godot/addons/gdharness_runtime/call_errors.gd +1 -2
- package/build/godot/addons/gdharness_runtime/declared_values.gd +68 -32
- package/build/godot/addons/gdharness_runtime/error_report.gd +1 -2
- package/build/godot/addons/gdharness_runtime/reading.gd +37 -12
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +84 -63
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +149 -147
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +64 -42
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +184 -122
- package/build/godot/addons/gdharness_runtime/runtime_frames.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd +24 -20
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +134 -92
- package/build/godot/addons/gdharness_runtime/runtime_items.gd +29 -13
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd +0 -1
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +34 -17
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +17 -13
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +84 -46
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +117 -79
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +14 -17
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +23 -16
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +117 -66
- package/build/godot/addons/gdharness_runtime/runtime_touch.gd +15 -15
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +44 -27
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +108 -73
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +111 -84
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +10 -7
- package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -51
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +5 -6
- package/build/godot/addons/gdharness_tests/bench.gd +4 -5
- package/build/godot/addons/gdharness_tests/event.gd +10 -5
- package/build/godot/addons/gdharness_tests/share.gd +38 -18
- package/build/godot/addons/gdharness_tests/suites.gd +3 -3
- package/build/godot/addons/gdharness_tests/wait.gd +3 -2
- package/build/godot/loader/loader.gd +9 -9
- package/build/godot/operations/animation_reads.gd +55 -33
- package/build/godot/operations/audio_buses.gd +48 -19
- package/build/godot/operations/class_cache.gd +16 -11
- package/build/godot/operations/classdb_queries.gd +89 -44
- package/build/godot/operations/declared_values.gd +68 -32
- package/build/godot/operations/dependencies.gd +139 -93
- package/build/godot/operations/editor_tools.gd +9 -10
- package/build/godot/operations/export_presets.gd +100 -54
- package/build/godot/operations/file_reads.gd +83 -42
- package/build/godot/operations/file_walk.gd +12 -5
- package/build/godot/operations/gdscript_analysis.gd +86 -65
- package/build/godot/operations/gdscript_authoring.gd +83 -36
- package/build/godot/operations/godot_operations.gd +17 -11
- package/build/godot/operations/import_pipeline.gd +110 -59
- package/build/godot/operations/input_actions.gd +62 -39
- package/build/godot/operations/patterns.gd +3 -1
- package/build/godot/operations/plugins.gd +36 -16
- package/build/godot/operations/project_config.gd +53 -29
- package/build/godot/operations/project_diagnostics.gd +24 -11
- package/build/godot/operations/project_file.gd +16 -16
- package/build/godot/operations/project_validation.gd +71 -44
- package/build/godot/operations/property_paths.gd +22 -12
- package/build/godot/operations/property_values.gd +143 -78
- package/build/godot/operations/reading.gd +37 -12
- package/build/godot/operations/rename_checks.gd +2 -2
- package/build/godot/operations/resource_files.gd +11 -9
- package/build/godot/operations/same_values.gd +4 -4
- package/build/godot/operations/scene_connections.gd +27 -13
- package/build/godot/operations/script_check.gd +52 -49
- package/build/godot/operations/serialisation.gd +75 -51
- package/build/godot/operations/setting_reads.gd +7 -6
- package/build/godot/operations/value_shapes.gd +5 -6
- package/build/godot/tests/listed.gd +321 -268
- 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
|
|
123
|
-
## and the inspector show and the number is only where the value happens to sit. Tried as the
|
|
124
|
-
## spells it, then as the hint does, then as the engine's constant.
|
|
125
|
-
static func enum_by_name(
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
318
|
-
## there, which the editor never runs for a script that is not a tool, so a resource_name
|
|
319
|
-
## was saved as if asked for. A property whose default the engine cannot report is left
|
|
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,
|
|
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 =
|
|
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
|
|
458
|
-
+ "takes 0 to %d and the engine keeps only those bits. Nothing was
|
|
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)]
|
|
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
|
|
488
|
-
+ "would play on %s; %s. project_info include audio_buses lists them,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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,
|
|
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 +=
|
|
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
|
|
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
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
9
|
-
## its first question makes the engine build the tree, and what it reads is what a screen
|
|
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
|
-
##
|
|
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
|
|
39
|
-
## has one, its bounds in the window's pixels, and the actions a screen reader is offered on it.
|
|
40
|
-
## stage it reaches is marked in $Progress as it is reached, with the time, so a client stopped
|
|
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
|
-
+ "
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
276
|
-
#
|
|
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),
|
|
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(
|
|
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
|
|
342
|
-
+ " AccessKit fills it in, has no property that carries it (measured on
|
|
343
|
-
+ " reader on Windows is not told it. Say what a reader needs in
|
|
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
|
|
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
|
|
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"
|
|
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 =
|
|
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
|
|
368
|
-
+ " reader reads only with
|
|
369
|
-
+ "
|
|
370
|
-
+
|
|
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
|
|
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
|
|
385
|
-
+ " text is read from its value,
|
|
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(
|
|
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(
|
|
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(
|
|
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) &
|
|
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
|
|
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
|
|
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
|
|
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 = {}
|