gdharness 1.0.23 → 1.0.25
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.
|
@@ -6,6 +6,51 @@ extends RefCounted
|
|
|
6
6
|
|
|
7
7
|
const Values = preload("runtime_values.gd")
|
|
8
8
|
|
|
9
|
+
## The calls a path makes on a list or a map: those that take no arguments and change nothing.
|
|
10
|
+
##
|
|
11
|
+
## Named rather than asked of the engine, because asking about a method a list does not have logs an
|
|
12
|
+
## error into the game's output, and a typo in a path is not the game's error. The ones that change
|
|
13
|
+
## what they are called on, such as clear(), sort() and pop_back(), are left out: a path is how a
|
|
14
|
+
## read and a wait reach what they read, and a wait asks many times.
|
|
15
|
+
const LIST_CALLS: Array[String] = [
|
|
16
|
+
"size",
|
|
17
|
+
"is_empty",
|
|
18
|
+
"front",
|
|
19
|
+
"back",
|
|
20
|
+
"max",
|
|
21
|
+
"min",
|
|
22
|
+
"pick_random",
|
|
23
|
+
"hash",
|
|
24
|
+
"duplicate",
|
|
25
|
+
"duplicate_deep",
|
|
26
|
+
"is_read_only",
|
|
27
|
+
"is_typed",
|
|
28
|
+
"get_typed_builtin",
|
|
29
|
+
"get_typed_class_name",
|
|
30
|
+
"get_typed_script",
|
|
31
|
+
]
|
|
32
|
+
const MAP_CALLS: Array[String] = [
|
|
33
|
+
"size",
|
|
34
|
+
"is_empty",
|
|
35
|
+
"keys",
|
|
36
|
+
"values",
|
|
37
|
+
"hash",
|
|
38
|
+
"duplicate",
|
|
39
|
+
"duplicate_deep",
|
|
40
|
+
"is_read_only",
|
|
41
|
+
"is_typed",
|
|
42
|
+
"is_typed_key",
|
|
43
|
+
"is_typed_value",
|
|
44
|
+
"get_typed_key_builtin",
|
|
45
|
+
"get_typed_key_class_name",
|
|
46
|
+
"get_typed_key_script",
|
|
47
|
+
"get_typed_value_builtin",
|
|
48
|
+
"get_typed_value_class_name",
|
|
49
|
+
"get_typed_value_script",
|
|
50
|
+
]
|
|
51
|
+
## The calls an empty list has nothing to answer, which the engine reports as an error when asked.
|
|
52
|
+
const NEEDS_AN_ELEMENT: Array[String] = ["front", "back", "pick_random"]
|
|
53
|
+
|
|
9
54
|
|
|
10
55
|
## What [param reaching] names something on, which is the node itself until the name has a colon
|
|
11
56
|
## in it, and the last name along that path.
|
|
@@ -23,7 +68,9 @@ const Values = preload("runtime_values.gd")
|
|
|
23
68
|
## and walks into what it returned. Some of what hangs off a node is only reachable by asking:
|
|
24
69
|
## which control holds the focus is a question for the viewport a control is in, and the viewport
|
|
25
70
|
## is a method's answer rather than a property. Spelled with the brackets so a reader sees a call
|
|
26
|
-
## where one happens, and so a method can never shadow a property of the same name.
|
|
71
|
+
## where one happens, and so a method can never shadow a property of the same name. A list or a map
|
|
72
|
+
## answers the calls that read it, "board:size()" or "inbox:keys()", listed in [constant LIST_CALLS]
|
|
73
|
+
## and [constant MAP_CALLS].
|
|
27
74
|
##
|
|
28
75
|
## Answers with a `message` instead when a step along the way is not there or holds something that
|
|
29
76
|
## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
|
|
@@ -117,9 +164,14 @@ static func nothing_under(holder: Variant, named: String, called: String) -> Str
|
|
|
117
164
|
static func can_read(holder: Variant, named: String) -> bool:
|
|
118
165
|
if holder is Array:
|
|
119
166
|
var items: Array = holder
|
|
167
|
+
var list_call: String = method_of(named)
|
|
168
|
+
if not list_call.is_empty():
|
|
169
|
+
return list_call in LIST_CALLS and not (items.is_empty() and list_call in NEEDS_AN_ELEMENT)
|
|
120
170
|
return _index_in(named, items.size()) != -1
|
|
121
171
|
if holder is Dictionary:
|
|
122
172
|
var map: Dictionary = holder
|
|
173
|
+
if not method_of(named).is_empty():
|
|
174
|
+
return method_of(named) in MAP_CALLS
|
|
123
175
|
return map.has(named) or map.has(StringName(named))
|
|
124
176
|
if holder is Object:
|
|
125
177
|
var object: Object = holder
|
|
@@ -173,6 +225,8 @@ static func write(holder: Variant, named: String, value: Variant) -> void:
|
|
|
173
225
|
## What [param holder] holds under [param named], or answers to it when [param named] is a call.
|
|
174
226
|
## Only ever called once [method can_read] agrees.
|
|
175
227
|
static func read_under(holder: Variant, named: String) -> Variant:
|
|
228
|
+
if (holder is Array or holder is Dictionary) and not method_of(named).is_empty():
|
|
229
|
+
return Callable.create(holder, method_of(named)).call()
|
|
176
230
|
if holder is Array:
|
|
177
231
|
var items: Array = holder
|
|
178
232
|
return items[_index_in(named, items.size())]
|
|
@@ -207,10 +261,17 @@ static func _index_in(named: String, size: int) -> int:
|
|
|
207
261
|
## so a list says how long it is and a map says what it is keyed by. The step is named rather than
|
|
208
262
|
## the whole path, because the step is the typo.
|
|
209
263
|
static func nothing_there(holder: Variant, named: String, called_as: String) -> String:
|
|
264
|
+
var container_call: String = method_of(named)
|
|
210
265
|
if holder is Array:
|
|
211
266
|
var items: Array = holder
|
|
267
|
+
if container_call in NEEDS_AN_ELEMENT and items.is_empty():
|
|
268
|
+
return "%s is an empty list, so it has no %s" % [called_as, named]
|
|
269
|
+
if not container_call.is_empty():
|
|
270
|
+
return container_calls_refused(called_as, "a list", container_call)
|
|
212
271
|
return "%s is a list of %d, so there is no %s in it" % [called_as, items.size(), named]
|
|
213
272
|
if holder is Dictionary:
|
|
273
|
+
if not container_call.is_empty():
|
|
274
|
+
return container_calls_refused(called_as, "a map", container_call)
|
|
214
275
|
var map: Dictionary = holder
|
|
215
276
|
var keys: Array = map.keys()
|
|
216
277
|
# Eight of them, because a map keyed by something unexpected is told by the first few and a
|
|
@@ -242,6 +303,19 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
242
303
|
return "%s has no property %s" % [called_as, named]
|
|
243
304
|
|
|
244
305
|
|
|
306
|
+
## Why [param method] is not a call a path makes on [param sort] ("a list" or "a map"), naming
|
|
307
|
+
## the calls it does make.
|
|
308
|
+
static func container_calls_refused(called_as: String, sort: String, method: String) -> String:
|
|
309
|
+
var calls: Array[String] = LIST_CALLS if sort == "a list" else MAP_CALLS
|
|
310
|
+
var spelled: Array[String] = []
|
|
311
|
+
for each: String in calls:
|
|
312
|
+
spelled.append(each + "()")
|
|
313
|
+
return (
|
|
314
|
+
"%s is %s, and %s() is not one of the calls a path makes on one, which read it and take no arguments: %s"
|
|
315
|
+
% [called_as, sort, method, ", ".join(spelled)]
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
245
319
|
## Whether [param holder] declares [param named]. Asked of the list rather than read, because a
|
|
246
320
|
## property read back as null answers it wrongly.
|
|
247
321
|
static func _has_property(holder: Object, named: String) -> bool:
|
|
@@ -379,11 +379,17 @@ static func _drawn(node: Node) -> bool:
|
|
|
379
379
|
## Public because three questions are the same question: what a screen reads as, which nodes say a
|
|
380
380
|
## given word, and whether anything has come to say it yet. Two copies of what a node says is how
|
|
381
381
|
## the three of them come to disagree about a SpinBox.
|
|
382
|
+
##
|
|
383
|
+
## Read rather than looked up in the property list, which the engine builds afresh on every call: a
|
|
384
|
+
## wait asks this of every node on the screen every frame, and on a hall of 3,500 nodes the lookup
|
|
385
|
+
## took the game it was watching from 60 frames a second to 11. A node without the property reads
|
|
386
|
+
## as null, which is not a string.
|
|
382
387
|
static func said_by(node: Node) -> String:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
388
|
+
var text: Variant = node.get("text")
|
|
389
|
+
if not text is String:
|
|
390
|
+
return ""
|
|
391
|
+
var words: String = text
|
|
392
|
+
return words.strip_edges()
|
|
387
393
|
|
|
388
394
|
|
|
389
395
|
func get_rect(params: Dictionary) -> Dictionary:
|
|
@@ -695,17 +701,33 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
695
701
|
if reached.has("message"):
|
|
696
702
|
return reached
|
|
697
703
|
|
|
698
|
-
# A list or a map
|
|
699
|
-
#
|
|
700
|
-
#
|
|
704
|
+
# A list or a map answers the calls a path makes on one, and nothing else: said in those terms
|
|
705
|
+
# rather than as "has no method", which reads as a misspelling of a method that was never going
|
|
706
|
+
# to be there, and with the element spelled out, since a method on what it holds is the usual aim.
|
|
701
707
|
if not reached["holder"] is Object:
|
|
702
708
|
var sort: String = "a list" if reached["holder"] is Array else "a map"
|
|
709
|
+
var spelled: String = str(reached["name"])
|
|
710
|
+
if Paths.method_of(spelled).is_empty():
|
|
711
|
+
spelled += "()"
|
|
712
|
+
if args.is_empty() and Paths.can_read(reached["holder"], spelled):
|
|
713
|
+
var answered: Variant = Paths.read_under(reached["holder"], spelled)
|
|
714
|
+
return {
|
|
715
|
+
"type": "method_result",
|
|
716
|
+
"path": node_path,
|
|
717
|
+
"method": method,
|
|
718
|
+
"result": _values.serialize(answered),
|
|
719
|
+
}
|
|
720
|
+
var why: String = (
|
|
721
|
+
"%s:%s takes no arguments" % [reached["called"], spelled]
|
|
722
|
+
if Paths.can_read(reached["holder"], spelled)
|
|
723
|
+
else Paths.nothing_there(reached["holder"], spelled, str(reached["called"]))
|
|
724
|
+
)
|
|
703
725
|
return {
|
|
704
726
|
"type": "error",
|
|
705
727
|
"message":
|
|
706
728
|
(
|
|
707
|
-
"%s
|
|
708
|
-
% [
|
|
729
|
+
"%s. A method of what %s holds is called on the element, as in %s:0:%s"
|
|
730
|
+
% [why, sort, reached["called"], reached["name"]]
|
|
709
731
|
)
|
|
710
732
|
}
|
|
711
733
|
var holder: Object = reached["holder"]
|
|
@@ -15,6 +15,10 @@ const CEILING_MSEC: int = 120000
|
|
|
15
15
|
## The most frames one wait may cover, which is about half a minute of a game drawing slowly.
|
|
16
16
|
const MOST_FRAMES: int = 600
|
|
17
17
|
|
|
18
|
+
## How long a wait for words leaves between looks, as a multiple of what the last look took: nine
|
|
19
|
+
## gives looking about a tenth of the time.
|
|
20
|
+
const LOOKS_APART: int = 9
|
|
21
|
+
|
|
18
22
|
var _host: Node
|
|
19
23
|
var _values: Values
|
|
20
24
|
|
|
@@ -140,6 +144,7 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
140
144
|
return {"type": "error", "message": refused}
|
|
141
145
|
|
|
142
146
|
var started: int = Time.get_ticks_msec()
|
|
147
|
+
var frames: int = 0
|
|
143
148
|
# The type is checked every time round, not only at the top: a property that holds an object a
|
|
144
149
|
# frame later is the same error arriving late.
|
|
145
150
|
while (
|
|
@@ -148,6 +153,7 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
148
153
|
and Time.get_ticks_msec() - started < timeout_ms
|
|
149
154
|
):
|
|
150
155
|
await _host.get_tree().process_frame
|
|
156
|
+
frames += 1
|
|
151
157
|
if not is_instance_valid(node):
|
|
152
158
|
return {"type": "error", "message": "%s was freed while waiting" % node_path}
|
|
153
159
|
watched = _watched(node, node_path, property)
|
|
@@ -162,6 +168,7 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
162
168
|
"met": Values.comparable(current, wanted) and current == wanted,
|
|
163
169
|
"value": _values.serialize(current),
|
|
164
170
|
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
171
|
+
"frames": frames,
|
|
165
172
|
}
|
|
166
173
|
|
|
167
174
|
|
|
@@ -232,13 +239,36 @@ static func _watched(node: Node, node_path: String, property: String) -> Diction
|
|
|
232
239
|
## answered met in two milliseconds while the sweep was still animating, and every wait on that
|
|
233
240
|
## screen fell back to counting frames. [param include_hidden] asks the other question, for a
|
|
234
241
|
## caller waiting on words a hidden node holds.
|
|
242
|
+
##
|
|
243
|
+
## Paced by what a look costs, so the watching leaves the game its speed: every frame while a look is
|
|
244
|
+
## cheap, and on a screen large enough for one to cost a real share of a frame, often enough that
|
|
245
|
+
## looking takes about a tenth of the time. Every frame on a hall of 3,500 nodes held the game at a
|
|
246
|
+
## sixth of its frame rate, and its clock, which advances by the frame's delta, lost three quarters
|
|
247
|
+
## of its time, so a wait for a date timed out on a clock that turned a day every thirty seconds.
|
|
248
|
+
## The answer says how many frames the wait spanned and how many looks it took, which is how a
|
|
249
|
+
## caller sees what it cost.
|
|
235
250
|
func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_hidden: bool) -> Dictionary:
|
|
236
251
|
var started: int = Time.get_ticks_msec()
|
|
237
252
|
var words: String = Queries.as_said(said)
|
|
238
|
-
var
|
|
239
|
-
|
|
253
|
+
var frames: int = 0
|
|
254
|
+
var looks: int = 0
|
|
255
|
+
var next_look: int = 0
|
|
256
|
+
var found: bool = false
|
|
257
|
+
while true:
|
|
258
|
+
if Time.get_ticks_usec() >= next_look:
|
|
259
|
+
var looking: int = Time.get_ticks_usec()
|
|
260
|
+
found = _anything_says(node_path, words, include_hidden)
|
|
261
|
+
looks += 1
|
|
262
|
+
next_look = Time.get_ticks_usec() + (Time.get_ticks_usec() - looking) * LOOKS_APART
|
|
263
|
+
if found or Time.get_ticks_msec() - started >= timeout_ms:
|
|
264
|
+
break
|
|
240
265
|
await _host.get_tree().process_frame
|
|
266
|
+
frames += 1
|
|
267
|
+
# One more look at the end, so words that arrived between two paced looks are not answered as
|
|
268
|
+
# never having come.
|
|
269
|
+
if not found:
|
|
241
270
|
found = _anything_says(node_path, words, include_hidden)
|
|
271
|
+
looks += 1
|
|
242
272
|
|
|
243
273
|
return {
|
|
244
274
|
"type": "condition",
|
|
@@ -247,6 +277,8 @@ func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_
|
|
|
247
277
|
"met": found,
|
|
248
278
|
"include_hidden": include_hidden,
|
|
249
279
|
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
280
|
+
"frames": frames,
|
|
281
|
+
"looks": looks,
|
|
250
282
|
}
|
|
251
283
|
|
|
252
284
|
|