gdharness 1.3.41 → 1.3.43

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.
Files changed (107) hide show
  1. package/build/cli.js +131 -47
  2. package/build/godot/addons/auto_reload/auto_reload.gd +18 -19
  3. package/build/godot/addons/gdharness_editor/animation_reads.gd +84 -35
  4. package/build/godot/addons/gdharness_editor/bridge_client.gd +53 -58
  5. package/build/godot/addons/gdharness_editor/declared_values.gd +68 -32
  6. package/build/godot/addons/gdharness_editor/file_reads.gd +114 -39
  7. package/build/godot/addons/gdharness_editor/file_walk.gd +12 -5
  8. package/build/godot/addons/gdharness_editor/headless_check.gd +94 -41
  9. package/build/godot/addons/gdharness_editor/plugin.gd +2 -3
  10. package/build/godot/addons/gdharness_editor/property_paths.gd +22 -12
  11. package/build/godot/addons/gdharness_editor/property_values.gd +143 -78
  12. package/build/godot/addons/gdharness_editor/reading.gd +37 -12
  13. package/build/godot/addons/gdharness_editor/same_values.gd +4 -4
  14. package/build/godot/addons/gdharness_editor/scene_file.gd +136 -79
  15. package/build/godot/addons/gdharness_editor/scene_uids.gd +12 -6
  16. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -51
  17. package/build/godot/addons/gdharness_editor/setting_reads.gd +7 -6
  18. package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -6
  19. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +41 -28
  20. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +84 -50
  21. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +70 -40
  22. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +34 -30
  23. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +24 -22
  24. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +49 -29
  25. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +26 -21
  26. package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +40 -40
  27. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +181 -107
  28. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -9
  29. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +75 -43
  30. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +35 -20
  31. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +28 -13
  32. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +66 -0
  33. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +171 -62
  35. package/build/godot/addons/gdharness_editor/tools/script_paths.gd +20 -8
  36. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +11 -8
  37. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +47 -34
  38. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +77 -50
  39. package/build/godot/addons/gdharness_editor/value_shapes.gd +5 -6
  40. package/build/godot/addons/gdharness_runtime/call_errors.gd +1 -2
  41. package/build/godot/addons/gdharness_runtime/declared_values.gd +68 -32
  42. package/build/godot/addons/gdharness_runtime/error_report.gd +1 -2
  43. package/build/godot/addons/gdharness_runtime/reading.gd +37 -12
  44. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +84 -63
  45. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +153 -148
  46. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +64 -42
  47. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +184 -122
  48. package/build/godot/addons/gdharness_runtime/runtime_frames.gd +2 -2
  49. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +24 -20
  50. package/build/godot/addons/gdharness_runtime/runtime_input.gd +134 -92
  51. package/build/godot/addons/gdharness_runtime/runtime_items.gd +29 -13
  52. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +0 -1
  53. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +38 -20
  54. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +17 -13
  55. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +84 -46
  56. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +117 -79
  57. package/build/godot/addons/gdharness_runtime/runtime_says.gd +14 -17
  58. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +23 -16
  59. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +117 -66
  60. package/build/godot/addons/gdharness_runtime/runtime_touch.gd +211 -0
  61. package/build/godot/addons/gdharness_runtime/runtime_touch.gd.uid +1 -0
  62. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +44 -27
  63. package/build/godot/addons/gdharness_runtime/runtime_values.gd +108 -73
  64. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +111 -84
  65. package/build/godot/addons/gdharness_runtime/runtime_words.gd +10 -7
  66. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -51
  67. package/build/godot/addons/gdharness_runtime/value_shapes.gd +5 -6
  68. package/build/godot/addons/gdharness_tests/bench.gd +4 -5
  69. package/build/godot/addons/gdharness_tests/event.gd +10 -5
  70. package/build/godot/addons/gdharness_tests/share.gd +38 -18
  71. package/build/godot/addons/gdharness_tests/suites.gd +3 -3
  72. package/build/godot/addons/gdharness_tests/wait.gd +3 -2
  73. package/build/godot/loader/loader.gd +9 -9
  74. package/build/godot/operations/animation_reads.gd +84 -35
  75. package/build/godot/operations/audio_buses.gd +48 -19
  76. package/build/godot/operations/class_cache.gd +16 -11
  77. package/build/godot/operations/classdb_queries.gd +89 -44
  78. package/build/godot/operations/declared_values.gd +68 -32
  79. package/build/godot/operations/dependencies.gd +139 -93
  80. package/build/godot/operations/editor_tools.gd +9 -10
  81. package/build/godot/operations/export_presets.gd +100 -54
  82. package/build/godot/operations/file_reads.gd +114 -39
  83. package/build/godot/operations/file_walk.gd +12 -5
  84. package/build/godot/operations/gdscript_analysis.gd +105 -66
  85. package/build/godot/operations/gdscript_authoring.gd +138 -39
  86. package/build/godot/operations/godot_operations.gd +17 -11
  87. package/build/godot/operations/import_pipeline.gd +110 -59
  88. package/build/godot/operations/input_actions.gd +63 -39
  89. package/build/godot/operations/patterns.gd +3 -1
  90. package/build/godot/operations/plugins.gd +36 -16
  91. package/build/godot/operations/project_config.gd +144 -27
  92. package/build/godot/operations/project_diagnostics.gd +24 -11
  93. package/build/godot/operations/project_file.gd +16 -16
  94. package/build/godot/operations/project_validation.gd +71 -44
  95. package/build/godot/operations/property_paths.gd +22 -12
  96. package/build/godot/operations/property_values.gd +143 -78
  97. package/build/godot/operations/reading.gd +37 -12
  98. package/build/godot/operations/rename_checks.gd +2 -2
  99. package/build/godot/operations/resource_files.gd +11 -9
  100. package/build/godot/operations/same_values.gd +4 -4
  101. package/build/godot/operations/scene_connections.gd +27 -13
  102. package/build/godot/operations/script_check.gd +52 -49
  103. package/build/godot/operations/serialisation.gd +75 -51
  104. package/build/godot/operations/setting_reads.gd +7 -6
  105. package/build/godot/operations/value_shapes.gd +5 -6
  106. package/build/godot/tests/listed.gd +321 -268
  107. package/package.json +1 -1
@@ -1,5 +1,4 @@
1
1
  extends RefCounted
2
-
3
2
  ## Which control a click means when it names the control by the words on it rather than by path.
4
3
 
5
4
  const GdhItems = preload("runtime_items.gd")
@@ -9,12 +8,10 @@ const GdhSays = preload("runtime_says.gd")
9
8
  const GdhScreen = preload("runtime_screen.gd")
10
9
  const GdhValues = preload("runtime_values.gd")
11
10
  const GdhWords = preload("runtime_words.gd")
12
-
13
11
  ## How many of the controls saying the same words a refusal names, with the index for each.
14
12
  const LISTED_CANDIDATES: int = 10
15
13
  ## How many hidden controls saying the words a refusal names.
16
14
  const NAMED_HIDDEN: int = 3
17
-
18
15
  ## Node.ProcessMode as its names, for a refusal to say which one is in force.
19
16
  const PROCESS_MODES: Dictionary[int, String] = {
20
17
  Node.PROCESS_MODE_INHERIT: "INHERIT",
@@ -23,11 +20,9 @@ const PROCESS_MODES: Dictionary[int, String] = {
23
20
  Node.PROCESS_MODE_ALWAYS: "ALWAYS",
24
21
  Node.PROCESS_MODE_DISABLED: "DISABLED",
25
22
  }
26
-
27
23
  ## What a game started without a window gets whatever the project settings say, and the usual
28
24
  ## reason a control is out of reach. Only worth telling somebody when it is what they have.
29
25
  const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
30
-
31
26
  ## What a match's rank is made of, one bit each, the highest deciding first.
32
27
  const TAKES_POINTER: int = 4
33
28
  const ENABLED_BUTTON: int = 2
@@ -55,7 +50,9 @@ const WHOLE: int = 1
55
50
  ## A control with something drawn over its centre that a pointer there would reach instead is not
56
51
  ## on screen for a player, and a click at it lands on the thing on top. The screen a game shows
57
52
  ## over its hall covers the hall's drawers without hiding them, so their words matched as well.
58
- static func control_saying(root: Node, root_path: String, wanted: String, which: Variant) -> Dictionary:
53
+ static func control_saying(
54
+ root: Node, root_path: String, wanted: String, which: Variant
55
+ ) -> Dictionary:
59
56
  var reached: Dictionary = GdhValues.node_at(root, root_path)
60
57
  if reached.has("message"):
61
58
  return reached
@@ -66,8 +63,8 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
66
63
  # Whether the words are only inside longer words there, "out" in "Scout".
67
64
  var inside: Array[bool] = []
68
65
  var hidden: Array[Node] = []
69
- # Controls saying exactly the words that a click cannot reach, kept to say what a match on screen
70
- # that only says them as part of more stands in for.
66
+ # Controls saying exactly the words that a click cannot reach, kept to say what a match on
67
+ # screen that only says them as part of more stands in for.
71
68
  var exact_out_of_reach: Array[Node] = []
72
69
  var out_of_reach_as: Array[String] = []
73
70
  # The item of a control drawing a list that matched, null for a control matched as a whole, and
@@ -84,8 +81,8 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
84
81
  exact_out_of_reach.append(node)
85
82
  out_of_reach_as.append("hidden")
86
83
  elif GdhWords.draws_a_list(node):
87
- # Each item a match of its own, so the words of one tab are matched as a whole and the
88
- # index picks between two items saying them as it does between two buttons.
84
+ # Each item a match of its own, so the words of one tab are matched as a whole and
85
+ # the index picks between two items saying them as it does between two buttons.
89
86
  var drawing: Control = node
90
87
  for entry: Dictionary in GdhWords.items_of(drawing):
91
88
  var words: String = entry["says"]
@@ -98,10 +95,14 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
98
95
  exact.append(whole)
99
96
  inside.append(not whole and _inside_a_word(words, wanted))
100
97
  items.append(entry["item"])
101
- # An item drawn nowhere yet, scrolled out of its list, is judged at the list's middle,
102
- # which is where the click scrolls it towards.
98
+ # An item drawn nowhere yet, scrolled out of its list, is judged at the list's
99
+ # middle, which is where the click scrolls it towards.
103
100
  aims.append(
104
- drawing.size * 0.5 if drawn_at_now.size == Vector2.ZERO else drawn_at_now.get_center()
101
+ (
102
+ drawing.size * 0.5
103
+ if drawn_at_now.size == Vector2.ZERO
104
+ else drawn_at_now.get_center()
105
+ )
105
106
  )
106
107
  else:
107
108
  var control: Control = node
@@ -148,9 +149,9 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
148
149
  out_of_reach_as.append("under %s, which is drawn over it" % over.get_path())
149
150
  if cover == null:
150
151
  cover = over
151
- # Stable, so matches of one rank keep the order they are drawn in. Words only inside longer words
152
- # come after everything else: ranked as a button saying them as part of more, "Scout, 90" was
153
- # pressed for "Out" over a control saying exactly "Out", and spent a game's silver.
152
+ # Stable, so matches of one rank keep the order they are drawn in. Words only inside longer
153
+ # words come after everything else: ranked as a button saying them as part of more, "Scout, 90"
154
+ # was pressed for "Out" over a control saying exactly "Out", and spent a game's silver.
154
155
  open.sort_custom(
155
156
  func(a: int, b: int) -> bool:
156
157
  if inside[a] != inside[b]:
@@ -190,7 +191,7 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
190
191
  _open_menu_note(root, wanted),
191
192
  _offered_note(from, drawn, areas),
192
193
  ]
193
- )
194
+ ),
194
195
  }
195
196
  var best: int = 0
196
197
  for at: int in targets.size():
@@ -204,7 +205,7 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
204
205
  (
205
206
  "%s, so which to click is not clear; pass index, counting from 0%s: %s"
206
207
  % [_described(best, ranks[0], wanted, under), order, _candidates(targets, lines)]
207
- )
208
+ ),
208
209
  }
209
210
  var index: int = 0 if which == null else GdhRead.as_int(which, -1)
210
211
  if index < 0 or index >= targets.size():
@@ -220,31 +221,36 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
220
221
  )
221
222
  return {
222
223
  "type": "error",
223
- "message": "index %s picks nothing: %s: %s" % [given, among, _candidates(targets, lines)]
224
+ "message":
225
+ "index %s picks nothing: %s: %s" % [given, among, _candidates(targets, lines)],
224
226
  }
225
- # A match saying the words as part of more, while another control says exactly them, is a stand-in
226
- # for that one, not the control meant: a tab reading "Out" sat in a collapsed drawer and a dispatch
227
- # saying somebody "fell out in the hall" was pressed, and with the tab's own words on screen in a
228
- # control that is not a button, a card reading "Scout, 90" was. Pressed only when asked for by
229
- # index, which is the caller saying the match is what they meant.
227
+ # A match saying the words as part of more, while another control says exactly them, is a
228
+ # stand-in for that one, not the control meant: a tab reading "Out" sat in a collapsed drawer
229
+ # and a dispatch saying somebody "fell out in the hall" was pressed, and with the tab's own
230
+ # words on screen in a control that is not a button, a card reading "Scout, 90" was. Pressed
231
+ # only when asked for by index, which is the caller saying the match is what they meant.
230
232
  var exact_elsewhere: Array[Node] = exact_out_of_reach.duplicate()
231
233
  var elsewhere_as: Array[String] = out_of_reach_as.duplicate()
232
234
  # On screen only when it takes clicks itself: a heading saying exactly "Onward" beside a button
233
235
  # saying "Onward now" is not what a click on "Onward" is for, and the button is pressed.
234
236
  for other: int in targets.size():
235
- var taking: bool = targets[other].get_mouse_filter_with_override() != Control.MOUSE_FILTER_IGNORE
237
+ var taking: bool = (
238
+ targets[other].get_mouse_filter_with_override() != Control.MOUSE_FILTER_IGNORE
239
+ )
236
240
  if other != index and wholes[other] and taking:
237
241
  exact_elsewhere.append(targets[other])
238
242
  elsewhere_as.append("on screen, taking clicks but not an enabled button")
239
- var best_one: String = "the one on screen," if targets.size() == 1 else "the best match on screen,"
243
+ var best_one: String = (
244
+ "the one on screen," if targets.size() == 1 else "the best match on screen,"
245
+ )
240
246
  if which == null and not wholes[index] and not exact_elsewhere.is_empty():
241
247
  return {
242
248
  "type": "error",
243
249
  "message":
244
250
  (
245
251
  (
246
- 'no %s on screen%s says exactly "%s", so nothing was clicked: %s %s says it as part of'
247
- + ' "%s", while %s; pass index %d to press that one anyway'
252
+ 'no %s on screen%s says exactly "%s", so nothing was clicked: %s %s says it as '
253
+ + 'part of "%s", while %s; pass index %d to press that one anyway'
248
254
  )
249
255
  % [
250
256
  "button" if exact_elsewhere.size() > exact_out_of_reach.size() else "control",
@@ -256,7 +262,7 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
256
262
  _out_of_reach_exactly(exact_elsewhere, elsewhere_as),
257
263
  index,
258
264
  ]
259
- )
265
+ ),
260
266
  }
261
267
  if which == null and insides[index]:
262
268
  return {
@@ -264,15 +270,23 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
264
270
  "message":
265
271
  (
266
272
  (
267
- 'no control on screen%s says "%s" as a word of its own, so nothing was clicked: %s %s has'
268
- + ' it only inside a longer word, "%s"; pass index %d to press that one anyway'
273
+ 'no control on screen%s says "%s" as a word of its own, so nothing was '
274
+ + 'clicked: %s %s has it only inside a longer word, "%s"; pass index %d to '
275
+ + "press that one anyway"
269
276
  )
270
- % [under, wanted, best_one, targets[index].get_path(), _shortened(lines[index]), index]
271
- )
277
+ % [
278
+ under,
279
+ wanted,
280
+ best_one,
281
+ targets[index].get_path(),
282
+ _shortened(lines[index]),
283
+ index,
284
+ ]
285
+ ),
272
286
  }
273
287
  # Words in a sentence on a control that lets the pointer through: the press lands on whatever is
274
- # under it, which is nothing the words name. A line saying what "is out there" was the best match
275
- # left for "Out" once the cards saying "Scout" were passed over.
288
+ # under it, which is nothing the words name. A line saying what "is out there" was the best
289
+ # match left for "Out" once the cards saying "Scout" were passed over.
276
290
  if (
277
291
  which == null
278
292
  and not wholes[index]
@@ -283,14 +297,21 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
283
297
  "message":
284
298
  (
285
299
  (
286
- 'no control on screen%s that takes a click says "%s", so nothing was clicked: %s %s says it'
287
- + ' as part of "%s" and lets the pointer through to what is under it; pass index %d to'
288
- + " click there anyway"
300
+ 'no control on screen%s that takes a click says "%s", so nothing was clicked: '
301
+ + '%s %s says it as part of "%s" and lets the pointer through to what is under '
302
+ + "it; pass index %d to click there anyway"
289
303
  )
290
- % [under, wanted, best_one, targets[index].get_path(), _shortened(lines[index]), index]
291
- )
304
+ % [
305
+ under,
306
+ wanted,
307
+ best_one,
308
+ targets[index].get_path(),
309
+ _shortened(lines[index]),
310
+ index,
311
+ ]
312
+ ),
292
313
  }
293
- var found: Dictionary = {"says": wanted, "index": index, "of": targets.size()}
314
+ var found: Dictionary = { "says": wanted, "index": index, "of": targets.size() }
294
315
  if not wholes[index]:
295
316
  found["partOf"] = _shortened(lines[index])
296
317
  if which == null and targets.size() > 1:
@@ -305,7 +326,7 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
305
326
  found["picked"] = "%s; %s" % [_described(1, ranks[0], wanted, under), others]
306
327
  if covered > 0:
307
328
  found["covered"] = covered
308
- var answer: Dictionary = {"path": str(targets[index].get_path()), "found": found}
329
+ var answer: Dictionary = { "path": str(targets[index].get_path()), "found": found }
309
330
  if picked_items[index] != null:
310
331
  answer["item"] = picked_items[index]
311
332
  found["item"] = GdhItems.item_named(targets[index], picked_items[index], lines[index])
@@ -392,11 +413,14 @@ static func _inside_a_word(written: String, wanted: String) -> bool:
392
413
  return found
393
414
 
394
415
 
395
- ## Whether the character [param code] is part of a word: a letter, a digit or an underscore. A letter
396
- ## is told by having a case, which leaves scripts without one counted as breaks between words.
416
+ ## Whether the character [param code] is part of a word: a letter, a digit or an underscore. A
417
+ ## letter is told by having a case, which leaves scripts without one counted as breaks between
418
+ ## words.
397
419
  static func _in_a_word(code: int) -> bool:
398
420
  var character: String = String.chr(code)
399
- return character.to_lower() != character.to_upper() or character.is_valid_int() or character == "_"
421
+ return (
422
+ character.to_lower() != character.to_upper() or character.is_valid_int() or character == "_"
423
+ )
400
424
 
401
425
 
402
426
  ## [param count] matches of [param rank], as the start of a sentence.
@@ -432,7 +456,8 @@ static func _passed_over(best: int, others: Array[int], one: bool) -> String:
432
456
  reasons.append("is not an enabled button" if one else "are not enabled buttons")
433
457
  if lacking & WHOLE:
434
458
  reasons.append("says it as part of more" if one else "say it as part of more")
435
- # Ranked level with the best, a match was passed over for having the words only inside longer ones.
459
+ # Ranked level with the best, a match was passed over for having the words only inside longer
460
+ # ones.
436
461
  if reasons.is_empty():
437
462
  return "says it only inside a longer word" if one else "say it only inside longer words"
438
463
  if reasons.size() == 1:
@@ -476,7 +501,8 @@ static func _outside_note(outside: int, root: Node) -> String:
476
501
  if outside == 0:
477
502
  return ""
478
503
  var note: String = (
479
- "; %d control%s it outside what the screen shows" % [outside, " says" if outside == 1 else "s say"]
504
+ "; %d control%s it outside what the screen shows"
505
+ % [outside, " says" if outside == 1 else "s say"]
480
506
  )
481
507
  var window: Window = root as Window
482
508
  return note if window == null else note + no_window_note(window, window)
@@ -499,7 +525,9 @@ static func no_window_note(viewport: Viewport, root: Window) -> String:
499
525
  ". This game has no window, and a game with no window has a 64 by 64 viewport whatever "
500
526
  + "the project settings say: run it with a window to reach this control"
501
527
  )
502
- return ". This game has no window, and its viewport is %d by %d" % [roundi(size.x), roundi(size.y)]
528
+ return (
529
+ ". This game has no window, and its viewport is %d by %d" % [roundi(size.x), roundi(size.y)]
530
+ )
503
531
 
504
532
 
505
533
  ## What to add to a refusal that found nothing on screen, when the controls saying it are covered.
@@ -508,7 +536,10 @@ static func _covered_note(covered: int, cover: Node) -> String:
508
536
  return ""
509
537
  if covered == 1:
510
538
  return "; 1 control says it under %s, which is drawn over it" % cover.get_path()
511
- return "; %d controls say it under what is drawn over them, such as %s" % [covered, cover.get_path()]
539
+ return (
540
+ "; %d controls say it under what is drawn over them, such as %s"
541
+ % [covered, cover.get_path()]
542
+ )
512
543
 
513
544
 
514
545
  ## [param control], or the button holding it when it is not one itself, which is what a click on
@@ -552,7 +583,10 @@ static func why_hidden(node: Node) -> String:
552
583
  var control: Control = node as Control
553
584
  var instead: Node = null if control == null else drawn_at(control)
554
585
  if instead != null:
555
- return " (%s; a click where it would be reaches %s)" % [", and ".join(named), instead.get_path()]
586
+ return (
587
+ " (%s; a click where it would be reaches %s)"
588
+ % [", and ".join(named), instead.get_path()]
589
+ )
556
590
  var outermost: Node = hiding.back()
557
591
  var beside: Array[String] = []
558
592
  var parent: Node = outermost.get_parent()
@@ -702,7 +736,9 @@ static func _areas_of(drawn: Array[Node]) -> Array[Rect2]:
702
736
  areas.append(Rect2(window.position, window.size))
703
737
  else:
704
738
  var control: Control = each
705
- areas.append(control.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, control.size))
739
+ areas.append(
740
+ control.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, control.size)
741
+ )
706
742
  return areas
707
743
 
708
744
 
@@ -717,16 +753,18 @@ static func _areas_of(drawn: Array[Node]) -> Array[Rect2]:
717
753
  ## the click scrolls it into view first: what is drawn over its place now is not what it will land
718
754
  ## under. Judged not covered at all instead, a button below the fold of a roster that a full-screen
719
755
  ## page covered was picked over the ones in view, scrolled up under the page and pressed there.
720
- ## Whatever else the scroll holds moves with it and is judged where it goes. Judged where it sat, the
721
- ## card at the top of a tray scrolled down was drawn over a button above the view, because a later
722
- ## card is drawn over an earlier one, and a button below the view was passed by the same card only
723
- ## because the cards beside the bottom edge come before it.
756
+ ## Whatever else the scroll holds moves with it and is judged where it goes. Judged where it sat,
757
+ ## the card at the top of a tray scrolled down was drawn over a button above the view, because a
758
+ ## later card is drawn over an earlier one, and a button below the view was passed by the same card
759
+ ## only because the cards beside the bottom edge come before it.
724
760
  ##
725
761
  ## [param areas] holds the box around each of [param drawn], so everything whose box misses the
726
762
  ## point is passed over before anything costlier is asked of it. A window's box is in the space of
727
763
  ## the viewport it is embedded in, which is not the target's when the target is in another window,
728
764
  ## so a window is judged by [method _window_over] instead.
729
- static func _cover_of(target: Control, drawn: Array[Node], areas: Array[Rect2], aim: Vector2) -> Node:
765
+ static func _cover_of(
766
+ target: Control, drawn: Array[Node], areas: Array[Rect2], aim: Vector2
767
+ ) -> Node:
730
768
  var viewport: Viewport = target.get_viewport()
731
769
  var moves: Dictionary[ScrollContainer, Vector2] = _scrolls_for(target)
732
770
  var point: Vector2 = target.get_global_transform_with_canvas() * aim + _moved_by(target, moves)
@@ -826,7 +864,9 @@ static func _holds(control: Control, point: Vector2) -> bool:
826
864
  ## inside each, innermost first, which is how one brings a control into view. A target larger than a
827
865
  ## container is centred in it. Only a ScrollContainer, because that is all a click scrolls: another
828
866
  ## container clipping what it holds, a collapsed section for one, leaves the target where it is.
829
- static func _where_it_lands(target: Control, point: Vector2) -> Dictionary[ScrollContainer, Vector2]:
867
+ static func _where_it_lands(
868
+ target: Control, point: Vector2
869
+ ) -> Dictionary[ScrollContainer, Vector2]:
830
870
  var drawn: Transform2D = target.get_global_transform_with_canvas()
831
871
  var half: Vector2 = (drawn * Rect2(Vector2.ZERO, target.size)).size * 0.5
832
872
  var moves: Dictionary[ScrollContainer, Vector2] = {}
@@ -835,7 +875,9 @@ static func _where_it_lands(target: Control, point: Vector2) -> Dictionary[Scrol
835
875
  while walk != null and not (walk is Viewport):
836
876
  var holder: ScrollContainer = walk as ScrollContainer
837
877
  if holder != null:
838
- var shown: Rect2 = holder.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, holder.size)
878
+ var shown: Rect2 = (
879
+ holder.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, holder.size)
880
+ )
839
881
  var low: Vector2 = shown.position + half
840
882
  var high: Vector2 = shown.end - half
841
883
  var next: Vector2 = Vector2(
@@ -857,7 +899,10 @@ static func _where_it_lands(target: Control, point: Vector2) -> Dictionary[Scrol
857
899
  static func _off_screen(target: Control, aim: Vector2) -> bool:
858
900
  var moves: Dictionary[ScrollContainer, Vector2] = _scrolls_for(target)
859
901
  var point: Vector2 = target.get_global_transform_with_canvas() * aim + _moved_by(target, moves)
860
- if _clipped_away(target, point, moves) or not target.get_viewport().get_visible_rect().has_point(point):
902
+ if (
903
+ _clipped_away(target, point, moves)
904
+ or not target.get_viewport().get_visible_rect().has_point(point)
905
+ ):
861
906
  return true
862
907
  var reached: Dictionary = GdhScreen.reach(target.get_viewport(), point)
863
908
  var viewport: Viewport = reached["viewport"]
@@ -877,7 +922,11 @@ static func _clipped_away(
877
922
  var walk: Node = control.get_parent()
878
923
  while walk != null and not (walk is Viewport):
879
924
  var holder: Control = walk as Control
880
- if holder != null and holder.clip_contents and not _holds(holder, point - _moved_by(holder, moves)):
925
+ if (
926
+ holder != null
927
+ and holder.clip_contents
928
+ and not _holds(holder, point - _moved_by(holder, moves))
929
+ ):
881
930
  return true
882
931
  walk = walk.get_parent()
883
932
  return false
@@ -901,9 +950,9 @@ static func _candidates(targets: Array[Control], said: Array[String]) -> String:
901
950
 
902
951
 
903
952
  ## What to add to a refusal that found nothing on screen, when an open menu has an item saying the
904
- ## words: a menu draws its items rather than holding a control for each, so a click finds none, while
905
- ## a wait for the words finds the item. A wait met on "Leave the hall" was followed by a click on it
906
- ## refused as nothing on screen saying it, with no word of the call that chooses it.
953
+ ## words: a menu draws its items rather than holding a control for each, so a click finds none,
954
+ ## while a wait for the words finds the item. A wait met on "Leave the hall" was followed by a click
955
+ ## on it refused as nothing on screen saying it, with no word of the call that chooses it.
907
956
  static func _open_menu_note(root: Node, wanted: String) -> String:
908
957
  var pending: Array[Node] = [root]
909
958
  while not pending.is_empty():
@@ -914,11 +963,13 @@ static func _open_menu_note(root: Node, wanted: String) -> String:
914
963
  var text: String = menu.get_item_text(item)
915
964
  if GdhSays.matches(text, wanted):
916
965
  var owner: Node = menu.get_parent()
917
- var named: Node = owner if owner is MenuButton or owner is OptionButton else menu
966
+ var named: Node = (
967
+ owner if owner is MenuButton or owner is OptionButton else menu
968
+ )
918
969
  return (
919
970
  (
920
- '; the open menu %s has an item saying it, "%s", which a click cannot reach:'
921
- + ' choose it with runtime_input choose, path %s and text "%s"'
971
+ '; the open menu %s has an item saying it, "%s", which a click cannot '
972
+ + 'reach: choose it with runtime_input choose, path %s and text "%s"'
922
973
  )
923
974
  % [menu.get_path(), text, named.get_path(), text]
924
975
  )
@@ -0,0 +1,211 @@
1
+ extends RefCounted
2
+ ## What reaches the running game the way a touchscreen reaches it: a finger pressed, held, lifted
3
+ ## and dragged, as InputEventScreenTouch and InputEventScreenDrag.
4
+ ##
5
+ ## The events themselves rather than a mouse standing in for them: input code tells a finger from a
6
+ ## mouse by the event's class, or by the mouse events the engine makes from a touch, which carry
7
+ ## InputEvent.DEVICE_ID_EMULATION. A mouse_click carries the mouse's own device, so a game switching
8
+ ## its glyphs or focus on a touch never saw one. Sent through Input for the game's window, so the
9
+ ## engine makes those mouse events itself where input_devices/pointing/emulate_mouse_from_touch is
10
+ ## on.
11
+
12
+ const GdhRead = preload("reading.gd")
13
+ const GdhValues = preload("runtime_values.gd")
14
+ const GdhMouse = preload("runtime_mouse.gd")
15
+ const EMULATE_SETTING: String = "input_devices/pointing/emulate_mouse_from_touch"
16
+
17
+ var _host: Node
18
+ # The finger the engine makes mouse events from, as Input keeps it for the touches sent here: the
19
+ # first one pressed while none is, until it lifts. -1 for none.
20
+ var _emulating: int = -1
21
+
22
+
23
+ func _init(host: Node) -> void:
24
+ _host = host
25
+
26
+
27
+ ## Presses a finger at a position, and lifts it a frame later unless asked to hold it, as
28
+ ## inject_mouse_click does a button: a finger left down is a touch that never ends.
29
+ func inject_touch(params: Dictionary) -> Dictionary:
30
+ var root: Window = _host.get_tree().root
31
+ var aimed: Variant = GdhMouse.aim_of(params, root)
32
+ if aimed is String:
33
+ return { "type": "error", "message": aimed }
34
+ var aim: Dictionary = aimed
35
+ var point: Variant = GdhMouse.read_point(params, "x", "y")
36
+ if point is String:
37
+ return { "type": "error", "message": point }
38
+ var finger: Variant = _finger_of(params)
39
+ if finger is String:
40
+ return { "type": "error", "message": finger }
41
+ var index: int = finger
42
+ var holding: Variant = GdhValues.held_for(params)
43
+ if holding is String:
44
+ return { "type": "error", "message": holding }
45
+ var frames: int = holding
46
+ var local: Vector2 = point
47
+ var placed: Dictionary = GdhMouse.carried(aim, local, root)
48
+ var viewport: Viewport = placed["viewport"]
49
+ var position: Vector2 = placed["position"]
50
+ var held: bool = GdhRead.as_bool(params.get("pressed", true), true)
51
+ var whole: bool = not params.has("pressed")
52
+
53
+ var at: Array[Vector2] = [position]
54
+ var where: Dictionary = GdhMouse.where(aim, viewport, at)
55
+ var emulates: bool = _send(viewport, _touch(position, index, whole or held))
56
+ var lasted: Variant = null
57
+ if whole:
58
+ lasted = await GdhValues.hold(_host.get_tree(), frames)
59
+ var _lifted: bool = _send(viewport, _touch(position, index, false))
60
+ await _host.get_tree().process_frame
61
+
62
+ var answer: Dictionary = {
63
+ "type": "input_injected",
64
+ "input_type": "touch",
65
+ "index": index,
66
+ "position": [position.x, position.y],
67
+ "held_frames": GdhValues.held_answer(whole, frames),
68
+ "held_for": lasted,
69
+ "pressed": held and not whole,
70
+ "whole": whole,
71
+ "emulates_mouse": emulates,
72
+ }
73
+ answer.merge(where)
74
+ return answer
75
+
76
+
77
+ ## Drags a finger from one point to another through any between: the touch, one drag each process
78
+ ## frame along the stroke, and the lift where the last drag left it, a frame after it. Each drag
79
+ ## goes in a frame of its own because the engine merges the drags it is given within one frame, as
80
+ ## it merges a mouse's motions.
81
+ func inject_touch_drag(params: Dictionary) -> Dictionary:
82
+ var root: Window = _host.get_tree().root
83
+ var aimed: Variant = GdhMouse.aim_of(params, root)
84
+ if aimed is String:
85
+ return { "type": "error", "message": aimed }
86
+ var aim: Dictionary = aimed
87
+ var stroke: Variant = GdhMouse.stroke_of(params, "touch_drag")
88
+ if stroke is String:
89
+ return { "type": "error", "message": stroke }
90
+ var finger: Variant = _finger_of(params)
91
+ if finger is String:
92
+ return { "type": "error", "message": finger }
93
+ var index: int = finger
94
+ var frames: int = GdhRead.as_int(params.get("frames", 0), 0)
95
+ if frames < 1 or frames > GdhValues.MOST_HELD:
96
+ return {
97
+ "type": "error",
98
+ "message": "frames is %d, and it takes 1 to %d." % [frames, GdhValues.MOST_HELD],
99
+ }
100
+ # Carried once, before the touch: a finger drags across the screen, not along a node that moves.
101
+ var named: Array[Vector2] = stroke
102
+ var points: Array[Vector2] = []
103
+ var viewport: Viewport = root
104
+ for local: Vector2 in named:
105
+ var placed: Dictionary = GdhMouse.carried(aim, local, root)
106
+ viewport = placed["viewport"]
107
+ points.append(placed["position"])
108
+
109
+ var lengths: Array[float] = GdhMouse.lengths_of(points)
110
+ var start: Vector2 = points[0]
111
+ var end: Vector2 = points[points.size() - 1]
112
+ var where: Dictionary = GdhMouse.where(aim, viewport, points)
113
+ var emulates: bool = _send(viewport, _touch(start, index, true))
114
+ var last: Vector2 = start
115
+ var travelled: Vector2 = Vector2.ZERO
116
+ for step: int in range(1, frames + 1):
117
+ await _host.get_tree().process_frame
118
+ if not is_instance_valid(viewport):
119
+ return {
120
+ "type": "error",
121
+ "message":
122
+ (
123
+ (
124
+ "%s was freed after the touch and %d of %d drags, so the rest of the stroke"
125
+ + " and the lift went nowhere"
126
+ )
127
+ % [where.get("window", "The window"), step - 1, frames]
128
+ ),
129
+ }
130
+ var at: Vector2 = GdhMouse.along(points, lengths, float(step) / frames)
131
+ var relative: Vector2 = at - last
132
+ last = at
133
+ travelled += relative
134
+ var _dragged: bool = _send(viewport, _drag(at, relative, index))
135
+ await _host.get_tree().process_frame
136
+ var _lifted: bool = _send(viewport, _touch(end, index, false))
137
+ await _host.get_tree().process_frame
138
+
139
+ var answer: Dictionary = {
140
+ "type": "input_injected",
141
+ "input_type": "touch_drag",
142
+ "index": index,
143
+ "from": [start.x, start.y],
144
+ "to": [end.x, end.y],
145
+ "motions": frames,
146
+ "relative_total": [travelled.x, travelled.y],
147
+ "emulates_mouse": emulates,
148
+ }
149
+ answer.merge(where)
150
+ return answer
151
+
152
+
153
+ ## The finger [param params].index names, 0 when none is given, or the String refusing it.
154
+ static func _finger_of(params: Dictionary) -> Variant:
155
+ if not params.has("index"):
156
+ return 0
157
+ var given: Variant = params["index"]
158
+ var number: float = GdhRead.as_float(given)
159
+ if not (given is int or given is float) or number != roundf(number):
160
+ return "index is the finger, a whole number from 0, and was %s." % JSON.stringify(given)
161
+ var index: int = roundi(number)
162
+ if index < 0:
163
+ return "index is the finger, a whole number from 0, and was %d." % index
164
+ return index
165
+
166
+
167
+ ## Sends [param event] into [param viewport]: through Input for the game's window, and pushed into
168
+ ## any other, since Input reaches only the game's window. Answers whether the engine makes a mouse
169
+ ## event from it, which it does in Input, with the setting on, for one finger at a time.
170
+ func _send(viewport: Viewport, event: InputEvent) -> bool:
171
+ if viewport != _host.get_tree().root:
172
+ if is_instance_valid(viewport):
173
+ viewport.push_input(event)
174
+ return false
175
+ Input.parse_input_event(event)
176
+ if not GdhRead.as_bool(ProjectSettings.get_setting(EMULATE_SETTING, true), true):
177
+ return false
178
+ var touch: InputEventScreenTouch = event as InputEventScreenTouch
179
+ if touch == null:
180
+ return (event as InputEventScreenDrag).index == _emulating
181
+ if touch.pressed:
182
+ if _emulating != -1:
183
+ return false
184
+ _emulating = touch.index
185
+ return true
186
+ if touch.index != _emulating:
187
+ return false
188
+ _emulating = -1
189
+ return true
190
+
191
+
192
+ static func _touch(position: Vector2, index: int, pressed: bool) -> InputEventScreenTouch:
193
+ var event: InputEventScreenTouch = InputEventScreenTouch.new()
194
+ event.index = index
195
+ event.position = position
196
+ event.pressed = pressed
197
+ return event
198
+
199
+
200
+ ## A drag carrying [param relative] as the movement since the last, and the speed that is at the
201
+ ## frame rate the game runs at.
202
+ func _drag(position: Vector2, relative: Vector2, index: int) -> InputEventScreenDrag:
203
+ var event: InputEventScreenDrag = InputEventScreenDrag.new()
204
+ event.index = index
205
+ event.position = position
206
+ event.relative = relative
207
+ event.screen_relative = relative
208
+ var seconds: float = maxf(_host.get_process_delta_time(), 1.0 / 60.0)
209
+ event.velocity = relative / seconds
210
+ event.screen_velocity = event.velocity
211
+ return event
@@ -0,0 +1 @@
1
+ uid://ciivqlhs2dev0