gdharness 1.0.2 → 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.
@@ -182,10 +182,10 @@ func _found(node: Node, wanted_property: String) -> Dictionary:
182
182
  if wanted_property.is_empty():
183
183
  return entry
184
184
  entry["property"] = wanted_property
185
- # 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
186
186
  # this node reads as not having the property rather than as a refusal, which is the rule this
187
187
  # whole function is written to: a find matches nodes of several classes on purpose.
188
- var reached: Dictionary = _reached(node, str(entry["path"]), wanted_property)
188
+ var reached: Dictionary = walk_to(node, str(entry["path"]), wanted_property)
189
189
  if reached.has("message"):
190
190
  entry["has_property"] = false
191
191
  return entry
@@ -194,7 +194,7 @@ func _found(node: Node, wanted_property: String) -> Dictionary:
194
194
  var has: bool = _can_read(holder, named)
195
195
  entry["has_property"] = has
196
196
  if has:
197
- entry["value"] = _values.serialize(_read(holder, named))
197
+ entry["value"] = _values.serialize(read_under(holder, named))
198
198
  return entry
199
199
 
200
200
 
@@ -548,7 +548,7 @@ func get_property(params: Dictionary) -> Dictionary:
548
548
  return standing
549
549
  var node: Node = standing["node"]
550
550
 
551
- var reached: Dictionary = _reached(node, node_path, property)
551
+ var reached: Dictionary = walk_to(node, node_path, property)
552
552
  if reached.has("message"):
553
553
  return reached
554
554
 
@@ -563,7 +563,7 @@ func get_property(params: Dictionary) -> Dictionary:
563
563
  "type": "property",
564
564
  "path": node_path,
565
565
  "property": property,
566
- "value": _values.serialize(_read(holder, named)),
566
+ "value": _values.serialize(read_under(holder, named)),
567
567
  }
568
568
 
569
569
 
@@ -579,10 +579,16 @@ func get_property(params: Dictionary) -> Dictionary:
579
579
  ## at a time rather than handed to that method, which answers null for a path that goes wrong
580
580
  ## halfway along and for one that ends on null.
581
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
+ ##
582
588
  ## Answers with a `message` instead when a step along the way is not there or holds something that
583
589
  ## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
584
590
  ## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle.
585
- func _reached(node: Node, node_path: String, reaching: String) -> Dictionary:
591
+ static func walk_to(node: Node, node_path: String, reaching: String) -> Dictionary:
586
592
  var parts: PackedStringArray = reaching.split(":")
587
593
  var holder: Variant = node
588
594
  var called: String = node_path
@@ -590,7 +596,7 @@ func _reached(node: Node, node_path: String, reaching: String) -> Dictionary:
590
596
  var named: String = parts[step]
591
597
  if not _can_read(holder, named):
592
598
  return {"type": "error", "message": _nothing_there(holder, named, called)}
593
- holder = _read(holder, named)
599
+ holder = read_under(holder, named)
594
600
  called = "%s:%s" % [called, named]
595
601
  if not _can_hold(holder):
596
602
  var wanted: String = parts[step + 1]
@@ -629,10 +635,33 @@ static func _can_read(holder: Variant, named: String) -> bool:
629
635
  return map.has(named) or map.has(StringName(named))
630
636
  if holder is Object:
631
637
  var object: Object = holder
638
+ var method: String = _called(named)
639
+ if not method.is_empty():
640
+ return _arguments_required(object, method) == 0
632
641
  return _has_property(object, named)
633
642
  return false
634
643
 
635
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
+
636
665
  ## Put [param value] where [param named] points. Only ever called once [method _can_read] agrees,
637
666
  ## and lists and maps are reference types, so writing into one reaches the game's own copy.
638
667
  static func _write(holder: Variant, named: String, value: Variant) -> void:
@@ -653,8 +682,9 @@ static func _write(holder: Variant, named: String, value: Variant) -> void:
653
682
  object.set(named, value)
654
683
 
655
684
 
656
- ## What [param holder] holds under [param named]. Only ever called once [method _can_read] agrees.
657
- 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:
658
688
  if holder is Array:
659
689
  var items: Array = holder
660
690
  return items[_index_in(named, items.size())]
@@ -664,6 +694,9 @@ static func _read(holder: Variant, named: String) -> Variant:
664
694
  return map[named]
665
695
  return map[StringName(named)]
666
696
  var object: Object = holder
697
+ var method: String = _called(named)
698
+ if not method.is_empty():
699
+ return object.call(method)
667
700
  return object.get(named)
668
701
 
669
702
 
@@ -701,6 +734,23 @@ static func _nothing_there(holder: Variant, named: String, called: String) -> St
701
734
  if keys.size() > 8:
702
735
  rest = " and %d more" % [keys.size() - 8]
703
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
+ )
704
754
  return "%s has no property %s" % [called, named]
705
755
 
706
756
 
@@ -726,18 +776,29 @@ func set_property(params: Dictionary) -> Dictionary:
726
776
  return standing
727
777
  var node: Node = standing["node"]
728
778
 
729
- # 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
730
780
  # holder afterwards: a set that does not take says so by answering with the old value, which is
731
781
  # how a typed container refusing a write is told from one accepting it.
732
- var reached: Dictionary = _reached(node, node_path, property)
782
+ var reached: Dictionary = walk_to(node, node_path, property)
733
783
  if reached.has("message"):
734
784
  return reached
735
785
 
736
786
  var holder: Variant = reached["holder"]
737
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
+ }
738
799
  if not _can_read(holder, named):
739
800
  return {"type": "error", "message": _nothing_there(holder, named, str(reached["called"]))}
740
- var old_value: Variant = _read(holder, named)
801
+ var old_value: Variant = read_under(holder, named)
741
802
  # The same rule the call path holds: a value that cannot become what the property holds is
742
803
  # refused rather than written. Writing it means the engine picks something, and what it picks
743
804
  # for a word where a number goes is zero, which the answer then reports as the new value.
@@ -759,7 +820,7 @@ func set_property(params: Dictionary) -> Dictionary:
759
820
  "path": node_path,
760
821
  "property": property,
761
822
  "old_value": _values.serialize(old_value),
762
- "new_value": _values.serialize(_read(holder, named))
823
+ "new_value": _values.serialize(read_under(holder, named))
763
824
  }
764
825
 
765
826
 
@@ -776,10 +837,10 @@ func call_method(params: Dictionary) -> Dictionary:
776
837
  return standing
777
838
  var node: Node = standing["node"]
778
839
 
779
- # 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
780
841
  # nodes as much as its state does, so reading `_game:run:day` while being unable to call
781
842
  # `_game:run:advance` answers half of what a node holds and refuses the other half.
782
- var reached: Dictionary = _reached(node, node_path, method)
843
+ var reached: Dictionary = walk_to(node, node_path, method)
783
844
  if reached.has("message"):
784
845
  return reached
785
846
 
@@ -798,6 +859,11 @@ func call_method(params: Dictionary) -> Dictionary:
798
859
  }
799
860
  var holder: Object = reached["holder"]
800
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)
801
867
  if not holder.has_method(named):
802
868
  return {"type": "error", "message": "%s has no method %s" % [reached["called"], named]}
803
869
 
@@ -888,13 +954,13 @@ func _serialize_node_tree(
888
954
  func _named_properties_of(node: Node, path: String, named: Array[String]) -> Dictionary:
889
955
  var properties: Dictionary = {}
890
956
  for wanted: String in named:
891
- var reached: Dictionary = _reached(node, path, wanted)
957
+ var reached: Dictionary = walk_to(node, path, wanted)
892
958
  if reached.has("message"):
893
959
  continue
894
960
  var holder: Variant = reached["holder"]
895
961
  var name: String = reached["name"]
896
962
  if _can_read(holder, name):
897
- properties[wanted] = _values.serialize(_read(holder, name))
963
+ properties[wanted] = _values.serialize(read_under(holder, name))
898
964
  return properties
899
965
 
900
966
 
@@ -128,44 +128,15 @@ func wait_until(params: Dictionary) -> Dictionary:
128
128
  # met" after the whole timeout, which is what a game that never reached the state answers too,
129
129
  # so a mistyped name and a condition that did not happen read the same. The property read
130
130
  # refuses that typo in one call, and two tools disagreeing about it is the fault.
131
- var missing: String = Queries.nothing_under(node, property, node_path)
132
- if not missing.is_empty():
133
- return {"type": "error", "message": missing}
131
+ var watched: Dictionary = _watched(node, node_path, property)
132
+ if watched.has("message"):
133
+ return watched
134
134
 
135
- var current: Variant = node.get(property)
135
+ var current: Variant = watched["value"]
136
136
  var wanted: Variant = _values.fitted(params["value"], typeof(current))
137
- # Refused before anything is evaluated, because the evaluation is what does the damage: an
138
- # object compared against anything else is a hard error in GDScript, raised inside the game,
139
- # which holds it at a debugger break. Both types are named because the caller cannot see either
140
- # from where they are standing.
141
- if not Values.comparable(current, wanted):
142
- return {
143
- "type": "error",
144
- "message":
145
- (
146
- (
147
- "%s.%s holds %s and the value to wait for is %s. Those cannot be compared, and"
148
- + " waiting on it would stop the game rather than answer about it."
149
- )
150
- % [node_path, property, type_string(typeof(current)), type_string(typeof(wanted))]
151
- )
152
- }
153
- # And a value that cannot become what the property holds, which lands worse than the halt: a
154
- # word where a number goes became 0.0, 0.0 already equalled the property, and the wait answered
155
- # met the instant it started, about a state nobody asked about, with the real value beside it.
156
- # A refusal is read and a met is acted on.
157
- if not Values.acceptable(wanted, typeof(current)):
158
- return {
159
- "type": "error",
160
- "message":
161
- (
162
- (
163
- "%s.%s holds %s and the value to wait for is %s, which cannot become one:"
164
- + " waiting on it would answer about a state nobody asked for."
165
- )
166
- % [node_path, property, type_string(typeof(current)), type_string(typeof(wanted))]
167
- )
168
- }
137
+ var refused: String = _not_comparable(current, wanted, node_path, property)
138
+ if not refused.is_empty():
139
+ return {"type": "error", "message": refused}
169
140
 
170
141
  var started: int = Time.get_ticks_msec()
171
142
  # The type is checked every time round, not only at the top: a property that holds an object a
@@ -178,7 +149,10 @@ func wait_until(params: Dictionary) -> Dictionary:
178
149
  await _host.get_tree().process_frame
179
150
  if not is_instance_valid(node):
180
151
  return {"type": "error", "message": "%s was freed while waiting" % node_path}
181
- current = node.get(property)
152
+ watched = _watched(node, node_path, property)
153
+ if watched.has("message"):
154
+ return watched
155
+ current = watched["value"]
182
156
 
183
157
  return {
184
158
  "type": "condition",
@@ -190,6 +164,60 @@ func wait_until(params: Dictionary) -> Dictionary:
190
164
  }
191
165
 
192
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
+
193
221
  ## Waits until something under [param node_path] has [param said] written on it.
194
222
  ##
195
223
  ## A screen rather than one node, because a panel following a clock builds its labels again every