gdharness 1.1.9 → 1.1.11
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 +928 -1251
- package/build/godot/addons/gdharness_editor/bridge_client.gd +2 -2
- package/build/godot/addons/gdharness_editor/reading.gd +13 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +1 -1
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +1 -1
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +1 -1
- package/build/godot/addons/gdharness_runtime/reading.gd +13 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +21 -2
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +6 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +113 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +1 -1
- package/build/godot/operations/import_pipeline.gd +1 -1
- package/build/godot/operations/reading.gd +13 -0
- package/build/godot/operations/serialisation.gd +1 -1
- package/build/index.js +94 -157
- package/package.json +1 -1
|
@@ -243,7 +243,7 @@ func announced_url() -> String:
|
|
|
243
243
|
var file: FileAccess = FileAccess.open(ANNOUNCEMENT, FileAccess.READ)
|
|
244
244
|
if file == null:
|
|
245
245
|
return ""
|
|
246
|
-
var said: Variant =
|
|
246
|
+
var said: Variant = Read.json_or_null(file.get_as_text())
|
|
247
247
|
file.close()
|
|
248
248
|
if not said is Dictionary:
|
|
249
249
|
return ""
|
|
@@ -481,7 +481,7 @@ func _on_reconnect_timer() -> void:
|
|
|
481
481
|
|
|
482
482
|
|
|
483
483
|
func _handle_message(json_string: String) -> void:
|
|
484
|
-
var parsed: Variant =
|
|
484
|
+
var parsed: Variant = Read.json_or_null(json_string)
|
|
485
485
|
if not parsed is Dictionary:
|
|
486
486
|
push_error("[gdharness] The server sent something that is not a message: %s" % json_string)
|
|
487
487
|
return
|
|
@@ -69,3 +69,16 @@ static func as_bool(value: Variant, fallback: bool = false) -> bool:
|
|
|
69
69
|
var text: String = value
|
|
70
70
|
return not text.is_empty()
|
|
71
71
|
return fallback
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## [param text] read as JSON, or null when it does not parse, without printing anything.
|
|
75
|
+
##
|
|
76
|
+
## [method JSON.parse_string] prints an engine error for text that does not parse, and that error
|
|
77
|
+
## lands in the log of whatever is running it. A caller sending the word "five" where a number was
|
|
78
|
+
## wanted was refused properly, and the game's own log then held an error it never made, so its
|
|
79
|
+
## verdict read `clean: false`. Text that is not JSON is an answer here, not a fault.
|
|
80
|
+
static func json_or_null(text: String) -> Variant:
|
|
81
|
+
var json: JSON = JSON.new()
|
|
82
|
+
if json.parse(text) != OK:
|
|
83
|
+
return null
|
|
84
|
+
return json.data
|
|
@@ -256,7 +256,7 @@ func fitted(value: Variant, type: int) -> Variant:
|
|
|
256
256
|
|
|
257
257
|
if rebuilt is String and type != TYPE_STRING:
|
|
258
258
|
var text: String = rebuilt
|
|
259
|
-
var parsed: Variant =
|
|
259
|
+
var parsed: Variant = Read.json_or_null(text)
|
|
260
260
|
# A string that does not read as the type wanted is handed back as the string it is, so
|
|
261
261
|
# whoever asked can refuse it. type_convert answers 0 for "not an index" and true for any
|
|
262
262
|
# text at all, and a caller who sent a word got back a number they never sent: get_child
|
|
@@ -86,7 +86,7 @@ func _parse_json_maybe(value: Variant) -> Variant:
|
|
|
86
86
|
if typeof(value) != TYPE_STRING:
|
|
87
87
|
return value
|
|
88
88
|
var text: String = value
|
|
89
|
-
var parsed: Variant =
|
|
89
|
+
var parsed: Variant = Read.json_or_null(text)
|
|
90
90
|
if parsed == null and text != "null":
|
|
91
91
|
return value
|
|
92
92
|
return parsed
|
|
@@ -127,7 +127,7 @@ func _parse_properties_arg(raw_properties: Variant) -> Dictionary:
|
|
|
127
127
|
var text: String = str(raw_properties)
|
|
128
128
|
if text.strip_edges().is_empty():
|
|
129
129
|
return {}
|
|
130
|
-
var parsed: Variant =
|
|
130
|
+
var parsed: Variant = Read.json_or_null(text)
|
|
131
131
|
if typeof(parsed) == TYPE_DICTIONARY:
|
|
132
132
|
return parsed
|
|
133
133
|
return {}
|
|
@@ -69,3 +69,16 @@ static func as_bool(value: Variant, fallback: bool = false) -> bool:
|
|
|
69
69
|
var text: String = value
|
|
70
70
|
return not text.is_empty()
|
|
71
71
|
return fallback
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## [param text] read as JSON, or null when it does not parse, without printing anything.
|
|
75
|
+
##
|
|
76
|
+
## [method JSON.parse_string] prints an engine error for text that does not parse, and that error
|
|
77
|
+
## lands in the log of whatever is running it. A caller sending the word "five" where a number was
|
|
78
|
+
## wanted was refused properly, and the game's own log then held an error it never made, so its
|
|
79
|
+
## verdict read `clean: false`. Text that is not JSON is an answer here, not a fault.
|
|
80
|
+
static func json_or_null(text: String) -> Variant:
|
|
81
|
+
var json: JSON = JSON.new()
|
|
82
|
+
if json.parse(text) != OK:
|
|
83
|
+
return null
|
|
84
|
+
return json.data
|
|
@@ -10,6 +10,7 @@ const Values = preload("runtime_values.gd")
|
|
|
10
10
|
## reports are the same place by construction rather than by agreement.
|
|
11
11
|
const Queries = preload("runtime_queries.gd")
|
|
12
12
|
const Read = preload("reading.gd")
|
|
13
|
+
const Targets = preload("runtime_targets.gd")
|
|
13
14
|
const Words = preload("runtime_words.gd")
|
|
14
15
|
|
|
15
16
|
## The distance from a capital letter to its small one in Unicode. A keycode holds the capital.
|
|
@@ -484,10 +485,25 @@ static func _scroll_into_view(control: Control) -> bool:
|
|
|
484
485
|
## A control out of sight inside a ScrollContainer is scrolled to first; the answer says so under
|
|
485
486
|
## `scrolled_into_view`, because the view having moved is a thing that happened to the screen and
|
|
486
487
|
## the caller is the only one who can tell whether that matters.
|
|
488
|
+
##
|
|
489
|
+
## `says` names the control by the words on it instead, under `path` when that is given as well,
|
|
490
|
+
## found and pressed in the same frame. A find and then a click by path was two round trips over a
|
|
491
|
+
## generated path copied whole, and a panel that rebuilt itself between them freed the button, so
|
|
492
|
+
## the click landed on nothing.
|
|
487
493
|
func click(params: Dictionary) -> Dictionary:
|
|
488
494
|
var node_path: String = str(params.get("path", ""))
|
|
495
|
+
var wanted: String = str(params.get("says", ""))
|
|
496
|
+
var found: Variant = null
|
|
497
|
+
if not wanted.is_empty():
|
|
498
|
+
var picked: Dictionary = Targets.control_saying(
|
|
499
|
+
_host.get_tree().root, "/root" if node_path.is_empty() else node_path, wanted, params.get("index")
|
|
500
|
+
)
|
|
501
|
+
if picked.has("message"):
|
|
502
|
+
return picked
|
|
503
|
+
node_path = picked["path"]
|
|
504
|
+
found = picked["found"]
|
|
489
505
|
if node_path.is_empty():
|
|
490
|
-
return {"type": "error", "message": "
|
|
506
|
+
return {"type": "error", "message": "click needs a path, or says to find the control by its words"}
|
|
491
507
|
|
|
492
508
|
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
493
509
|
if standing.has("message"):
|
|
@@ -582,7 +598,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
582
598
|
if is_instance_valid(control):
|
|
583
599
|
afterwards = "in_tree" if control.is_inside_tree() else "removed"
|
|
584
600
|
|
|
585
|
-
|
|
601
|
+
var answer: Dictionary = {
|
|
586
602
|
"type": "clicked",
|
|
587
603
|
"path": node_path,
|
|
588
604
|
"position": _values.serialize(position),
|
|
@@ -593,6 +609,9 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
593
609
|
"control_afterwards": afterwards,
|
|
594
610
|
"scrolled_into_view": scrolled,
|
|
595
611
|
}
|
|
612
|
+
if found != null:
|
|
613
|
+
answer["found"] = found
|
|
614
|
+
return answer
|
|
596
615
|
|
|
597
616
|
|
|
598
617
|
## What to add to a refusal about a point outside the viewport, when the reason is that nobody
|
|
@@ -259,6 +259,12 @@ static func _says(said: String, wanted: String) -> bool:
|
|
|
259
259
|
return said.containsn(words) if _is_literal(words) else said.matchn(words)
|
|
260
260
|
|
|
261
261
|
|
|
262
|
+
## Whether [param node]'s own words are what [param wanted] asks for, by the rules a find uses, so a
|
|
263
|
+
## click that names its control by its words finds the one a find would have answered with.
|
|
264
|
+
static func says(node: Node, wanted: String) -> bool:
|
|
265
|
+
return _says(Words.said_by(node), wanted)
|
|
266
|
+
|
|
267
|
+
|
|
262
268
|
## [param wanted], a glob, open at both ends so it matches its words anywhere in a text; "" when
|
|
263
269
|
## it is not a glob, or already open at both ends, and so has nothing to suggest.
|
|
264
270
|
static func _widened(wanted: String) -> String:
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Which control a click means when it names the control by the words on it rather than by path.
|
|
4
|
+
|
|
5
|
+
const Queries = preload("runtime_queries.gd")
|
|
6
|
+
const Read = preload("reading.gd")
|
|
7
|
+
const Values = preload("runtime_values.gd")
|
|
8
|
+
const Words = preload("runtime_words.gd")
|
|
9
|
+
|
|
10
|
+
## How many of the controls saying the same words a refusal names, with the index for each.
|
|
11
|
+
const LISTED_CANDIDATES: int = 10
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## The control a click by [param wanted] words presses, found under [param root_path] in the tree
|
|
15
|
+
## [param root] heads, or the refusal saying why there is none or which of several:
|
|
16
|
+
## {"path", "found"} or {"type", "message"}.
|
|
17
|
+
##
|
|
18
|
+
## Matched the way a find matches `says`, against a control's own words, and only among controls
|
|
19
|
+
## the player can see. A match inside a button presses the button, because the label is what says
|
|
20
|
+
## the words and the button is what takes the click: aimed at the label, the pointer lands on the
|
|
21
|
+
## button and the answer would call the click a miss. Several matches are refused rather than
|
|
22
|
+
## guessed, each named with the [param which] that picks it, counting from 0.
|
|
23
|
+
static func control_saying(root: Node, root_path: String, wanted: String, which: Variant) -> Dictionary:
|
|
24
|
+
var reached: Dictionary = Values.node_at(root, root_path)
|
|
25
|
+
if reached.has("message"):
|
|
26
|
+
return reached
|
|
27
|
+
var targets: Array[Control] = []
|
|
28
|
+
# The words of the control that matched, which a button pressed on a label's behalf has none of.
|
|
29
|
+
var said: Array[String] = []
|
|
30
|
+
var hidden: int = 0
|
|
31
|
+
var pending: Array[Node] = [reached["node"]]
|
|
32
|
+
while not pending.is_empty():
|
|
33
|
+
var node: Node = pending.pop_front()
|
|
34
|
+
if node is Control and Queries.says(node, wanted):
|
|
35
|
+
if not Queries.shown(node):
|
|
36
|
+
hidden += 1
|
|
37
|
+
else:
|
|
38
|
+
var matched: Control = node
|
|
39
|
+
var target: Control = _pressed_through(matched)
|
|
40
|
+
if not targets.has(target):
|
|
41
|
+
targets.append(target)
|
|
42
|
+
said.append(Words.said_by(node))
|
|
43
|
+
var children: Array[Node] = node.get_children(true)
|
|
44
|
+
for index: int in range(children.size() - 1, -1, -1):
|
|
45
|
+
pending.push_front(children[index])
|
|
46
|
+
|
|
47
|
+
var under: String = "" if root_path == "/root" else " under %s" % root_path
|
|
48
|
+
if targets.is_empty():
|
|
49
|
+
return {
|
|
50
|
+
"type": "error",
|
|
51
|
+
"message":
|
|
52
|
+
(
|
|
53
|
+
'no control on screen%s says "%s", so nothing was clicked%s'
|
|
54
|
+
% [under, wanted, _hidden_note(hidden)]
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
if which == null and targets.size() > 1:
|
|
58
|
+
return {
|
|
59
|
+
"type": "error",
|
|
60
|
+
"message":
|
|
61
|
+
(
|
|
62
|
+
'%d controls on screen%s say "%s", so which to click is not clear; pass index, counting from 0: %s'
|
|
63
|
+
% [targets.size(), under, wanted, _candidates(targets, said)]
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
var index: int = 0 if which == null else Read.as_int(which, -1)
|
|
67
|
+
if index < 0 or index >= targets.size():
|
|
68
|
+
var plural: String = "" if targets.size() == 1 else "s"
|
|
69
|
+
return {
|
|
70
|
+
"type": "error",
|
|
71
|
+
"message":
|
|
72
|
+
(
|
|
73
|
+
'index %s is not one of the %d control%s on screen%s saying "%s": %s'
|
|
74
|
+
% [str(which), targets.size(), plural, under, wanted, _candidates(targets, said)]
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
"path": str(targets[index].get_path()),
|
|
79
|
+
"found": {"says": wanted, "index": index, "of": targets.size()},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## What to add to a refusal that found nothing on screen, when words matched controls out of sight.
|
|
84
|
+
static func _hidden_note(hidden: int) -> String:
|
|
85
|
+
if hidden == 0:
|
|
86
|
+
return ""
|
|
87
|
+
return "; %d hidden control%s" % [hidden, " says it" if hidden == 1 else "s say it"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
## [param control], or the button holding it when it is not one itself, which is what a click on
|
|
91
|
+
## its words means.
|
|
92
|
+
static func _pressed_through(control: Control) -> Control:
|
|
93
|
+
var walk: Node = control
|
|
94
|
+
while walk != null:
|
|
95
|
+
if walk is BaseButton:
|
|
96
|
+
var pressed: BaseButton = walk
|
|
97
|
+
return pressed
|
|
98
|
+
walk = walk.get_parent()
|
|
99
|
+
return control
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## The controls a click by words could mean, each with its index, its path and the words that
|
|
103
|
+
## matched, [param said] holding those in the same order.
|
|
104
|
+
static func _candidates(targets: Array[Control], said: Array[String]) -> String:
|
|
105
|
+
var named: Array[String] = []
|
|
106
|
+
for index: int in mini(targets.size(), LISTED_CANDIDATES):
|
|
107
|
+
var words: String = said[index].replace("\n", " ")
|
|
108
|
+
if words.length() > 60:
|
|
109
|
+
words = words.substr(0, 57) + "..."
|
|
110
|
+
named.append('%d %s ("%s")' % [index, targets[index].get_path(), words])
|
|
111
|
+
if targets.size() > LISTED_CANDIDATES:
|
|
112
|
+
named.append("and %d more" % (targets.size() - LISTED_CANDIDATES))
|
|
113
|
+
return "; ".join(named)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://be1ydcqg2ag4t
|
|
@@ -256,7 +256,7 @@ func fitted(value: Variant, type: int) -> Variant:
|
|
|
256
256
|
|
|
257
257
|
if rebuilt is String and type != TYPE_STRING:
|
|
258
258
|
var text: String = rebuilt
|
|
259
|
-
var parsed: Variant =
|
|
259
|
+
var parsed: Variant = Read.json_or_null(text)
|
|
260
260
|
# A string that does not read as the type wanted is handed back as the string it is, so
|
|
261
261
|
# whoever asked can refuse it. type_convert answers 0 for "not an index" and true for any
|
|
262
262
|
# text at all, and a caller who sent a word got back a number they never sent: get_child
|
|
@@ -645,7 +645,7 @@ static func _images_of(scene_path: String) -> Array[String]:
|
|
|
645
645
|
text = bytes.slice(20, mini(20 + length, bytes.size())).get_string_from_utf8()
|
|
646
646
|
if text.is_empty():
|
|
647
647
|
return images
|
|
648
|
-
var parsed: Variant =
|
|
648
|
+
var parsed: Variant = Read.json_or_null(text)
|
|
649
649
|
if not parsed is Dictionary:
|
|
650
650
|
return images
|
|
651
651
|
var document: Dictionary = parsed
|
|
@@ -69,3 +69,16 @@ static func as_bool(value: Variant, fallback: bool = false) -> bool:
|
|
|
69
69
|
var text: String = value
|
|
70
70
|
return not text.is_empty()
|
|
71
71
|
return fallback
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## [param text] read as JSON, or null when it does not parse, without printing anything.
|
|
75
|
+
##
|
|
76
|
+
## [method JSON.parse_string] prints an engine error for text that does not parse, and that error
|
|
77
|
+
## lands in the log of whatever is running it. A caller sending the word "five" where a number was
|
|
78
|
+
## wanted was refused properly, and the game's own log then held an error it never made, so its
|
|
79
|
+
## verdict read `clean: false`. Text that is not JSON is an answer here, not a fault.
|
|
80
|
+
static func json_or_null(text: String) -> Variant:
|
|
81
|
+
var json: JSON = JSON.new()
|
|
82
|
+
if json.parse(text) != OK:
|
|
83
|
+
return null
|
|
84
|
+
return json.data
|
|
@@ -256,7 +256,7 @@ func fitted(value: Variant, type: int) -> Variant:
|
|
|
256
256
|
|
|
257
257
|
if rebuilt is String and type != TYPE_STRING:
|
|
258
258
|
var text: String = rebuilt
|
|
259
|
-
var parsed: Variant =
|
|
259
|
+
var parsed: Variant = Read.json_or_null(text)
|
|
260
260
|
# A string that does not read as the type wanted is handed back as the string it is, so
|
|
261
261
|
# whoever asked can refuse it. type_convert answers 0 for "not an index" and true for any
|
|
262
262
|
# text at all, and a caller who sent a word got back a number they never sent: get_child
|