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.
@@ -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
- return {
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
- ## pointer last was unless the caller says otherwise.
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 viewport remembers where the last motion put the pointer, injected or not,
350
- ## so the difference from there is what a real move would have carried. A relative the caller gives
351
- ## is kept, since a game may want to read a motion the position does not show.
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 - _host.get_viewport().get_mouse_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, Vector2.ZERO))
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, Vector2.ZERO))
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
- return Read.as_int(raw, -1)
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 {"type": "tree", "root": _serialize_node_tree(root, 0, max_depth, include_properties)}
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 _shown(node):
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 _shown(node: Node) -> bool:
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 _reached] gives. A step that is not there on
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 = _reached(node, str(entry["path"]), wanted_property)
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(_read(holder, named))
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 = _reached(node, node_path, property)
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(_read(holder, named)),
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 _reached(node: Node, node_path: String, reaching: String) -> Dictionary:
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 = _read(holder, named)
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]. Only ever called once [method _can_read] agrees.
641
- static func _read(holder: Variant, named: String) -> Variant:
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 _reached] gives, and read back off the same
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 = _reached(node, node_path, property)
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 = _read(holder, named)
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(_read(holder, named))
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 _reached] gives. What a game does hangs off its
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 = _reached(node, node_path, method)
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(node: Node, depth: int, max_depth: int, include_properties: bool) -> Dictionary:
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
- return await _wait_until_said(node_path, says, timeout_ms)
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 missing: String = Queries.nothing_under(node, property, node_path)
131
- if not missing.is_empty():
132
- return {"type": "error", "message": missing}
131
+ var watched: Dictionary = _watched(node, node_path, property)
132
+ if watched.has("message"):
133
+ return watched
133
134
 
134
- var current: Variant = node.get(property)
135
+ var current: Variant = watched["value"]
135
136
  var wanted: Variant = _values.fitted(params["value"], typeof(current))
136
- # Refused before anything is evaluated, because the evaluation is what does the damage: an
137
- # object compared against anything else is a hard error in GDScript, raised inside the game,
138
- # which holds it at a debugger break. Both types are named because the caller cannot see either
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
- current = node.get(property)
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
- func _wait_until_said(node_path: String, said: String, timeout_ms: int) -> Dictionary:
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. Hidden
217
- ## nodes count, for the reason a find answers off them: a caller may be waiting for a dialog that
218
- ## is built before it is shown.
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