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.
@@ -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 = JSON.parse_string(file.get_as_text())
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 = JSON.parse_string(json_string)
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 = JSON.parse_string(text)
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 = JSON.parse_string(text)
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 = JSON.parse_string(text)
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": "Node path required"}
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
- return {
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 = JSON.parse_string(text)
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 = JSON.parse_string(text)
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 = JSON.parse_string(text)
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