gdharness 1.1.45 → 1.1.47
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 +217 -51
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +97 -3
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +13 -2
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd +1 -1
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +160 -41
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +26 -2
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +183 -12
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +14 -3
- package/build/index.js +218 -58
- package/package.json +1 -1
|
@@ -233,13 +233,17 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
233
233
|
var read_from: int = Time.get_ticks_usec()
|
|
234
234
|
var answered: Variant = Paths.read_under(reached["holder"], spelled)
|
|
235
235
|
var read_for: int = Time.get_ticks_usec() - read_from
|
|
236
|
-
|
|
236
|
+
var listed: Dictionary = {
|
|
237
237
|
"type": "method_result",
|
|
238
238
|
"path": node_path,
|
|
239
239
|
"method": method,
|
|
240
240
|
"result": _values.serialize(answered),
|
|
241
241
|
"elapsed_usec": read_for,
|
|
242
242
|
}
|
|
243
|
+
var asked_of_it: Variant = params.get("properties")
|
|
244
|
+
if _reads_the_result(asked_of_it):
|
|
245
|
+
listed.merge(_read_off_result(answered, asked_of_it))
|
|
246
|
+
return listed
|
|
243
247
|
var why: String = (
|
|
244
248
|
"%s:%s takes no arguments" % [reached["called"], spelled]
|
|
245
249
|
if Paths.can_read(reached["holder"], spelled)
|
|
@@ -258,10 +262,11 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
258
262
|
# The method a call ends on is the one being called whether or not it carries the brackets a
|
|
259
263
|
# step along the way would: "get_viewport():gui_get_focus_owner()" is the same call as without
|
|
260
264
|
# the last pair.
|
|
265
|
+
var not_called: String = _not_a_method_to_call(holder, named, str(reached["called"]))
|
|
266
|
+
if not not_called.is_empty():
|
|
267
|
+
return {"type": "error", "message": not_called}
|
|
261
268
|
if not Paths.method_of(named).is_empty():
|
|
262
269
|
named = Paths.method_of(named)
|
|
263
|
-
if not holder.has_method(named):
|
|
264
|
-
return {"type": "error", "message": "%s has no method %s" % [reached["called"], named]}
|
|
265
270
|
|
|
266
271
|
# Checked before the call rather than left to it. `callv` raises inside the game when an
|
|
267
272
|
# argument cannot be converted, and an error raised in a game somebody is playing holds it at a
|
|
@@ -360,9 +365,98 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
360
365
|
answer["errors"] = caught.raised.slice(0, ERRORS_KEPT)
|
|
361
366
|
if caught.raised.size() > ERRORS_KEPT:
|
|
362
367
|
answer["errors_not_shown"] = caught.raised.size() - ERRORS_KEPT
|
|
368
|
+
var asked: Variant = params.get("properties")
|
|
369
|
+
if _reads_the_result(asked):
|
|
370
|
+
answer.merge(_read_off_result(result, asked))
|
|
363
371
|
return answer
|
|
364
372
|
|
|
365
373
|
|
|
374
|
+
## Why [param named], the last step of a call's method, names no method of [param holder] to call, or
|
|
375
|
+
## "" when it does. Brackets on it holding arguments are refused rather than read: the arguments are
|
|
376
|
+
## taken from args alone, so a call is never made with half of them in one place and half in the
|
|
377
|
+
## other, or with the bracketed ones quietly dropped.
|
|
378
|
+
static func _not_a_method_to_call(holder: Object, named: String, called: String) -> String:
|
|
379
|
+
var method: String = named
|
|
380
|
+
if not Paths.method_of(named).is_empty():
|
|
381
|
+
var bracketed: Dictionary = Paths.arguments_of(named)
|
|
382
|
+
var in_brackets: Array = bracketed.get("args", [])
|
|
383
|
+
if bracketed.has("message") or not in_brackets.is_empty():
|
|
384
|
+
return (
|
|
385
|
+
"%s:%s: the method a call ends on takes its arguments under args, not in its brackets"
|
|
386
|
+
% [called, named]
|
|
387
|
+
)
|
|
388
|
+
method = Paths.method_of(named)
|
|
389
|
+
return "" if holder.has_method(method) else "%s has no method %s" % [called, method]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
## Whether a call's `properties` asks for its result to be read: true, or a list of names.
|
|
393
|
+
static func _reads_the_result(asked: Variant) -> bool:
|
|
394
|
+
return asked is Array or (asked is bool and asked)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
## What [param asked] reads off the object a call answered with: every variable its script declares
|
|
398
|
+
## when [param asked] is true, or the properties a list names, each a path walked from the object.
|
|
399
|
+
##
|
|
400
|
+
## In the same answer because an object handed back is otherwise only its class and an id, and
|
|
401
|
+
## reading one field of it meant finding the same object again by a path into the game's private
|
|
402
|
+
## state, which works only where the caller already knows a key. A name that is not there is listed
|
|
403
|
+
## rather than refusing the answer, since the call has run by then and its result is what was asked.
|
|
404
|
+
func _read_off_result(result: Variant, asked: Variant) -> Dictionary:
|
|
405
|
+
if result is Object and not is_instance_valid(result):
|
|
406
|
+
return {
|
|
407
|
+
"properties_note": "the call answered an object that has since been freed, so nothing was read"
|
|
408
|
+
}
|
|
409
|
+
if not result is Object:
|
|
410
|
+
return {
|
|
411
|
+
"properties_note":
|
|
412
|
+
"the call answered %s, which has no properties to read" % type_string(typeof(result))
|
|
413
|
+
}
|
|
414
|
+
var object: Object = result
|
|
415
|
+
var wanted: Array[String] = []
|
|
416
|
+
if asked is Array:
|
|
417
|
+
for name: Variant in asked:
|
|
418
|
+
wanted.append(str(name))
|
|
419
|
+
else:
|
|
420
|
+
wanted = _script_variables(object)
|
|
421
|
+
var read: Dictionary = {}
|
|
422
|
+
var missing: Array[String] = []
|
|
423
|
+
for path: String in wanted:
|
|
424
|
+
var walked: Dictionary = _walk_from(object, path)
|
|
425
|
+
if walked.has("message"):
|
|
426
|
+
missing.append(str(walked["message"]))
|
|
427
|
+
else:
|
|
428
|
+
read[path] = _values.serialize(walked["value"])
|
|
429
|
+
var answered: Dictionary = {"result_properties": read}
|
|
430
|
+
if not missing.is_empty():
|
|
431
|
+
answered["result_properties_missing"] = missing
|
|
432
|
+
return answered
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
## The variables the script attached to [param object] declares, in the order it declares them.
|
|
436
|
+
static func _script_variables(object: Object) -> Array[String]:
|
|
437
|
+
var names: Array[String] = []
|
|
438
|
+
for entry: Dictionary in object.get_property_list():
|
|
439
|
+
var usage: int = entry.get("usage", 0)
|
|
440
|
+
if usage & PROPERTY_USAGE_SCRIPT_VARIABLE != 0:
|
|
441
|
+
names.append(str(entry.get("name", "")))
|
|
442
|
+
return names
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
## What [param path] reads walked from [param object], as {"value"}, or {"message"} naming the step
|
|
446
|
+
## that is not there, by the same rules a path read from a node follows.
|
|
447
|
+
static func _walk_from(object: Object, path: String) -> Dictionary:
|
|
448
|
+
var holder: Variant = object
|
|
449
|
+
var called: String = "the result"
|
|
450
|
+
for named: String in Paths.steps_of(path):
|
|
451
|
+
if not (holder is Object or holder is Array or holder is Dictionary or Paths.packed(holder)):
|
|
452
|
+
return {"message": "%s holds no object to read %s off" % [called, named]}
|
|
453
|
+
if not Paths.can_read(holder, named):
|
|
454
|
+
return {"message": Paths.nothing_there(holder, named, called)}
|
|
455
|
+
holder = Paths.read_under(holder, named)
|
|
456
|
+
called = "%s:%s" % [called, named]
|
|
457
|
+
return {"value": holder}
|
|
458
|
+
|
|
459
|
+
|
|
366
460
|
## Why [param given] arguments are the wrong number for [param method], or "" when they are not.
|
|
367
461
|
##
|
|
368
462
|
## Asked before the call, because `callv` given the wrong number refuses inside the game and returns
|
|
@@ -25,6 +25,9 @@ const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
|
|
|
25
25
|
## everything that process starts. Kept in step with `DESKTOP_VARIABLE` in src/desktop.ts.
|
|
26
26
|
const DESKTOP_VARIABLE: String = "GDHARNESS_DESKTOP"
|
|
27
27
|
|
|
28
|
+
## Whether this run has had the pointer note in full; the game process is the run.
|
|
29
|
+
static var _pointer_explained: bool = false
|
|
30
|
+
|
|
28
31
|
var _host: Node
|
|
29
32
|
var _values: Values
|
|
30
33
|
|
|
@@ -60,13 +63,21 @@ func _init(host: Node, values: Values) -> void:
|
|
|
60
63
|
## that would take the screen. What reads the injected event is right, tooltips included, since the
|
|
61
64
|
## engine places a tooltip from the motion that reached the control.
|
|
62
65
|
static func _pointer_noted(answer: Dictionary) -> Dictionary:
|
|
63
|
-
if OS.get_environment(DESKTOP_VARIABLE)
|
|
66
|
+
if OS.get_environment(DESKTOP_VARIABLE) == "":
|
|
67
|
+
return answer
|
|
68
|
+
# The fact on every answer and the explanation on the first of the run: a session sends dozens of
|
|
69
|
+
# pointer calls to one game, and the same four hundred characters on each was most of what came
|
|
70
|
+
# back.
|
|
71
|
+
answer["pointer_on_hidden_desktop"] = true
|
|
72
|
+
if not _pointer_explained:
|
|
73
|
+
_pointer_explained = true
|
|
64
74
|
answer["pointer_note"] = (
|
|
65
75
|
"This game is on gdharness's own desktop, where Windows refuses to say where the"
|
|
66
76
|
+ " pointer is, so get_mouse_position() on the root window, and get_global_mouse_position()"
|
|
67
77
|
+ " and get_local_mouse_position() on what is drawn there, read a meaningless position"
|
|
68
78
|
+ " rather than where this put it. What reads the event itself gets this position: the"
|
|
69
|
-
+ " control under the pointer, its hover and tooltip, and the game's input handlers."
|
|
79
|
+
+ " control under the pointer, its hover and tooltip, and the game's input handlers. Said"
|
|
80
|
+
+ " once a run; pointer_on_hidden_desktop marks every answer it applies to."
|
|
70
81
|
)
|
|
71
82
|
return answer
|
|
72
83
|
|
|
@@ -61,7 +61,7 @@ static func wanted_items(menu: PopupMenu, params: Dictionary) -> Array[int]:
|
|
|
61
61
|
if words[at] == wanted:
|
|
62
62
|
return _items([order[at]])
|
|
63
63
|
for at: int in words.size():
|
|
64
|
-
if words[at].nocasecmp_to(wanted) == 0:
|
|
64
|
+
if Says.spaced(words[at]).nocasecmp_to(Says.spaced(wanted)) == 0:
|
|
65
65
|
return _items([order[at]])
|
|
66
66
|
# Separators are left out here: a heading is refused when named in full, and a pattern that
|
|
67
67
|
# matched one beside the item it heads would tie the two.
|
|
@@ -83,19 +83,21 @@ const READS_ONLY: String = ", and a path, which a wait reads every frame, only c
|
|
|
83
83
|
## at a time rather than handed to that method, which answers null for a path that goes wrong
|
|
84
84
|
## halfway along and for one that ends on null.
|
|
85
85
|
##
|
|
86
|
-
## A step written as a call, "get_viewport()", calls a method of that name
|
|
87
|
-
##
|
|
88
|
-
##
|
|
89
|
-
##
|
|
90
|
-
##
|
|
91
|
-
##
|
|
92
|
-
##
|
|
86
|
+
## A step written as a call, "get_viewport()", calls a method of that name and walks into what it
|
|
87
|
+
## returned. Some of what hangs off a node is only reachable by asking: which control holds the
|
|
88
|
+
## focus is a question for the viewport a control is in, and the viewport is a method's answer
|
|
89
|
+
## rather than a property. Spelled with the brackets so a reader sees a call where one happens, and
|
|
90
|
+
## so a method can never shadow a property of the same name. A list or a map answers the calls that
|
|
91
|
+
## read it, "board:size()" or "inbox:keys()", listed in [constant LIST_CALLS] and
|
|
92
|
+
## [constant MAP_CALLS]. An engine method that reads takes literal arguments in the brackets,
|
|
93
|
+
## "get_theme_stylebox(\"panel\"):content_margin_top", since what a control's theme gives it is only
|
|
94
|
+
## reachable by naming what is asked for; see [method arguments_of].
|
|
93
95
|
##
|
|
94
96
|
## Answers with a `message` instead when a step along the way is not there or holds something that
|
|
95
97
|
## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
|
|
96
98
|
## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle.
|
|
97
99
|
static func walk_to(node: Node, node_path: String, reaching: String) -> Dictionary:
|
|
98
|
-
var parts: PackedStringArray = reaching
|
|
100
|
+
var parts: PackedStringArray = steps_of(reaching)
|
|
99
101
|
var holder: Variant = node
|
|
100
102
|
var called: String = node_path
|
|
101
103
|
for step: int in parts.size() - 1:
|
|
@@ -187,26 +189,33 @@ static func nothing_under(holder: Variant, named: String, called: String) -> Str
|
|
|
187
189
|
## first one. An index reads the way a property does, and a negative one counts from the end the
|
|
188
190
|
## way GDScript's own does, so reading the last of something does not mean asking how many first.
|
|
189
191
|
static func can_read(holder: Variant, named: String) -> bool:
|
|
192
|
+
var method: String = method_of(named)
|
|
193
|
+
var given: Dictionary = arguments_of(named) if not method.is_empty() else {"args": []}
|
|
194
|
+
if given.has("message"):
|
|
195
|
+
return false
|
|
196
|
+
var args: Array = given["args"]
|
|
190
197
|
if holder is Array:
|
|
191
198
|
var items: Array = holder
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
if not method.is_empty():
|
|
200
|
+
return (
|
|
201
|
+
args.is_empty()
|
|
202
|
+
and method in LIST_CALLS
|
|
203
|
+
and not (items.is_empty() and method in NEEDS_AN_ELEMENT)
|
|
204
|
+
)
|
|
195
205
|
return _index_in(named, items.size()) != -1
|
|
196
206
|
if packed(holder):
|
|
197
|
-
if not
|
|
198
|
-
return
|
|
207
|
+
if not method.is_empty():
|
|
208
|
+
return args.is_empty() and method in PACKED_CALLS
|
|
199
209
|
return _index_in(named, len(holder)) != -1
|
|
200
210
|
if holder is Dictionary:
|
|
201
211
|
var map: Dictionary = holder
|
|
202
|
-
if not
|
|
203
|
-
return
|
|
212
|
+
if not method.is_empty():
|
|
213
|
+
return args.is_empty() and method in MAP_CALLS
|
|
204
214
|
return not _key_in(map, named).is_empty()
|
|
205
215
|
if holder is Object:
|
|
206
216
|
var object: Object = holder
|
|
207
|
-
var method: String = method_of(named)
|
|
208
217
|
if not method.is_empty():
|
|
209
|
-
return call_refused(object, method, "").is_empty()
|
|
218
|
+
return call_refused(object, method, "", args.size()).is_empty()
|
|
210
219
|
return _has_property(object, named)
|
|
211
220
|
return false
|
|
212
221
|
|
|
@@ -236,24 +245,103 @@ static func _key_in(map: Dictionary, named: String) -> Array:
|
|
|
236
245
|
return []
|
|
237
246
|
|
|
238
247
|
|
|
239
|
-
##
|
|
240
|
-
##
|
|
248
|
+
## [param reaching] split into its steps at each colon, except one inside a call's brackets or a
|
|
249
|
+
## quoted argument: `get_node("/root/Main"):name` is two steps, and split at every colon a quoted
|
|
250
|
+
## argument holding one became a step that names nothing.
|
|
251
|
+
static func steps_of(reaching: String) -> PackedStringArray:
|
|
252
|
+
var steps: PackedStringArray = []
|
|
253
|
+
var step: String = ""
|
|
254
|
+
var depth: int = 0
|
|
255
|
+
var quoted: bool = false
|
|
256
|
+
var escaped: bool = false
|
|
257
|
+
for character: String in reaching:
|
|
258
|
+
if quoted:
|
|
259
|
+
if escaped:
|
|
260
|
+
escaped = false
|
|
261
|
+
elif character == "\\":
|
|
262
|
+
escaped = true
|
|
263
|
+
elif character == '"':
|
|
264
|
+
quoted = false
|
|
265
|
+
elif character == '"':
|
|
266
|
+
quoted = true
|
|
267
|
+
elif character == "(":
|
|
268
|
+
depth += 1
|
|
269
|
+
elif character == ")":
|
|
270
|
+
depth = maxi(depth - 1, 0)
|
|
271
|
+
elif character == ":" and depth == 0:
|
|
272
|
+
var _kept: bool = steps.push_back(step)
|
|
273
|
+
step = ""
|
|
274
|
+
continue
|
|
275
|
+
step += character
|
|
276
|
+
var _last: bool = steps.push_back(step)
|
|
277
|
+
return steps
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
## The method a step written as a call names, "get_viewport()" being get_viewport and
|
|
281
|
+
## `get_theme_constant("separation")` get_theme_constant, or "" for a step that is a name.
|
|
241
282
|
static func method_of(named: String) -> String:
|
|
242
|
-
|
|
283
|
+
if not named.ends_with(")"):
|
|
284
|
+
return ""
|
|
285
|
+
var opened: int = named.find("(")
|
|
286
|
+
if opened < 1:
|
|
287
|
+
return ""
|
|
288
|
+
var method: String = named.substr(0, opened)
|
|
289
|
+
return method if method.is_valid_ascii_identifier() else ""
|
|
243
290
|
|
|
244
291
|
|
|
245
|
-
##
|
|
246
|
-
##
|
|
247
|
-
##
|
|
248
|
-
##
|
|
249
|
-
|
|
292
|
+
## The arguments a call step gives in its brackets, as {"args"}, or {"message"} saying why they are
|
|
293
|
+
## not ones a path can give.
|
|
294
|
+
##
|
|
295
|
+
## Literals only: words in double quotes, numbers, true, false and null, read as JSON reads them, a
|
|
296
|
+
## whole number as an int. A path names what is read, and an argument that was itself a path or an
|
|
297
|
+
## object would make a read depend on other state in a way the step does not show.
|
|
298
|
+
static func arguments_of(named: String) -> Dictionary:
|
|
299
|
+
var inside: String = named.substr(named.find("(") + 1, named.length() - named.find("(") - 2).strip_edges()
|
|
300
|
+
if inside.is_empty():
|
|
301
|
+
return {"args": []}
|
|
302
|
+
var reader: JSON = JSON.new()
|
|
303
|
+
var parsed: Variant = reader.data if reader.parse("[%s]" % inside) == OK else null
|
|
304
|
+
if not parsed is Array:
|
|
305
|
+
return {"message": _not_literals(named)}
|
|
306
|
+
var args: Array = []
|
|
307
|
+
for given: Variant in parsed:
|
|
308
|
+
if given is float:
|
|
309
|
+
var number: float = given
|
|
310
|
+
if number == floorf(number) and absf(number) < 9.0e15:
|
|
311
|
+
args.append(int(number))
|
|
312
|
+
else:
|
|
313
|
+
args.append(number)
|
|
314
|
+
elif given == null or given is String or given is bool:
|
|
315
|
+
args.append(given)
|
|
316
|
+
else:
|
|
317
|
+
return {"message": _not_literals(named)}
|
|
318
|
+
return {"args": args}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
static func _not_literals(named: String) -> String:
|
|
322
|
+
return (
|
|
323
|
+
"%s: a path gives a call literal arguments only, words in double quotes, numbers, true, false or null"
|
|
324
|
+
% named
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
## How many arguments [param method] on [param object] takes, as {"least", "most", "any_more"}, or
|
|
329
|
+
## an empty map when there is no such method. Read off the method list rather than
|
|
330
|
+
## [method Object.has_method], because a method given too few or too many raises inside the game
|
|
331
|
+
## rather than answering.
|
|
332
|
+
static func _arity(object: Object, method: String) -> Dictionary:
|
|
250
333
|
for entry: Dictionary in object.get_method_list():
|
|
251
334
|
if str(entry.get("name", "")) != method:
|
|
252
335
|
continue
|
|
253
336
|
var params: Array = entry.get("args", [])
|
|
254
337
|
var defaults: Array = entry.get("default_args", [])
|
|
255
|
-
|
|
256
|
-
|
|
338
|
+
var flags: int = entry.get("flags", 0)
|
|
339
|
+
return {
|
|
340
|
+
"least": params.size() - defaults.size(),
|
|
341
|
+
"most": params.size(),
|
|
342
|
+
"any_more": flags & METHOD_FLAG_VARARG != 0,
|
|
343
|
+
}
|
|
344
|
+
return {}
|
|
257
345
|
|
|
258
346
|
|
|
259
347
|
## Why a path may not call [param method] on [param object], said about [param called_as], or ""
|
|
@@ -265,16 +353,36 @@ static func _arguments_required(object: Object, method: String) -> int:
|
|
|
265
353
|
## doing something and answering nothing, and one left untyped is trusted. Without this a find
|
|
266
354
|
## with property "queue_free()" freed every node it matched, and a wait on "advance_day()" called
|
|
267
355
|
## it once a frame and then reported the state it had driven the game to.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
356
|
+
##
|
|
357
|
+
## Given [param given] arguments, a call is the engine's and marked as reading, or it is refused. A
|
|
358
|
+
## game's method taking arguments is trusted on nothing at all: one answering a value can as well
|
|
359
|
+
## change what it was asked about, and runtime_invoke call is how one is called on purpose.
|
|
360
|
+
static func call_refused(object: Object, method: String, called_as: String, given: int = 0) -> String:
|
|
361
|
+
var arity: Dictionary = _arity(object, method)
|
|
362
|
+
if arity.is_empty():
|
|
271
363
|
return "%s has no method %s" % [called_as, method]
|
|
272
|
-
|
|
364
|
+
var least: int = arity["least"]
|
|
365
|
+
var most: int = arity["most"]
|
|
366
|
+
var any_more: bool = arity["any_more"]
|
|
367
|
+
var scripted: Dictionary = _script_method(object, method)
|
|
368
|
+
if not scripted.is_empty() and (given > 0 or least > 0):
|
|
369
|
+
var why: String = (
|
|
370
|
+
"since nothing marks one as only reading%s; runtime_invoke call takes them" % READS_ONLY
|
|
371
|
+
)
|
|
273
372
|
return (
|
|
274
|
-
"%s.%s
|
|
275
|
-
% [called_as, method,
|
|
373
|
+
"%s.%s is the game's own and a path gives a game's method no arguments, %s"
|
|
374
|
+
% [called_as, method, why]
|
|
375
|
+
)
|
|
376
|
+
if given < least or (given > most and not any_more):
|
|
377
|
+
var takes: String = "%d" % least
|
|
378
|
+
if any_more:
|
|
379
|
+
takes = "at least %d" % least
|
|
380
|
+
elif most != least:
|
|
381
|
+
takes = "%d to %d" % [least, most]
|
|
382
|
+
return (
|
|
383
|
+
'%s.%s takes %s argument%s and the path gives it %d, in the brackets as in %s("name")'
|
|
384
|
+
% [called_as, method, takes, "" if takes == "1" else "s", given, method]
|
|
276
385
|
)
|
|
277
|
-
var scripted: Dictionary = _script_method(object, method)
|
|
278
386
|
if not scripted.is_empty():
|
|
279
387
|
var returned: Dictionary = scripted.get("return", {})
|
|
280
388
|
var usage: int = returned.get("usage", 0)
|
|
@@ -350,7 +458,8 @@ static func read_under(holder: Variant, named: String) -> Variant:
|
|
|
350
458
|
var object: Object = holder
|
|
351
459
|
var method: String = method_of(named)
|
|
352
460
|
if not method.is_empty():
|
|
353
|
-
|
|
461
|
+
var args: Array = arguments_of(named)["args"]
|
|
462
|
+
return object.callv(method, args)
|
|
354
463
|
return object.get(named)
|
|
355
464
|
|
|
356
465
|
|
|
@@ -374,20 +483,25 @@ static func _index_in(named: String, size: int) -> int:
|
|
|
374
483
|
## the whole path, because the step is the typo.
|
|
375
484
|
static func nothing_there(holder: Variant, named: String, called_as: String) -> String:
|
|
376
485
|
var container_call: String = method_of(named)
|
|
486
|
+
var given: Dictionary = arguments_of(named) if not container_call.is_empty() else {"args": []}
|
|
487
|
+
if given.has("message"):
|
|
488
|
+
return "%s:%s" % [called_as, given["message"]]
|
|
489
|
+
var given_args: Array = given["args"]
|
|
490
|
+
var given_count: int = given_args.size()
|
|
377
491
|
if holder is Array:
|
|
378
492
|
var items: Array = holder
|
|
379
493
|
if container_call in NEEDS_AN_ELEMENT and items.is_empty():
|
|
380
494
|
return "%s is an empty list, so it has no %s" % [called_as, named]
|
|
381
495
|
if not container_call.is_empty():
|
|
382
|
-
return container_calls_refused(called_as, "a list", container_call)
|
|
496
|
+
return container_calls_refused(called_as, "a list", container_call, given_count)
|
|
383
497
|
return "%s is a list of %d, so there is no %s in it" % [called_as, items.size(), named]
|
|
384
498
|
if packed(holder):
|
|
385
499
|
if not container_call.is_empty():
|
|
386
|
-
return container_calls_refused(called_as, "a packed list", container_call)
|
|
500
|
+
return container_calls_refused(called_as, "a packed list", container_call, given_count)
|
|
387
501
|
return "%s is a packed list of %d, so there is no %s in it" % [called_as, len(holder), named]
|
|
388
502
|
if holder is Dictionary:
|
|
389
503
|
if not container_call.is_empty():
|
|
390
|
-
return container_calls_refused(called_as, "a map", container_call)
|
|
504
|
+
return container_calls_refused(called_as, "a map", container_call, given_count)
|
|
391
505
|
var map: Dictionary = holder
|
|
392
506
|
var keys: Array = map.keys()
|
|
393
507
|
# A key reading as the step and still not reached is one of a type a step cannot spell, and
|
|
@@ -410,7 +524,7 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
410
524
|
var object: Object = holder
|
|
411
525
|
var method: String = method_of(named)
|
|
412
526
|
if not method.is_empty():
|
|
413
|
-
return call_refused(object, method, called_as)
|
|
527
|
+
return call_refused(object, method, called_as, given_count)
|
|
414
528
|
# A method spelled as a property is the likeliest thing behind a name the object has no property
|
|
415
529
|
# for, and the caller is one pair of brackets away from what they meant.
|
|
416
530
|
if object.has_method(named):
|
|
@@ -422,13 +536,18 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
422
536
|
|
|
423
537
|
|
|
424
538
|
## Why [param method] is not a call a path makes on [param sort] ("a list", "a packed list" or "a
|
|
425
|
-
## map"), naming the calls it does make
|
|
426
|
-
|
|
539
|
+
## map"), naming the calls it does make, or that it makes this one with no arguments when
|
|
540
|
+
## [param given] were.
|
|
541
|
+
static func container_calls_refused(
|
|
542
|
+
called_as: String, sort: String, method: String, given: int = 0
|
|
543
|
+
) -> String:
|
|
427
544
|
var calls: Array[String] = MAP_CALLS
|
|
428
545
|
if sort == "a list":
|
|
429
546
|
calls = LIST_CALLS
|
|
430
547
|
elif sort == "a packed list":
|
|
431
548
|
calls = PACKED_CALLS
|
|
549
|
+
if given > 0 and method in calls:
|
|
550
|
+
return "%s is %s, and a path calls %s() on one with no arguments" % [called_as, sort, method]
|
|
432
551
|
var spelled: Array[String] = []
|
|
433
552
|
for each: String in calls:
|
|
434
553
|
spelled.append(each + "()")
|
|
@@ -16,6 +16,9 @@ const SPLITS_KEPT: int = 8
|
|
|
16
16
|
## Each `says` split into alternatives, by what was written; see [method alternatives].
|
|
17
17
|
static var _splits: Dictionary[String, Array] = {}
|
|
18
18
|
|
|
19
|
+
## Every space separator Unicode has besides the plain space, built once; see [method spaced].
|
|
20
|
+
static var _spaces: RegEx = null
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
## Whether [param node]'s own words are what [param wanted] asks for.
|
|
21
24
|
static func says(node: Node, wanted: String) -> bool:
|
|
@@ -35,12 +38,33 @@ static func says(node: Node, wanted: String) -> bool:
|
|
|
35
38
|
## first, and read as one glob with the bars in it, it waited out its whole timeout for words no
|
|
36
39
|
## screen says.
|
|
37
40
|
static func matches(said: String, wanted: String) -> bool:
|
|
41
|
+
var shown: String = spaced(said)
|
|
38
42
|
for words: String in alternatives(wanted):
|
|
39
|
-
if
|
|
43
|
+
if shown.containsn(words) if is_plain(words) else shown.matchn(words):
|
|
40
44
|
return true
|
|
41
45
|
return false
|
|
42
46
|
|
|
43
47
|
|
|
48
|
+
## [param text] with every Unicode space separator written as a plain space, which is what a caller
|
|
49
|
+
## types for any of them. A game keeping a name on one line writes it with a non-breaking space, and
|
|
50
|
+
## "Deskanem Bonituk" typed with a plain one found nothing while the screen showed exactly those
|
|
51
|
+
## words. One character for one, so a place found in the result is the same place in [param text].
|
|
52
|
+
static func spaced(text: String) -> String:
|
|
53
|
+
if _spaces == null:
|
|
54
|
+
var separators: String = (
|
|
55
|
+
String.chr(0x00A0)
|
|
56
|
+
+ String.chr(0x1680)
|
|
57
|
+
+ String.chr(0x2000)
|
|
58
|
+
+ "-"
|
|
59
|
+
+ String.chr(0x200A)
|
|
60
|
+
+ String.chr(0x202F)
|
|
61
|
+
+ String.chr(0x205F)
|
|
62
|
+
+ String.chr(0x3000)
|
|
63
|
+
)
|
|
64
|
+
_spaces = RegEx.create_from_string("[%s]" % separators)
|
|
65
|
+
return _spaces.sub(text, " ", true)
|
|
66
|
+
|
|
67
|
+
|
|
44
68
|
## The alternatives [param wanted] names, each as words on a screen: split at every `|` not written
|
|
45
69
|
## as `\|`, which stays a bar in the words. An empty alternative, a stray bar at either end, is
|
|
46
70
|
## dropped: it names no words, and a find that came back empty widened it to `**` and suggested a
|
|
@@ -110,4 +134,4 @@ static func widened(wanted: String) -> String:
|
|
|
110
134
|
## which reads as a control that is not on the screen. Nothing on a screen says a backslash and
|
|
111
135
|
## an n, so the two characters mean the break to everybody who writes them.
|
|
112
136
|
static func as_said(wanted: String) -> String:
|
|
113
|
-
return wanted.replace("\\n", "\n")
|
|
137
|
+
return spaced(wanted.replace("\\n", "\n"))
|