gdharness 1.2.28 → 1.2.29
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 +393 -167
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +259 -31
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +29 -7
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +33 -7
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +10 -3
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +89 -15
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +3 -3
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +120 -0
- package/build/index.js +382 -167
- package/build/keeper.js +20 -17
- package/package.json +1 -1
|
@@ -93,23 +93,37 @@ const READS_ONLY: String = ", and a path, which a wait reads every frame, only c
|
|
|
93
93
|
## "get_theme_stylebox(\"panel\"):content_margin_top", since what a control's theme gives it is only
|
|
94
94
|
## reachable by naming what is asked for; see [method arguments_of].
|
|
95
95
|
##
|
|
96
|
+
## A step into a vector, a colour, a rectangle or a transform reads one of its components,
|
|
97
|
+
## "size:x" or "global_transform:origin:y", by the names GDScript gives them; see
|
|
98
|
+
## [method Values.components_of].
|
|
99
|
+
##
|
|
96
100
|
## Answers with a `message` instead when a step along the way is not there or holds something that
|
|
97
101
|
## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
|
|
98
|
-
## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle.
|
|
99
|
-
|
|
102
|
+
## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle. Answers `chain` as
|
|
103
|
+
## well, each holder walked through and the step taken off it, for [method write_back].
|
|
104
|
+
static func walk_to(node: Object, node_path: String, reaching: String) -> Dictionary:
|
|
100
105
|
var parts: PackedStringArray = steps_of(reaching)
|
|
101
106
|
var holder: Variant = node
|
|
102
107
|
var called: String = node_path
|
|
108
|
+
var chain: Array[Dictionary] = []
|
|
103
109
|
for step: int in parts.size() - 1:
|
|
104
110
|
var named: String = parts[step]
|
|
105
111
|
if not can_read(holder, named):
|
|
106
112
|
return {"type": "error", "message": nothing_there(holder, named, called)}
|
|
113
|
+
chain.append({"holder": holder, "named": named, "called": called})
|
|
107
114
|
holder = read_under(holder, named)
|
|
108
115
|
called = "%s:%s" % [called, named]
|
|
109
|
-
if not
|
|
116
|
+
if not can_hold(holder):
|
|
110
117
|
var wanted: String = parts[step + 1]
|
|
111
|
-
return {
|
|
112
|
-
|
|
118
|
+
return {
|
|
119
|
+
"type": "error",
|
|
120
|
+
"message":
|
|
121
|
+
(
|
|
122
|
+
"%s holds %s, which has nothing to read %s off"
|
|
123
|
+
% [called, type_string(typeof(holder)), wanted]
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
return {"holder": holder, "name": parts[parts.size() - 1], "called": called, "chain": chain}
|
|
113
127
|
|
|
114
128
|
|
|
115
129
|
## The object [param given] names for a method's argument, as `{"object": ...}`, or as
|
|
@@ -160,10 +174,17 @@ static func object_at(root: Node, node: Node, node_path: String, given: Variant)
|
|
|
160
174
|
return {"object": value}
|
|
161
175
|
|
|
162
176
|
|
|
163
|
-
## Whether a step can be taken into [param value] at all: an object, a list
|
|
164
|
-
## into, and a number or a string is where a path ends whether the caller
|
|
165
|
-
|
|
166
|
-
|
|
177
|
+
## Whether a step can be taken into [param value] at all: an object, a list, a map or a value with
|
|
178
|
+
## components can be walked into, and a number or a string is where a path ends whether the caller
|
|
179
|
+
## meant it to or not.
|
|
180
|
+
static func can_hold(value: Variant) -> bool:
|
|
181
|
+
return value is Object or value is Array or value is Dictionary or packed(value) or composite(value)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
## Whether [param value] is read by its components, a vector or a colour or a transform, and is a
|
|
185
|
+
## copy wherever it is read from.
|
|
186
|
+
static func composite(value: Variant) -> bool:
|
|
187
|
+
return not Values.components_of(value).is_empty()
|
|
167
188
|
|
|
168
189
|
|
|
169
190
|
## Whether [param value] is a packed list. Stepped into the way a list is, by index, and a write into
|
|
@@ -212,6 +233,8 @@ static func can_read(holder: Variant, named: String) -> bool:
|
|
|
212
233
|
if not method.is_empty():
|
|
213
234
|
return args.is_empty() and method in MAP_CALLS
|
|
214
235
|
return not _key_in(map, named).is_empty()
|
|
236
|
+
if composite(holder):
|
|
237
|
+
return method.is_empty() and named in Values.components_of(holder)
|
|
215
238
|
if holder is Object:
|
|
216
239
|
var object: Object = holder
|
|
217
240
|
if not method.is_empty():
|
|
@@ -420,25 +443,62 @@ static func _native_flags(object: Object, method: String) -> int:
|
|
|
420
443
|
return 0
|
|
421
444
|
|
|
422
445
|
|
|
423
|
-
## Put [param value] where [param named] points
|
|
424
|
-
##
|
|
425
|
-
|
|
446
|
+
## Put [param value] where [param named] points, and answer the holder written into. Only ever
|
|
447
|
+
## called once [method can_read] agrees. Lists and maps are reference types, so writing into one
|
|
448
|
+
## reaches the game's own copy; a vector or a transform is a copy, and the one answered is the
|
|
449
|
+
## changed copy, for [method write_back] to put where it came from.
|
|
450
|
+
static func write(holder: Variant, named: String, value: Variant) -> Variant:
|
|
426
451
|
if holder is Array:
|
|
427
452
|
var items: Array = holder
|
|
428
453
|
items[_index_in(named, items.size())] = value
|
|
429
|
-
return
|
|
454
|
+
return holder
|
|
430
455
|
if packed(holder):
|
|
431
456
|
var list: Variant = holder
|
|
432
457
|
list[_index_in(named, len(holder))] = value
|
|
433
|
-
return
|
|
458
|
+
return holder
|
|
434
459
|
if holder is Dictionary:
|
|
435
460
|
var map: Dictionary = holder
|
|
436
461
|
# Written back under the key it was found under, since a map keyed by StringName and one
|
|
437
462
|
# keyed by String both read the same way and a write to the wrong one adds a second entry.
|
|
438
463
|
map[_key_in(map, named)[0]] = value
|
|
439
|
-
return
|
|
464
|
+
return holder
|
|
465
|
+
if composite(holder):
|
|
466
|
+
var changed: Variant = holder
|
|
467
|
+
changed[named] = value
|
|
468
|
+
return changed
|
|
440
469
|
var object: Object = holder
|
|
441
470
|
object.set(named, value)
|
|
471
|
+
return holder
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
## Why a write into [param chain]'s last holder, a copy, cannot reach the game, or "" when it can:
|
|
475
|
+
## each copy is put back where it was read from on the way up, and a step that was a call answered
|
|
476
|
+
## a copy the game does not keep anywhere. [param chain] is the one [method walk_to] answered.
|
|
477
|
+
static func unwritable_copy(chain: Array[Dictionary]) -> String:
|
|
478
|
+
for index: int in range(chain.size() - 1, -1, -1):
|
|
479
|
+
var link: Dictionary = chain[index]
|
|
480
|
+
var named: String = link["named"]
|
|
481
|
+
if not method_of(named).is_empty():
|
|
482
|
+
return (
|
|
483
|
+
"%s:%s is what a call answered, a copy the game keeps nowhere, so a part of it is not a place to write"
|
|
484
|
+
% [link["called"], named]
|
|
485
|
+
)
|
|
486
|
+
if not composite(link["holder"]):
|
|
487
|
+
return ""
|
|
488
|
+
return ""
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
## Puts [param changed], the copy a write into a vector or a transform changed, back where
|
|
492
|
+
## [param chain] read it from, and every copy it was read out of back where that came from, up to
|
|
493
|
+
## the first holder keeping its own: an object, whose setter runs, a list or a map.
|
|
494
|
+
static func write_back(chain: Array[Dictionary], changed: Variant) -> void:
|
|
495
|
+
var value: Variant = changed
|
|
496
|
+
for index: int in range(chain.size() - 1, -1, -1):
|
|
497
|
+
var link: Dictionary = chain[index]
|
|
498
|
+
var holder: Variant = link["holder"]
|
|
499
|
+
value = write(holder, str(link["named"]), value)
|
|
500
|
+
if not composite(holder):
|
|
501
|
+
return
|
|
442
502
|
|
|
443
503
|
|
|
444
504
|
## What [param holder] holds under [param named], or answers to it when [param named] is a call.
|
|
@@ -455,6 +515,8 @@ static func read_under(holder: Variant, named: String) -> Variant:
|
|
|
455
515
|
if holder is Dictionary:
|
|
456
516
|
var map: Dictionary = holder
|
|
457
517
|
return map[_key_in(map, named)[0]]
|
|
518
|
+
if composite(holder):
|
|
519
|
+
return holder[named]
|
|
458
520
|
var object: Object = holder
|
|
459
521
|
var method: String = method_of(named)
|
|
460
522
|
if not method.is_empty():
|
|
@@ -521,6 +583,18 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
521
583
|
if keys.size() > 8:
|
|
522
584
|
rest = " and %d more" % [keys.size() - 8]
|
|
523
585
|
return "%s has no key %s; it is keyed by %s%s" % [called_as, named, ", ".join(some), rest]
|
|
586
|
+
if composite(holder):
|
|
587
|
+
var kind: String = type_string(typeof(holder))
|
|
588
|
+
var sort: String = ("an " if kind.left(1) in ["A", "E", "I", "O", "U"] else "a ") + kind
|
|
589
|
+
var has: String = ", ".join(Values.components_of(holder))
|
|
590
|
+
if not container_call.is_empty():
|
|
591
|
+
return (
|
|
592
|
+
"%s is %s, which a path reads by its components, %s, and makes no call on"
|
|
593
|
+
% [called_as, sort, has]
|
|
594
|
+
)
|
|
595
|
+
return "%s is %s, so it has no %s; it has %s" % [called_as, sort, named, has]
|
|
596
|
+
if not holder is Object:
|
|
597
|
+
return "%s is %s, which has nothing to read %s off" % [called_as, type_string(typeof(holder)), named]
|
|
524
598
|
var object: Object = holder
|
|
525
599
|
var method: String = method_of(named)
|
|
526
600
|
if not method.is_empty():
|
|
@@ -634,12 +634,12 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
634
634
|
if node_path.is_empty() or property.is_empty():
|
|
635
635
|
return {"type": "error", "message": "Node path and property required"}
|
|
636
636
|
|
|
637
|
-
var standing: Dictionary = Values.
|
|
637
|
+
var standing: Dictionary = Values.target_at(_host.get_tree().root, node_path)
|
|
638
638
|
if standing.has("message"):
|
|
639
639
|
return standing
|
|
640
|
-
var
|
|
640
|
+
var target: Object = standing["object"]
|
|
641
641
|
|
|
642
|
-
var reached: Dictionary = Paths.walk_to(
|
|
642
|
+
var reached: Dictionary = Paths.walk_to(target, node_path, property)
|
|
643
643
|
if reached.has("message"):
|
|
644
644
|
return reached
|
|
645
645
|
|
|
@@ -11,6 +11,25 @@ extends RefCounted
|
|
|
11
11
|
const Read = preload("reading.gd")
|
|
12
12
|
const Serialisation = preload("serialisation.gd")
|
|
13
13
|
|
|
14
|
+
## The engine singletons a call, a write or a read may name instead of a node: what the engine
|
|
15
|
+
## measures, and the engine settings that are the game's own. Not every singleton: OS runs programs
|
|
16
|
+
## and opens files, DisplayServer switches the keyboard's layout and Input moves the real pointer,
|
|
17
|
+
## and none of that is a game's to do to the machine it runs on.
|
|
18
|
+
const SINGLETONS: Array[String] = [
|
|
19
|
+
"Engine",
|
|
20
|
+
"Performance",
|
|
21
|
+
"RenderingServer",
|
|
22
|
+
"AudioServer",
|
|
23
|
+
"PhysicsServer2D",
|
|
24
|
+
"PhysicsServer3D",
|
|
25
|
+
"NavigationServer2D",
|
|
26
|
+
"NavigationServer3D",
|
|
27
|
+
"Time",
|
|
28
|
+
"TranslationServer",
|
|
29
|
+
]
|
|
30
|
+
## The most frames a press is held in one call, as many as a frames wait takes.
|
|
31
|
+
const MOST_HELD: int = 600
|
|
32
|
+
|
|
14
33
|
var _shared: Serialisation = Serialisation.new()
|
|
15
34
|
|
|
16
35
|
|
|
@@ -55,6 +74,56 @@ static func node_at(root: Node, path: String) -> Dictionary:
|
|
|
55
74
|
return {"node": node}
|
|
56
75
|
|
|
57
76
|
|
|
77
|
+
## How many process frames a whole press holds before its release, from `holdFrames`: one unless
|
|
78
|
+
## asked, as the halves of a press have always been a frame apart. Held inside the game because the
|
|
79
|
+
## time between a press, a wait and a release sent as three calls counts as held too: a 60-frame
|
|
80
|
+
## pour poured for nearly three seconds. A String saying why, where the hold cannot be.
|
|
81
|
+
static func held_for(params: Dictionary) -> Variant:
|
|
82
|
+
if not params.has("holdFrames"):
|
|
83
|
+
return 1
|
|
84
|
+
if params.has("pressed"):
|
|
85
|
+
return (
|
|
86
|
+
"holdFrames holds a whole press and then lets go, and pressed asks for one half of one:"
|
|
87
|
+
+ " give one or the other."
|
|
88
|
+
)
|
|
89
|
+
var frames: int = Read.as_int(params["holdFrames"], 0)
|
|
90
|
+
if frames < 1 or frames > MOST_HELD:
|
|
91
|
+
return "holdFrames is %d, and it takes 1 to %d." % [frames, MOST_HELD]
|
|
92
|
+
return frames
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
## What a press says it was held for: [param frames] for a whole one, nothing for one half.
|
|
96
|
+
static func held_answer(whole: bool, frames: int) -> Variant:
|
|
97
|
+
if whole:
|
|
98
|
+
return frames
|
|
99
|
+
return null
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## What [param path] names: a node under [param root], as [method node_at] reads it, or an engine
|
|
103
|
+
## singleton by its name. Answers the object under "object", and under "node" the node that objects
|
|
104
|
+
## named in a value are found from, which for a singleton is [param root].
|
|
105
|
+
static func target_at(root: Node, path: String) -> Dictionary:
|
|
106
|
+
if SINGLETONS.has(path):
|
|
107
|
+
return {"object": Engine.get_singleton(path), "node": root}
|
|
108
|
+
if not path.contains("/") and Engine.has_singleton(path):
|
|
109
|
+
return {
|
|
110
|
+
"type": "error",
|
|
111
|
+
"message":
|
|
112
|
+
(
|
|
113
|
+
(
|
|
114
|
+
"%s is an engine singleton, and the ones that can be reached are %s. The rest act"
|
|
115
|
+
+ " on the machine rather than the game: OS runs programs, DisplayServer switches the"
|
|
116
|
+
+ " keyboard's layout and Input moves the real pointer."
|
|
117
|
+
)
|
|
118
|
+
% [path, ", ".join(SINGLETONS)]
|
|
119
|
+
),
|
|
120
|
+
}
|
|
121
|
+
var standing: Dictionary = node_at(root, path)
|
|
122
|
+
if standing.has("message"):
|
|
123
|
+
return standing
|
|
124
|
+
return {"object": standing["node"], "node": standing["node"]}
|
|
125
|
+
|
|
126
|
+
|
|
58
127
|
## Whether two values can be compared without the comparison itself failing.
|
|
59
128
|
static func comparable(one: Variant, other: Variant) -> bool:
|
|
60
129
|
return Serialisation.comparable(one, other)
|
|
@@ -98,6 +167,52 @@ func parameter_class(object: Object, method: String, index: int) -> String:
|
|
|
98
167
|
return ""
|
|
99
168
|
|
|
100
169
|
|
|
170
|
+
## The components a path reads off [param value], by the names GDScript gives them, or none for a
|
|
171
|
+
## value that has none: a vector's axes, a colour's channels, a rectangle's corners and size, a
|
|
172
|
+
## transform's basis and origin. A value of one of these is a copy wherever it is read, so a write
|
|
173
|
+
## into a component is a write into the copy until it is put back where it was read from.
|
|
174
|
+
static func components_of(value: Variant) -> PackedStringArray:
|
|
175
|
+
match typeof(value):
|
|
176
|
+
TYPE_VECTOR2, TYPE_VECTOR2I:
|
|
177
|
+
return PackedStringArray(["x", "y"])
|
|
178
|
+
TYPE_VECTOR3, TYPE_VECTOR3I:
|
|
179
|
+
return PackedStringArray(["x", "y", "z"])
|
|
180
|
+
TYPE_VECTOR4, TYPE_VECTOR4I, TYPE_QUATERNION:
|
|
181
|
+
return PackedStringArray(["x", "y", "z", "w"])
|
|
182
|
+
TYPE_COLOR:
|
|
183
|
+
return PackedStringArray(
|
|
184
|
+
[
|
|
185
|
+
"r",
|
|
186
|
+
"g",
|
|
187
|
+
"b",
|
|
188
|
+
"a",
|
|
189
|
+
"r8",
|
|
190
|
+
"g8",
|
|
191
|
+
"b8",
|
|
192
|
+
"a8",
|
|
193
|
+
"h",
|
|
194
|
+
"s",
|
|
195
|
+
"v",
|
|
196
|
+
"ok_hsl_h",
|
|
197
|
+
"ok_hsl_s",
|
|
198
|
+
"ok_hsl_l"
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
TYPE_RECT2, TYPE_RECT2I, TYPE_AABB:
|
|
202
|
+
return PackedStringArray(["position", "size", "end"])
|
|
203
|
+
TYPE_PLANE:
|
|
204
|
+
return PackedStringArray(["normal", "x", "y", "z", "d"])
|
|
205
|
+
TYPE_BASIS:
|
|
206
|
+
return PackedStringArray(["x", "y", "z"])
|
|
207
|
+
TYPE_TRANSFORM2D:
|
|
208
|
+
return PackedStringArray(["x", "y", "origin"])
|
|
209
|
+
TYPE_TRANSFORM3D:
|
|
210
|
+
return PackedStringArray(["basis", "origin"])
|
|
211
|
+
TYPE_PROJECTION:
|
|
212
|
+
return PackedStringArray(["x", "y", "z", "w"])
|
|
213
|
+
return PackedStringArray()
|
|
214
|
+
|
|
215
|
+
|
|
101
216
|
## What the slot [param named] on [param holder] declares it holds, as `{"type": int, "class":
|
|
102
217
|
## String, "script": Script or null}`: a property's declaration, or the element type of a typed list
|
|
103
218
|
## or the value type of a typed map. TYPE_NIL when nothing is declared, which is also what a
|
|
@@ -111,6 +226,11 @@ func parameter_class(object: Object, method: String, index: int) -> String:
|
|
|
111
226
|
## as a script class with no `class_name` is declared under the engine class the script extends,
|
|
112
227
|
## measured on 4.7.2: `var quarrel: Walker` with Walker a preloaded Node3D script lists as Node3D.
|
|
113
228
|
static func slot_declared(holder: Variant, named: String) -> Dictionary:
|
|
229
|
+
# A component holds one kind of value and no other: a vector's axis is a number, its integer
|
|
230
|
+
# form's an integer, a rectangle's size a vector of the same form.
|
|
231
|
+
if named in components_of(holder):
|
|
232
|
+
var component: Variant = holder[named]
|
|
233
|
+
return {"type": typeof(component), "class": "", "script": null}
|
|
114
234
|
if holder is Array:
|
|
115
235
|
var items: Array = holder
|
|
116
236
|
if items.is_typed():
|