gdharness 1.0.1 → 1.0.3
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 +276 -63
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +44 -9
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +123 -21
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +81 -46
- package/build/godot/operations/classdb_queries.gd +75 -0
- package/build/index.js +275 -61
- package/package.json +1 -1
|
@@ -25,12 +25,31 @@ const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
|
|
|
25
25
|
var _host: Node
|
|
26
26
|
var _values: Values
|
|
27
27
|
|
|
28
|
+
## Where the last injected pointer event put the pointer, once one has.
|
|
29
|
+
##
|
|
30
|
+
## Kept here rather than asked of the viewport: the root viewport answers get_mouse_position
|
|
31
|
+
## from the operating system's pointer, which an injected event never moves, so on a desktop it
|
|
32
|
+
## is wherever the user's mouse is, another monitor included, and every motion sent through the
|
|
33
|
+
## bridge carried the distance from there. Only a headless run, with no pointer to read, followed
|
|
34
|
+
## the injected events, which is why that reading measured right and a played game read it wrong.
|
|
35
|
+
var _pointer: Vector2 = Vector2.ZERO
|
|
36
|
+
var _pointer_placed: bool = false
|
|
37
|
+
|
|
28
38
|
|
|
29
39
|
func _init(host: Node, values: Values) -> void:
|
|
30
40
|
_host = host
|
|
31
41
|
_values = values
|
|
32
42
|
|
|
33
43
|
|
|
44
|
+
## The movement a pointer arriving at [param position] carries, which is the distance from where
|
|
45
|
+
## the last injected event put it, and none at all for the first. The arrival is recorded.
|
|
46
|
+
func _arrive(position: Vector2) -> Vector2:
|
|
47
|
+
var relative: Vector2 = position - _pointer if _pointer_placed else Vector2.ZERO
|
|
48
|
+
_pointer = position
|
|
49
|
+
_pointer_placed = true
|
|
50
|
+
return relative
|
|
51
|
+
|
|
52
|
+
|
|
34
53
|
## Presses [param params].action, and lets go of it again unless asked to hold it.
|
|
35
54
|
##
|
|
36
55
|
## The whole press by default, for the reason [method click] is the whole click: an action left
|
|
@@ -329,9 +348,19 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
329
348
|
var pressed: bool = Read.as_bool(params.get("pressed", true), true)
|
|
330
349
|
var double: bool = Read.as_bool(params.get("doubleClick", false))
|
|
331
350
|
|
|
351
|
+
# A button pressed somewhere puts the pointer there: the motion after a held press measures
|
|
352
|
+
# its drag from the press, which is where a real pointer would be.
|
|
353
|
+
var _moved: Vector2 = _arrive(position)
|
|
332
354
|
Input.parse_input_event(_button(position, button, pressed, double))
|
|
355
|
+
# A wheel step is a press and a release together, as a mouse sends one: there is no holding a
|
|
356
|
+
# wheel. A lone wheel press left the viewport's mouse focus on the control that took it, with
|
|
357
|
+
# the wheel's bit in the focus mask, and every click after it landed on that control rather
|
|
358
|
+
# than under the pointer, landing true and doing nothing, until a release was sent by hand.
|
|
359
|
+
var notch: bool = pressed and _mask_of(button) == 0
|
|
360
|
+
if notch:
|
|
361
|
+
Input.parse_input_event(_button(position, button, false, false))
|
|
333
362
|
|
|
334
|
-
|
|
363
|
+
var answer: Dictionary = {
|
|
335
364
|
"type": "input_injected",
|
|
336
365
|
"input_type": "mouse_click",
|
|
337
366
|
"position": [position.x, position.y],
|
|
@@ -339,22 +368,25 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
339
368
|
"pressed": pressed,
|
|
340
369
|
"double": double
|
|
341
370
|
}
|
|
371
|
+
if notch:
|
|
372
|
+
answer["released"] = true
|
|
373
|
+
return answer
|
|
342
374
|
|
|
343
375
|
|
|
344
376
|
## Moves the pointer to a position, with the movement the event carries taken from where the
|
|
345
|
-
##
|
|
377
|
+
## last injected event put it unless the caller says otherwise.
|
|
346
378
|
##
|
|
347
379
|
## A real pointer never arrives without a `relative`, and a control that drags reads that field
|
|
348
380
|
## rather than the position, so a motion carrying none leaves a grip where it was however far the
|
|
349
|
-
## position moved. The
|
|
350
|
-
##
|
|
351
|
-
##
|
|
381
|
+
## position moved. The distance from where the bridge last put the pointer is what a real move
|
|
382
|
+
## would have carried. A relative the caller gives is kept, since a game may want to read a motion
|
|
383
|
+
## the position does not show.
|
|
352
384
|
func inject_mouse_motion(params: Dictionary) -> Dictionary:
|
|
353
385
|
var point: Variant = _read_point(params, "x", "y", "position")
|
|
354
386
|
if point is String:
|
|
355
387
|
return {"type": "error", "message": point}
|
|
356
388
|
var position: Vector2 = point
|
|
357
|
-
var relative: Vector2 = position
|
|
389
|
+
var relative: Vector2 = _arrive(position)
|
|
358
390
|
if params.has("relativeX") or params.has("relativeY"):
|
|
359
391
|
relative = Vector2(
|
|
360
392
|
Read.as_float(params.get("relativeX", 0.0)), Read.as_float(params.get("relativeY", 0.0))
|
|
@@ -516,7 +548,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
516
548
|
# Pushed into the viewport rather than through Input: Input accumulates events and flushes
|
|
517
549
|
# them at the next frame, so the hovered control read below would be the one from before
|
|
518
550
|
# the pointer moved. The viewport delivers it to the GUI the same way a real one arrives.
|
|
519
|
-
viewport.push_input(_motion(position,
|
|
551
|
+
viewport.push_input(_motion(position, _arrive(position)))
|
|
520
552
|
# What the engine itself thinks is under the pointer, which is the answer to "did it land",
|
|
521
553
|
# read before the press so the caller learns about a control on top rather than a click
|
|
522
554
|
# that went to it. Read in full here, path included, because nothing about that control
|
|
@@ -776,7 +808,7 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
|
|
|
776
808
|
return _no_such_button(params.get("button"))
|
|
777
809
|
var double: bool = Read.as_bool(params.get("double", false))
|
|
778
810
|
|
|
779
|
-
viewport.push_input(_motion(position,
|
|
811
|
+
viewport.push_input(_motion(position, _arrive(position)))
|
|
780
812
|
# Read before the press, for the reason the Control click reads it: what the caller needs to
|
|
781
813
|
# know is whether a panel is sitting over the room, and the press is what would change it.
|
|
782
814
|
var hovered: Control = viewport.gui_get_hovered_control()
|
|
@@ -873,4 +905,7 @@ func _resolve_mouse_button(raw: Variant) -> int:
|
|
|
873
905
|
return MOUSE_BUTTON_WHEEL_DOWN
|
|
874
906
|
_:
|
|
875
907
|
return -1
|
|
876
|
-
|
|
908
|
+
# The engine's buttons run from 1 to 9: 0 is MOUSE_BUTTON_NONE, which no event carries, and a
|
|
909
|
+
# number past the two extra buttons names nothing a mask bit can be made for.
|
|
910
|
+
var number: int = Read.as_int(raw, -1)
|
|
911
|
+
return number if number >= MOUSE_BUTTON_LEFT and number <= MOUSE_BUTTON_XBUTTON2 else -1
|
|
@@ -30,17 +30,33 @@ func _init(host: Node, values: Values) -> void:
|
|
|
30
30
|
_values = values
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
## The tree under a node, to a depth, with every stored property on each node when asked for all
|
|
34
|
+
## of them or the named ones when a list is given.
|
|
35
|
+
##
|
|
36
|
+
## The list is the shape most questions about a screen take: the `text` of every label and
|
|
37
|
+
## button under a panel. All of them for one row of a form answered seventy-six thousand
|
|
38
|
+
## characters, and the same question as a find with one property answered two.
|
|
33
39
|
func get_tree(params: Dictionary) -> Dictionary:
|
|
34
40
|
var root_path: String = str(params.get("root", "/root"))
|
|
35
41
|
var max_depth: int = Read.as_int(params.get("depth", 3), 3)
|
|
36
42
|
var include_properties: bool = Read.as_bool(params.get("include_properties", false))
|
|
43
|
+
var named: Array[String] = []
|
|
44
|
+
var listed: Variant = params.get("properties", [])
|
|
45
|
+
if listed is Array:
|
|
46
|
+
for each: Variant in listed:
|
|
47
|
+
named.append(str(each))
|
|
48
|
+
else:
|
|
49
|
+
return {"type": "error", "message": "properties must be a list of property names"}
|
|
37
50
|
|
|
38
51
|
var reached: Dictionary = Values.node_at(_host.get_tree().root, root_path)
|
|
39
52
|
if reached.has("message"):
|
|
40
53
|
return reached
|
|
41
54
|
var root: Node = reached["node"]
|
|
42
55
|
|
|
43
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
"type": "tree",
|
|
58
|
+
"root": _serialize_node_tree(root, 0, max_depth, include_properties, named),
|
|
59
|
+
}
|
|
44
60
|
|
|
45
61
|
|
|
46
62
|
## Nodes matching every filter given, as paths, so a caller can name what it wants without
|
|
@@ -91,7 +107,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
91
107
|
var node: Node = pending.pop_front()
|
|
92
108
|
var rest: bool = _matches_apart_from_name(node, wanted)
|
|
93
109
|
if rest and _named(node, wanted["name"]):
|
|
94
|
-
if not include_hidden and not
|
|
110
|
+
if not include_hidden and not shown(node):
|
|
95
111
|
hidden += 1
|
|
96
112
|
elif found.size() >= limit:
|
|
97
113
|
truncated = true
|
|
@@ -144,8 +160,8 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
144
160
|
## a plain Node sitting between a hidden panel and a label has no visibility to ask about, and the
|
|
145
161
|
## label answers that it is visible while nothing of it is on screen. Walking up is what makes a
|
|
146
162
|
## row hidden because the panel holding it is hidden, which is what somebody checking a screen is
|
|
147
|
-
## asking about.
|
|
148
|
-
static func
|
|
163
|
+
## asking about. A wait on words asks the same, so it reads this too.
|
|
164
|
+
static func shown(node: Node) -> bool:
|
|
149
165
|
var walk: Node = node
|
|
150
166
|
while walk != null:
|
|
151
167
|
if not _drawn(walk):
|
|
@@ -166,10 +182,10 @@ func _found(node: Node, wanted_property: String) -> Dictionary:
|
|
|
166
182
|
if wanted_property.is_empty():
|
|
167
183
|
return entry
|
|
168
184
|
entry["property"] = wanted_property
|
|
169
|
-
# Through a path as well, for the reason [method
|
|
185
|
+
# Through a path as well, for the reason [method walk_to] gives. A step that is not there on
|
|
170
186
|
# this node reads as not having the property rather than as a refusal, which is the rule this
|
|
171
187
|
# whole function is written to: a find matches nodes of several classes on purpose.
|
|
172
|
-
var reached: Dictionary =
|
|
188
|
+
var reached: Dictionary = walk_to(node, str(entry["path"]), wanted_property)
|
|
173
189
|
if reached.has("message"):
|
|
174
190
|
entry["has_property"] = false
|
|
175
191
|
return entry
|
|
@@ -178,7 +194,7 @@ func _found(node: Node, wanted_property: String) -> Dictionary:
|
|
|
178
194
|
var has: bool = _can_read(holder, named)
|
|
179
195
|
entry["has_property"] = has
|
|
180
196
|
if has:
|
|
181
|
-
entry["value"] = _values.serialize(
|
|
197
|
+
entry["value"] = _values.serialize(read_under(holder, named))
|
|
182
198
|
return entry
|
|
183
199
|
|
|
184
200
|
|
|
@@ -532,7 +548,7 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
532
548
|
return standing
|
|
533
549
|
var node: Node = standing["node"]
|
|
534
550
|
|
|
535
|
-
var reached: Dictionary =
|
|
551
|
+
var reached: Dictionary = walk_to(node, node_path, property)
|
|
536
552
|
if reached.has("message"):
|
|
537
553
|
return reached
|
|
538
554
|
|
|
@@ -547,7 +563,7 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
547
563
|
"type": "property",
|
|
548
564
|
"path": node_path,
|
|
549
565
|
"property": property,
|
|
550
|
-
"value": _values.serialize(
|
|
566
|
+
"value": _values.serialize(read_under(holder, named)),
|
|
551
567
|
}
|
|
552
568
|
|
|
553
569
|
|
|
@@ -563,10 +579,16 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
563
579
|
## at a time rather than handed to that method, which answers null for a path that goes wrong
|
|
564
580
|
## halfway along and for one that ends on null.
|
|
565
581
|
##
|
|
582
|
+
## A step written as a call, "get_viewport()", calls a method of that name that takes no arguments
|
|
583
|
+
## and walks into what it returned. Some of what hangs off a node is only reachable by asking:
|
|
584
|
+
## which control holds the focus is a question for the viewport a control is in, and the viewport
|
|
585
|
+
## is a method's answer rather than a property. Spelled with the brackets so a reader sees a call
|
|
586
|
+
## where one happens, and so a method can never shadow a property of the same name.
|
|
587
|
+
##
|
|
566
588
|
## Answers with a `message` instead when a step along the way is not there or holds something that
|
|
567
589
|
## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
|
|
568
590
|
## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle.
|
|
569
|
-
func
|
|
591
|
+
static func walk_to(node: Node, node_path: String, reaching: String) -> Dictionary:
|
|
570
592
|
var parts: PackedStringArray = reaching.split(":")
|
|
571
593
|
var holder: Variant = node
|
|
572
594
|
var called: String = node_path
|
|
@@ -574,7 +596,7 @@ func _reached(node: Node, node_path: String, reaching: String) -> Dictionary:
|
|
|
574
596
|
var named: String = parts[step]
|
|
575
597
|
if not _can_read(holder, named):
|
|
576
598
|
return {"type": "error", "message": _nothing_there(holder, named, called)}
|
|
577
|
-
holder =
|
|
599
|
+
holder = read_under(holder, named)
|
|
578
600
|
called = "%s:%s" % [called, named]
|
|
579
601
|
if not _can_hold(holder):
|
|
580
602
|
var wanted: String = parts[step + 1]
|
|
@@ -613,10 +635,33 @@ static func _can_read(holder: Variant, named: String) -> bool:
|
|
|
613
635
|
return map.has(named) or map.has(StringName(named))
|
|
614
636
|
if holder is Object:
|
|
615
637
|
var object: Object = holder
|
|
638
|
+
var method: String = _called(named)
|
|
639
|
+
if not method.is_empty():
|
|
640
|
+
return _arguments_required(object, method) == 0
|
|
616
641
|
return _has_property(object, named)
|
|
617
642
|
return false
|
|
618
643
|
|
|
619
644
|
|
|
645
|
+
## The method a step written as a call names, "get_viewport()" being get_viewport, or "" for a
|
|
646
|
+
## step that is a name.
|
|
647
|
+
static func _called(named: String) -> String:
|
|
648
|
+
return named.trim_suffix("()") if named.ends_with("()") else ""
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
## How many arguments [param method] on [param object] has to be given, or -1 when there is no such
|
|
652
|
+
## method. Read off the method list rather than [method Object.has_method], because a path can only
|
|
653
|
+
## call a method with nothing, and a method with a required argument called with nothing raises
|
|
654
|
+
## inside the game rather than answering.
|
|
655
|
+
static func _arguments_required(object: Object, method: String) -> int:
|
|
656
|
+
for entry: Dictionary in object.get_method_list():
|
|
657
|
+
if str(entry.get("name", "")) != method:
|
|
658
|
+
continue
|
|
659
|
+
var params: Array = entry.get("args", [])
|
|
660
|
+
var defaults: Array = entry.get("default_args", [])
|
|
661
|
+
return params.size() - defaults.size()
|
|
662
|
+
return -1
|
|
663
|
+
|
|
664
|
+
|
|
620
665
|
## Put [param value] where [param named] points. Only ever called once [method _can_read] agrees,
|
|
621
666
|
## and lists and maps are reference types, so writing into one reaches the game's own copy.
|
|
622
667
|
static func _write(holder: Variant, named: String, value: Variant) -> void:
|
|
@@ -637,8 +682,9 @@ static func _write(holder: Variant, named: String, value: Variant) -> void:
|
|
|
637
682
|
object.set(named, value)
|
|
638
683
|
|
|
639
684
|
|
|
640
|
-
## What [param holder] holds under [param named]
|
|
641
|
-
|
|
685
|
+
## What [param holder] holds under [param named], or answers to it when [param named] is a call.
|
|
686
|
+
## Only ever called once [method _can_read] agrees.
|
|
687
|
+
static func read_under(holder: Variant, named: String) -> Variant:
|
|
642
688
|
if holder is Array:
|
|
643
689
|
var items: Array = holder
|
|
644
690
|
return items[_index_in(named, items.size())]
|
|
@@ -648,6 +694,9 @@ static func _read(holder: Variant, named: String) -> Variant:
|
|
|
648
694
|
return map[named]
|
|
649
695
|
return map[StringName(named)]
|
|
650
696
|
var object: Object = holder
|
|
697
|
+
var method: String = _called(named)
|
|
698
|
+
if not method.is_empty():
|
|
699
|
+
return object.call(method)
|
|
651
700
|
return object.get(named)
|
|
652
701
|
|
|
653
702
|
|
|
@@ -685,6 +734,23 @@ static func _nothing_there(holder: Variant, named: String, called: String) -> St
|
|
|
685
734
|
if keys.size() > 8:
|
|
686
735
|
rest = " and %d more" % [keys.size() - 8]
|
|
687
736
|
return "%s has no key %s; it is keyed by %s%s" % [called, named, ", ".join(some), rest]
|
|
737
|
+
var object: Object = holder
|
|
738
|
+
var method: String = _called(named)
|
|
739
|
+
if not method.is_empty():
|
|
740
|
+
var required: int = _arguments_required(object, method)
|
|
741
|
+
if required == -1:
|
|
742
|
+
return "%s has no method %s" % [called, method]
|
|
743
|
+
return (
|
|
744
|
+
"%s.%s takes %d argument%s, and a path can only call a method that takes none"
|
|
745
|
+
% [called, method, required, "" if required == 1 else "s"]
|
|
746
|
+
)
|
|
747
|
+
# A method spelled as a property is the likeliest thing behind a name the object has no property
|
|
748
|
+
# for, and the caller is one pair of brackets away from what they meant.
|
|
749
|
+
if object.has_method(named):
|
|
750
|
+
return (
|
|
751
|
+
"%s has no property %s; it has a method of that name, which a path calls as %s()"
|
|
752
|
+
% [called, named, named]
|
|
753
|
+
)
|
|
688
754
|
return "%s has no property %s" % [called, named]
|
|
689
755
|
|
|
690
756
|
|
|
@@ -710,18 +776,29 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
710
776
|
return standing
|
|
711
777
|
var node: Node = standing["node"]
|
|
712
778
|
|
|
713
|
-
# Through a path as well, for the reason [method
|
|
779
|
+
# Through a path as well, for the reason [method walk_to] gives, and read back off the same
|
|
714
780
|
# holder afterwards: a set that does not take says so by answering with the old value, which is
|
|
715
781
|
# how a typed container refusing a write is told from one accepting it.
|
|
716
|
-
var reached: Dictionary =
|
|
782
|
+
var reached: Dictionary = walk_to(node, node_path, property)
|
|
717
783
|
if reached.has("message"):
|
|
718
784
|
return reached
|
|
719
785
|
|
|
720
786
|
var holder: Variant = reached["holder"]
|
|
721
787
|
var named: String = reached["name"]
|
|
788
|
+
# A call can be walked through and read, and is not a place: what it returns is the method's
|
|
789
|
+
# to hand out, and writing "into" it would set nothing the game keeps.
|
|
790
|
+
if not _called(named).is_empty():
|
|
791
|
+
return {
|
|
792
|
+
"type": "error",
|
|
793
|
+
"message":
|
|
794
|
+
(
|
|
795
|
+
"%s:%s is a call, and a call is not a place to write: name a property"
|
|
796
|
+
% [reached["called"], named]
|
|
797
|
+
)
|
|
798
|
+
}
|
|
722
799
|
if not _can_read(holder, named):
|
|
723
800
|
return {"type": "error", "message": _nothing_there(holder, named, str(reached["called"]))}
|
|
724
|
-
var old_value: Variant =
|
|
801
|
+
var old_value: Variant = read_under(holder, named)
|
|
725
802
|
# The same rule the call path holds: a value that cannot become what the property holds is
|
|
726
803
|
# refused rather than written. Writing it means the engine picks something, and what it picks
|
|
727
804
|
# for a word where a number goes is zero, which the answer then reports as the new value.
|
|
@@ -743,7 +820,7 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
743
820
|
"path": node_path,
|
|
744
821
|
"property": property,
|
|
745
822
|
"old_value": _values.serialize(old_value),
|
|
746
|
-
"new_value": _values.serialize(
|
|
823
|
+
"new_value": _values.serialize(read_under(holder, named))
|
|
747
824
|
}
|
|
748
825
|
|
|
749
826
|
|
|
@@ -760,10 +837,10 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
760
837
|
return standing
|
|
761
838
|
var node: Node = standing["node"]
|
|
762
839
|
|
|
763
|
-
# Through a path as well, for the reason [method
|
|
840
|
+
# Through a path as well, for the reason [method walk_to] gives. What a game does hangs off its
|
|
764
841
|
# nodes as much as its state does, so reading `_game:run:day` while being unable to call
|
|
765
842
|
# `_game:run:advance` answers half of what a node holds and refuses the other half.
|
|
766
|
-
var reached: Dictionary =
|
|
843
|
+
var reached: Dictionary = walk_to(node, node_path, method)
|
|
767
844
|
if reached.has("message"):
|
|
768
845
|
return reached
|
|
769
846
|
|
|
@@ -782,6 +859,11 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
782
859
|
}
|
|
783
860
|
var holder: Object = reached["holder"]
|
|
784
861
|
var named: String = reached["name"]
|
|
862
|
+
# The method a call ends on is the one being called whether or not it carries the brackets a
|
|
863
|
+
# step along the way would: "get_viewport():gui_get_focus_owner()" is the same call as without
|
|
864
|
+
# the last pair.
|
|
865
|
+
if not _called(named).is_empty():
|
|
866
|
+
named = _called(named)
|
|
785
867
|
if not holder.has_method(named):
|
|
786
868
|
return {"type": "error", "message": "%s has no method %s" % [reached["called"], named]}
|
|
787
869
|
|
|
@@ -846,8 +928,12 @@ func get_metrics(params: Dictionary) -> Dictionary:
|
|
|
846
928
|
return {"type": "metrics", "data": selected}
|
|
847
929
|
|
|
848
930
|
|
|
849
|
-
func _serialize_node_tree(
|
|
931
|
+
func _serialize_node_tree(
|
|
932
|
+
node: Node, depth: int, max_depth: int, include_properties: bool, named: Array[String]
|
|
933
|
+
) -> Dictionary:
|
|
850
934
|
var result: Dictionary = _serialize_node(node, include_properties)
|
|
935
|
+
if not named.is_empty():
|
|
936
|
+
result["properties"] = _named_properties_of(node, str(result["path"]), named)
|
|
851
937
|
|
|
852
938
|
if depth < max_depth:
|
|
853
939
|
var children: Array = []
|
|
@@ -856,12 +942,28 @@ func _serialize_node_tree(node: Node, depth: int, max_depth: int, include_proper
|
|
|
856
942
|
# it is wrong, and it is what sends somebody looking for another way to press the button.
|
|
857
943
|
# `depth` is what keeps the answer a size worth reading.
|
|
858
944
|
for child: Node in node.get_children(true):
|
|
859
|
-
children.append(_serialize_node_tree(child, depth + 1, max_depth, include_properties))
|
|
945
|
+
children.append(_serialize_node_tree(child, depth + 1, max_depth, include_properties, named))
|
|
860
946
|
result["children"] = children
|
|
861
947
|
|
|
862
948
|
return result
|
|
863
949
|
|
|
864
950
|
|
|
951
|
+
## The named properties a node has, read the way a find reads its one: through a colon path as
|
|
952
|
+
## well, and a name this node has not got is left out rather than answered null, since a tree
|
|
953
|
+
## holds nodes of every class and a label's `text` is not a container's.
|
|
954
|
+
func _named_properties_of(node: Node, path: String, named: Array[String]) -> Dictionary:
|
|
955
|
+
var properties: Dictionary = {}
|
|
956
|
+
for wanted: String in named:
|
|
957
|
+
var reached: Dictionary = walk_to(node, path, wanted)
|
|
958
|
+
if reached.has("message"):
|
|
959
|
+
continue
|
|
960
|
+
var holder: Variant = reached["holder"]
|
|
961
|
+
var name: String = reached["name"]
|
|
962
|
+
if _can_read(holder, name):
|
|
963
|
+
properties[wanted] = _values.serialize(read_under(holder, name))
|
|
964
|
+
return properties
|
|
965
|
+
|
|
966
|
+
|
|
865
967
|
func _serialize_node(node: Node, include_properties: bool) -> Dictionary:
|
|
866
968
|
var result: Dictionary = {"name": node.name, "type": node.get_class(), "path": str(node.get_path())}
|
|
867
969
|
|
|
@@ -115,7 +115,8 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
115
115
|
)
|
|
116
116
|
}
|
|
117
117
|
if not says.is_empty():
|
|
118
|
-
|
|
118
|
+
var include_hidden: bool = Read.as_bool(params.get("include_hidden", false))
|
|
119
|
+
return await _wait_until_said(node_path, says, timeout_ms, include_hidden)
|
|
119
120
|
if property.is_empty():
|
|
120
121
|
return {"type": "error", "message": "A property and a value, or says, are required"}
|
|
121
122
|
if not params.has("value"):
|
|
@@ -127,44 +128,15 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
127
128
|
# met" after the whole timeout, which is what a game that never reached the state answers too,
|
|
128
129
|
# so a mistyped name and a condition that did not happen read the same. The property read
|
|
129
130
|
# refuses that typo in one call, and two tools disagreeing about it is the fault.
|
|
130
|
-
var
|
|
131
|
-
if
|
|
132
|
-
return
|
|
131
|
+
var watched: Dictionary = _watched(node, node_path, property)
|
|
132
|
+
if watched.has("message"):
|
|
133
|
+
return watched
|
|
133
134
|
|
|
134
|
-
var current: Variant =
|
|
135
|
+
var current: Variant = watched["value"]
|
|
135
136
|
var wanted: Variant = _values.fitted(params["value"], typeof(current))
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
# from where they are standing.
|
|
140
|
-
if not Values.comparable(current, wanted):
|
|
141
|
-
return {
|
|
142
|
-
"type": "error",
|
|
143
|
-
"message":
|
|
144
|
-
(
|
|
145
|
-
(
|
|
146
|
-
"%s.%s holds %s and the value to wait for is %s. Those cannot be compared, and"
|
|
147
|
-
+ " waiting on it would stop the game rather than answer about it."
|
|
148
|
-
)
|
|
149
|
-
% [node_path, property, type_string(typeof(current)), type_string(typeof(wanted))]
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
# And a value that cannot become what the property holds, which lands worse than the halt: a
|
|
153
|
-
# word where a number goes became 0.0, 0.0 already equalled the property, and the wait answered
|
|
154
|
-
# met the instant it started, about a state nobody asked about, with the real value beside it.
|
|
155
|
-
# A refusal is read and a met is acted on.
|
|
156
|
-
if not Values.acceptable(wanted, typeof(current)):
|
|
157
|
-
return {
|
|
158
|
-
"type": "error",
|
|
159
|
-
"message":
|
|
160
|
-
(
|
|
161
|
-
(
|
|
162
|
-
"%s.%s holds %s and the value to wait for is %s, which cannot become one:"
|
|
163
|
-
+ " waiting on it would answer about a state nobody asked for."
|
|
164
|
-
)
|
|
165
|
-
% [node_path, property, type_string(typeof(current)), type_string(typeof(wanted))]
|
|
166
|
-
)
|
|
167
|
-
}
|
|
137
|
+
var refused: String = _not_comparable(current, wanted, node_path, property)
|
|
138
|
+
if not refused.is_empty():
|
|
139
|
+
return {"type": "error", "message": refused}
|
|
168
140
|
|
|
169
141
|
var started: int = Time.get_ticks_msec()
|
|
170
142
|
# The type is checked every time round, not only at the top: a property that holds an object a
|
|
@@ -177,7 +149,10 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
177
149
|
await _host.get_tree().process_frame
|
|
178
150
|
if not is_instance_valid(node):
|
|
179
151
|
return {"type": "error", "message": "%s was freed while waiting" % node_path}
|
|
180
|
-
|
|
152
|
+
watched = _watched(node, node_path, property)
|
|
153
|
+
if watched.has("message"):
|
|
154
|
+
return watched
|
|
155
|
+
current = watched["value"]
|
|
181
156
|
|
|
182
157
|
return {
|
|
183
158
|
"type": "condition",
|
|
@@ -189,6 +164,60 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
189
164
|
}
|
|
190
165
|
|
|
191
166
|
|
|
167
|
+
## Why [param wanted] cannot be waited for against [param current], or "" when it can.
|
|
168
|
+
##
|
|
169
|
+
## Refused before anything is evaluated, because the evaluation is what does the damage: an object
|
|
170
|
+
## compared against anything else is a hard error in GDScript, raised inside the game, which holds
|
|
171
|
+
## it at a debugger break. Both types are named because the caller cannot see either from where
|
|
172
|
+
## they are standing.
|
|
173
|
+
##
|
|
174
|
+
## And a value that cannot become what the property holds, which lands worse than the halt: a word
|
|
175
|
+
## where a number goes became 0.0, 0.0 already equalled the property, and the wait answered met the
|
|
176
|
+
## instant it started, about a state nobody asked about, with the real value beside it. A refusal
|
|
177
|
+
## is read and a met is acted on.
|
|
178
|
+
static func _not_comparable(current: Variant, wanted: Variant, node_path: String, property: String) -> String:
|
|
179
|
+
var types: Array[String] = [
|
|
180
|
+
node_path, property, type_string(typeof(current)), type_string(typeof(wanted))
|
|
181
|
+
]
|
|
182
|
+
if not Values.comparable(current, wanted):
|
|
183
|
+
return (
|
|
184
|
+
(
|
|
185
|
+
"%s.%s holds %s and the value to wait for is %s. Those cannot be compared, and"
|
|
186
|
+
+ " waiting on it would stop the game rather than answer about it."
|
|
187
|
+
)
|
|
188
|
+
% types
|
|
189
|
+
)
|
|
190
|
+
if not Values.acceptable(wanted, typeof(current)):
|
|
191
|
+
return (
|
|
192
|
+
(
|
|
193
|
+
"%s.%s holds %s and the value to wait for is %s, which cannot become one:"
|
|
194
|
+
+ " waiting on it would answer about a state nobody asked for."
|
|
195
|
+
)
|
|
196
|
+
% types
|
|
197
|
+
)
|
|
198
|
+
return ""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
## What [param property] reads as on [param node] right now, under `value`, or a `message` saying
|
|
202
|
+
## why it cannot be read.
|
|
203
|
+
##
|
|
204
|
+
## Through the same walk as the property read, so a wait can watch what a node holds,
|
|
205
|
+
## "_game:run:day", and what a call answers, "get_viewport():gui_get_focus_owner()". Walked from the
|
|
206
|
+
## node every time rather than read off the holder found at the start, because a game replaces what
|
|
207
|
+
## its nodes hold: a run object built afresh each day would leave a wait watching the day before. A
|
|
208
|
+
## step that has gone in the meantime ends the wait with what went.
|
|
209
|
+
static func _watched(node: Node, node_path: String, property: String) -> Dictionary:
|
|
210
|
+
var reached: Dictionary = Queries.walk_to(node, node_path, property)
|
|
211
|
+
if reached.has("message"):
|
|
212
|
+
return reached
|
|
213
|
+
var holder: Variant = reached["holder"]
|
|
214
|
+
var named: String = reached["name"]
|
|
215
|
+
var missing: String = Queries.nothing_under(holder, named, str(reached["called"]))
|
|
216
|
+
if not missing.is_empty():
|
|
217
|
+
return {"type": "error", "message": missing}
|
|
218
|
+
return {"value": Queries.read_under(holder, named)}
|
|
219
|
+
|
|
220
|
+
|
|
192
221
|
## Waits until something under [param node_path] has [param said] written on it.
|
|
193
222
|
##
|
|
194
223
|
## A screen rather than one node, because a panel following a clock builds its labels again every
|
|
@@ -196,34 +225,40 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
196
225
|
## on a node that was freed a frame later, and that is what it answered: the date along the top of a
|
|
197
226
|
## hall could not be waited on at all. What a caller is watching for is a word arriving on a screen,
|
|
198
227
|
## and the screen is the part that stays put.
|
|
199
|
-
|
|
228
|
+
##
|
|
229
|
+
## On the screen, which is to say shown: a control carrying the words while hidden satisfied the
|
|
230
|
+
## wait at once, so a duel screen whose "Carry on" button exists hidden through the whole sweep
|
|
231
|
+
## answered met in two milliseconds while the sweep was still animating, and every wait on that
|
|
232
|
+
## screen fell back to counting frames. [param include_hidden] asks the other question, for a
|
|
233
|
+
## caller waiting on words a hidden node holds.
|
|
234
|
+
func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_hidden: bool) -> Dictionary:
|
|
200
235
|
var started: int = Time.get_ticks_msec()
|
|
201
236
|
var words: String = Queries.as_said(said)
|
|
202
|
-
var found: bool = _anything_says(node_path, words)
|
|
237
|
+
var found: bool = _anything_says(node_path, words, include_hidden)
|
|
203
238
|
while not found and Time.get_ticks_msec() - started < timeout_ms:
|
|
204
239
|
await _host.get_tree().process_frame
|
|
205
|
-
found = _anything_says(node_path, words)
|
|
240
|
+
found = _anything_says(node_path, words, include_hidden)
|
|
206
241
|
|
|
207
242
|
return {
|
|
208
243
|
"type": "condition",
|
|
209
244
|
"path": node_path,
|
|
210
245
|
"says": said,
|
|
211
246
|
"met": found,
|
|
247
|
+
"include_hidden": include_hidden,
|
|
212
248
|
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
213
249
|
}
|
|
214
250
|
|
|
215
251
|
|
|
216
|
-
## Whether anything under [param node_path] says [param said], the node itself included
|
|
217
|
-
##
|
|
218
|
-
|
|
219
|
-
func _anything_says(node_path: String, said: String) -> bool:
|
|
252
|
+
## Whether anything under [param node_path] says [param said], the node itself included, and shown
|
|
253
|
+
## unless hidden ones are wanted too.
|
|
254
|
+
func _anything_says(node_path: String, said: String, include_hidden: bool) -> bool:
|
|
220
255
|
var root: Node = _host.get_tree().root.get_node_or_null(node_path)
|
|
221
256
|
if root == null:
|
|
222
257
|
return false
|
|
223
258
|
var pending: Array[Node] = [root]
|
|
224
259
|
while not pending.is_empty():
|
|
225
260
|
var node: Node = pending.pop_back()
|
|
226
|
-
if Queries.said_by(node).containsn(said):
|
|
261
|
+
if Queries.said_by(node).containsn(said) and (include_hidden or Queries.shown(node)):
|
|
227
262
|
return true
|
|
228
263
|
pending.append_array(node.get_children(true))
|
|
229
264
|
return false
|