gdharness 1.3.59 → 1.3.61

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 (77) hide show
  1. package/README.md +20 -20
  2. package/build/cli.js +2264 -1706
  3. package/build/godot/addons/auto_reload/auto_reload.gd +3 -3
  4. package/build/godot/addons/gdharness_editor/bridge_client.gd +4 -4
  5. package/build/godot/addons/gdharness_editor/declared_values.gd +13 -13
  6. package/build/godot/addons/gdharness_editor/file_reads.gd +6 -6
  7. package/build/godot/addons/gdharness_editor/headless_check.gd +9 -6
  8. package/build/godot/addons/gdharness_editor/property_paths.gd +3 -3
  9. package/build/godot/addons/gdharness_editor/property_values.gd +18 -11
  10. package/build/godot/addons/gdharness_editor/reading.gd +22 -18
  11. package/build/godot/addons/gdharness_editor/scene_file.gd +5 -5
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +4 -4
  13. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +5 -5
  14. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +6 -6
  15. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +9 -9
  16. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +3 -3
  17. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +5 -5
  18. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +5 -5
  19. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +2 -2
  20. package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +1 -1
  21. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +5 -5
  22. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +3 -3
  23. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +3 -3
  24. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +4 -4
  25. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +3 -3
  26. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +10 -10
  27. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +1 -1
  28. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +5 -5
  29. package/build/godot/addons/gdharness_runtime/declared_values.gd +13 -13
  30. package/build/godot/addons/gdharness_runtime/reading.gd +22 -18
  31. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +2 -2
  32. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +89 -18
  33. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +6 -6
  34. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +10 -10
  35. package/build/godot/addons/gdharness_runtime/runtime_frames.gd +5 -4
  36. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +3 -4
  37. package/build/godot/addons/gdharness_runtime/runtime_input.gd +8 -8
  38. package/build/godot/addons/gdharness_runtime/runtime_items.gd +1 -1
  39. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +3 -3
  40. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +5 -5
  41. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +20 -20
  42. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +13 -13
  43. package/build/godot/addons/gdharness_runtime/runtime_says.gd +5 -5
  44. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +39 -39
  45. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +3 -3
  46. package/build/godot/addons/gdharness_runtime/runtime_values.gd +7 -7
  47. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +8 -8
  48. package/build/godot/addons/gdharness_runtime/runtime_words.gd +5 -5
  49. package/build/godot/addons/gdharness_runtime/serialisation.gd +4 -4
  50. package/build/godot/addons/gdharness_tests/bench.gd +1 -1
  51. package/build/godot/addons/gdharness_tests/share.gd +6 -6
  52. package/build/godot/addons/gdharness_tests/suites.gd +1 -1
  53. package/build/godot/operations/audio_buses.gd +4 -4
  54. package/build/godot/operations/class_cache.gd +3 -3
  55. package/build/godot/operations/classdb_queries.gd +1 -1
  56. package/build/godot/operations/declared_values.gd +13 -13
  57. package/build/godot/operations/dependencies.gd +6 -6
  58. package/build/godot/operations/export_presets.gd +7 -7
  59. package/build/godot/operations/file_reads.gd +6 -6
  60. package/build/godot/operations/gdscript_analysis.gd +6 -6
  61. package/build/godot/operations/gdscript_authoring.gd +11 -11
  62. package/build/godot/operations/godot_operations.gd +1 -1
  63. package/build/godot/operations/import_pipeline.gd +12 -12
  64. package/build/godot/operations/input_actions.gd +3 -3
  65. package/build/godot/operations/logger.gd +1 -1
  66. package/build/godot/operations/plugins.gd +2 -2
  67. package/build/godot/operations/project_config.gd +11 -11
  68. package/build/godot/operations/project_validation.gd +1 -1
  69. package/build/godot/operations/property_paths.gd +3 -3
  70. package/build/godot/operations/property_values.gd +18 -11
  71. package/build/godot/operations/reading.gd +22 -18
  72. package/build/godot/operations/resource_files.gd +1 -1
  73. package/build/godot/operations/script_check.gd +2 -2
  74. package/build/godot/operations/serialisation.gd +4 -4
  75. package/build/godot/tests/listed.gd +28 -10
  76. package/build/keeper.js +511 -161
  77. package/package.json +1 -1
@@ -139,7 +139,7 @@ static func from_the_root(said: String, node_path: String, root_name: String) ->
139
139
 
140
140
  ## [param said], the refusal for a path a scene does not have, with where the path leaves it: the
141
141
  ## last node of the path it has, [param reached] from the root, called [param root_name], when that
142
- ## is empty; the name below it that is missing; and [param children], the names that node has, as
142
+ ## is empty, the name below it that is missing, and [param children], the names that node has, as
143
143
  ## after a node was moved or renamed. Names up to [constant NAMED_CHILDREN] of them.
144
144
  static func missing_below(
145
145
  said: String,
@@ -152,17 +152,21 @@ static func missing_below(
152
152
  if children.size() > NAMED_CHILDREN:
153
153
  shown += " and %d more" % (children.size() - NAMED_CHILDREN)
154
154
  return (
155
- "%s%s %s has no %s; %s."
155
+ "%s %s has no %s. %s."
156
156
  % [
157
- said,
158
- "" if said.ends_with(".") else ".",
157
+ ended(said),
159
158
  "The root, %s," % root_name if reached.is_empty() else "/".join(reached),
160
159
  missing,
161
- "its children are " + shown if not children.is_empty() else "it has no children",
160
+ "Its children are " + shown if not children.is_empty() else "It has no children",
162
161
  ]
163
162
  )
164
163
 
165
164
 
165
+ ## [param said] with a full stop at its end, so another sentence can follow it.
166
+ static func ended(said: String) -> String:
167
+ return said if said.ends_with(".") else said + "."
168
+
169
+
166
170
  ## [param said], the refusal for a [param kind] the scene does not have at [param node_path], with
167
171
  ## what is there when the scene has a node there, [param found] its class, and [param holders], the
168
172
  ## paths of the nodes in the scene that are one, so the caller has the path to pass. Names up to
@@ -180,7 +184,7 @@ static func not_of_kind(
180
184
  return (
181
185
  told
182
186
  + (
183
- " The scene holds no %s; scene_node add with nodeType %s puts one in it."
187
+ " The scene holds no %s. scene_node add with nodeType %s puts one in it."
184
188
  % [kind, kind]
185
189
  )
186
190
  )
@@ -222,17 +226,17 @@ static func offered(said: String, word: String, names: Array) -> String:
222
226
  var every: PackedStringArray = PackedStringArray(names)
223
227
  var close: PackedStringArray = nearest(word, every)
224
228
  if close.is_empty():
225
- return "%s; it has %s" % [said, ", ".join(every)]
229
+ return "%s It has %s" % [ended(said), ", ".join(every)]
226
230
  return (
227
- "%s; the nearest it has %s %s"
228
- % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
231
+ "%s The nearest it has %s %s"
232
+ % [ended(said), "is" if close.size() == 1 else "are", ", ".join(close)]
229
233
  )
230
234
 
231
235
 
232
236
  ## [param said], the refusal for [param property], which [param target] does not declare, with the
233
237
  ## names it declares nearest it when any are, as a misspelt one is usually one of them, or else the
234
238
  ## variables its script declares. The engine's own are not listed when none is near, since an object
235
- ## has a hundred, while a script's are the few a caller most likely meant; an object with no script
239
+ ## has a hundred, while a script's are the few a caller most likely meant. An object with no script
236
240
  ## is told where they are listed instead, since the refusal alone left nothing to try.
237
241
  static func near_property(said: String, target: Object, property: String) -> String:
238
242
  var headings: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
@@ -243,7 +247,7 @@ static func near_property(said: String, target: Object, property: String) -> Str
243
247
  if usage & PROPERTY_USAGE_ARRAY:
244
248
  var listed: String = _list_called(target, declared, property)
245
249
  if not listed.is_empty():
246
- return "%s; %s" % [said, listed]
250
+ return "%s %s" % [ended(said), listed]
247
251
  if not usage & headings:
248
252
  names.append(str(declared.get("name", "")))
249
253
  if usage & PROPERTY_USAGE_SCRIPT_VARIABLE and not usage & headings:
@@ -251,8 +255,8 @@ static func near_property(said: String, target: Object, property: String) -> Str
251
255
  var close: PackedStringArray = nearest(property, PackedStringArray(names))
252
256
  if not close.is_empty():
253
257
  return (
254
- "%s; the nearest it has %s %s"
255
- % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
258
+ "%s The nearest it has %s %s"
259
+ % [ended(said), "is" if close.size() == 1 else "are", ", ".join(close)]
256
260
  )
257
261
  # With a script that declares nothing readable, as one that does not compile, the name is
258
262
  # likelier the script's than the class's, so the class's list would point the wrong way.
@@ -261,17 +265,17 @@ static func near_property(said: String, target: Object, property: String) -> Str
261
265
  if scripted.is_empty():
262
266
  return (
263
267
  (
264
- "%s; editor_classes info with className %s and includeInherited true lists the "
268
+ "%s editor_classes info with className %s and includeInherited true lists the "
265
269
  + "ones it has"
266
270
  )
267
- % [said, target.get_class()]
271
+ % [ended(said), target.get_class()]
268
272
  )
269
273
  scripted.sort()
270
- return "%s; its script declares %s" % [said, ", ".join(PackedStringArray(scripted))]
274
+ return "%s Its script declares %s" % [ended(said), ", ".join(PackedStringArray(scripted))]
271
275
 
272
276
 
273
277
  ## How [param target] keeps the list [param property] names, when that is the label of one of the
274
- ## engine's numbered lists, such as an OptionButton's Items; empty when it is not. The engine has no
278
+ ## engine's numbered lists, such as an OptionButton's Items. Empty when it is not. The engine has no
275
279
  ## property by that name, only a count and each entry's fields under a numbered prefix, so "items"
276
280
  ## was refused with nothing near it to offer.
277
281
  static func _list_called(target: Object, count: Dictionary, property: String) -> String:
@@ -292,7 +296,7 @@ static func _list_called(target: Object, count: Dictionary, property: String) ->
292
296
  )
293
297
  return (
294
298
  (
295
- "it keeps its %s as %s, how many there are, and %s under %s for the first, %s for the "
299
+ "It keeps its %s as %s, how many there are, and %s under %s for the first, %s for the "
296
300
  + "second and so on: give %s before the fields of the entries it makes"
297
301
  )
298
302
  % [parts[0].to_lower(), counted, each, first, parts[1] + "1/", counted]
@@ -381,7 +381,7 @@ func get_accessibility(params: Dictionary) -> Dictionary:
381
381
  if elements <= windows.size():
382
382
  var setting: String = _setting()
383
383
  # The command line's --accessibility is consumed by the engine and not handed on, so only
384
- # the setting and a screen reader can say the tree is built; otherwise an empty window is
384
+ # the setting and a screen reader can say the tree is built. Otherwise an empty window is
385
385
  # most likely a game that builds none.
386
386
  # 1 is a screen reader running, 0 none and -1 a platform that cannot say.
387
387
  var read_aloud: bool = (
@@ -606,7 +606,7 @@ static func _match(element: Dictionary, made_by: Dictionary, placing: GdhPlacing
606
606
  ## Control, a RichTextLabel and each of its lines are containers. A RichTextLabel's text in a
607
607
  ## column of plain containers came out straight under the window. So the node is found where it
608
608
  ## must be: below the shown element's node, itself left out of the tree (a node that is shown would
609
- ## have the part under it), drawn around the part, and, for text, holding its words.
609
+ ## have the part under it), drawn around the part, and, for text, one that holds its words.
610
610
  static func _attribute(
611
611
  element: Dictionary, holder: Node, placing: GdhPlacing, is_window: bool
612
612
  ) -> void:
@@ -1,7 +1,7 @@
1
1
  extends Node
2
2
  ## The runtime autoload: a TCP server inside the running game that the gdharness server asks
3
3
  ## about the scene tree, drives with input, and captures. This file is the server and the
4
- ## command table; what each command does lives in a sibling module.
4
+ ## command table. What each command does lives in a sibling module.
5
5
  ##
6
6
  ## The port is whatever the operating system hands out, and the game announces it by writing
7
7
  ## one file named after its process id into a directory the server derives the same way. Two
@@ -14,10 +14,12 @@ const GdhFrames = preload("runtime_frames.gd")
14
14
  const GdhRead = preload("reading.gd")
15
15
  ## Where the modules are loaded from. Kept in step with the markers below.
16
16
  const ADDON_DIR: String = "res://addons/gdharness_runtime/"
17
- ## The modules that answer commands, loaded at the first command one answers rather than preloaded.
17
+ ## The modules that answer commands, compiled on the engine's loading threads rather than preloaded.
18
18
  ## A preload compiles as this script parses, and the modules are twelve thousand lines: compiling
19
19
  ## them took most of a second of every debug start, in games nobody asks anything, a bench's quiet
20
- ## workers and script runs among them.
20
+ ## workers and script runs among them. Compiled at a command instead, they held that frame for up
21
+ ## to 590 ms, which a game's frame bench read as its own stall. So a served game compiles them on a
22
+ ## loading thread from its first frame, where a game nobody serves compiles none.
21
23
  const QUERIES: String = "runtime_queries.gd"
22
24
  const CHANGES: String = "runtime_changes.gd"
23
25
  const INPUT: String = "runtime_input.gd"
@@ -28,6 +30,11 @@ const CAPTURE: String = "runtime_capture.gd"
28
30
  const WAITS: String = "runtime_waits.gd"
29
31
  const ACCESSIBILITY: String = "runtime_accessibility.gd"
30
32
  const VALUES: String = "runtime_values.gd"
33
+ ## Every module in the order a loading thread compiles them: the values one first, since most others
34
+ ## are built with it, then by how soon a caller tends to need one.
35
+ const MODULES: PackedStringArray = [
36
+ VALUES, QUERIES, WAITS, INPUT, TYPING, CHANGES, CAPTURE, PAD, TOUCH, ACCESSIBILITY,
37
+ ]
31
38
  ## Every module command, by the name a request uses, as the module and the method that answers it.
32
39
  const MODULE_COMMANDS: Dictionary = {
33
40
  "get_tree": [QUERIES, "get_tree"],
@@ -132,11 +139,16 @@ const DIGEST_MARKER: String = "res://addons/gdharness_runtime/.gdharness-digest"
132
139
  var values: RefCounted:
133
140
  get:
134
141
  if _values == null:
135
- _values = _loaded(VALUES).new()
142
+ _values = _compiled(VALUES).new()
136
143
  return _values
137
144
 
138
145
  var _values: RefCounted
139
146
  var _frames: GdhFrames = GdhFrames.new()
147
+ var _modules_requested: bool = false
148
+ ## The module path a loading thread is compiling, or "" for none.
149
+ var _in_flight: String = ""
150
+ ## The module paths waiting for a loading thread, the next first.
151
+ var _queued: Array[String] = []
140
152
  ## Each module loaded so far, by its file. Held here because a Callable holds its object by id
141
153
  ## rather than by reference, and a module referenced only by the Callable answering one command
142
154
  ## would be freed once it answered.
@@ -216,6 +228,11 @@ func _process(_delta: float) -> void:
216
228
  return
217
229
  _frames.note_frame(Time.get_ticks_usec())
218
230
  _calibrate_the_profiler()
231
+ if not _modules_requested:
232
+ _modules_requested = true
233
+ _request_modules()
234
+ elif not _in_flight.is_empty() or not _queued.is_empty():
235
+ _advance_modules()
219
236
 
220
237
  # Every connection waiting rather than one a frame: the engine queues eight a game has not taken
221
238
  # and refuses the rest, so calls sent together would be refused as if the game were not up.
@@ -233,7 +250,7 @@ func _process(_delta: float) -> void:
233
250
  var gone: Array[StreamPeerTCP] = []
234
251
  for client: StreamPeerTCP in _clients:
235
252
  # A poll that fails is a client that has already gone, which the status check below is
236
- # about to find; the answer is read so it is not dropped, not because it adds anything.
253
+ # about to find. The answer is read so it is not dropped, not because it adds anything.
237
254
  var polled: Error = client.poll()
238
255
  if polled != OK or client.get_status() != StreamPeerTCP.STATUS_CONNECTED:
239
256
  gone.append(client)
@@ -276,17 +293,66 @@ func _notification(what: int) -> void:
276
293
  _cleanup()
277
294
 
278
295
 
279
- ## The script of the module in [param file], compiled at the first call that needs it.
296
+ ## The script of the module in [param file], compiled here if no thread has it.
280
297
  static func _loaded(file: String) -> GDScript:
281
298
  return load(ADDON_DIR + file) as GDScript
282
299
 
283
300
 
301
+ ## Queues every module not yet compiled for a loading thread. Called at the first frame a served
302
+ ## game runs, after its main scene has loaded.
303
+ func _request_modules() -> void:
304
+ for file: String in MODULES:
305
+ var path: String = ADDON_DIR + file
306
+ if path != _in_flight and not _queued.has(path) and not ResourceLoader.has_cached(path):
307
+ _queued.append(path)
308
+ _advance_modules()
309
+
310
+
311
+ ## Takes the module a loading thread has finished and asks for the next. One at a time, because
312
+ ## many threaded loads at once have deadlocked a game's boot in Godot's loader.
313
+ func _advance_modules() -> void:
314
+ if not _in_flight.is_empty():
315
+ var status: ResourceLoader.ThreadLoadStatus = ResourceLoader.load_threaded_get_status(
316
+ _in_flight
317
+ )
318
+ if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
319
+ return
320
+ # Taken so the loader lets go of the request. A failed one answers null, and the module's
321
+ # first command then compiles it and reports why.
322
+ var _taken: Resource = ResourceLoader.load_threaded_get(_in_flight)
323
+ _in_flight = ""
324
+ while not _queued.is_empty():
325
+ var path: String = _queued.pop_front()
326
+ if ResourceLoader.has_cached(path):
327
+ continue
328
+ # A request the loader refuses leaves the module to be compiled at its first command.
329
+ if ResourceLoader.load_threaded_request(path, "GDScript") == OK:
330
+ _in_flight = path
331
+ return
332
+
333
+
334
+ ## The script of the module in [param file], finished in this frame when its loading thread has not
335
+ ## finished it yet. A command acts in the frame it arrives, so it never waits frames for its module:
336
+ ## a frame wait let 38 frames pass for three, and a signal fired before the wait began. Such a frame
337
+ ## is the runtime's slowness, and is left out of the timing.
338
+ func _compiled(file: String) -> GDScript:
339
+ var path: String = ADDON_DIR + file
340
+ if not ResourceLoader.has_cached(path):
341
+ _frames.skip_this_frame()
342
+ if path == _in_flight:
343
+ var _taken: Resource = ResourceLoader.load_threaded_get(path)
344
+ _in_flight = ""
345
+ else:
346
+ _queued.erase(path)
347
+ return _loaded(file)
348
+
349
+
284
350
  ## The module in [param file], built at its first command with what it is built with.
285
351
  func _module(file: String) -> Object:
286
352
  var made: Object = _modules.get(file)
287
353
  if made != null:
288
354
  return made
289
- var script: GDScript = _loaded(file)
355
+ var script: GDScript = _compiled(file)
290
356
  match file:
291
357
  QUERIES:
292
358
  made = script.new(self, values, _frames)
@@ -295,7 +361,6 @@ func _module(file: String) -> Object:
295
361
  _:
296
362
  made = script.new(self, values)
297
363
  _modules[file] = made
298
- _frames.skip_this_frame()
299
364
  return made
300
365
 
301
366
 
@@ -420,7 +485,7 @@ func _note_the_exit() -> void:
420
485
  ## The debugger reads in order, so its answer comes once it has read everything the game queued
421
486
  ## before the totals, and a debugger that has fallen behind answers late rather than not at all. A
422
487
  ## debugger gdharness gave this game, the keeper of a run the server started or an editor with the
423
- ## gdharness addon, is waited for until it answers; a fixed three seconds lost the totals of a run
488
+ ## gdharness addon, is waited for until it answers. A fixed three seconds lost the totals of a run
424
489
  ## whose first frame named 1536 functions, on a loaded runner that read them slower than that. Any
425
490
  ## other debugger may never answer, and holds the exit for the short wait only.
426
491
  ##
@@ -455,7 +520,7 @@ func _send_the_profile() -> void:
455
520
 
456
521
 
457
522
  ## Sends the debugger the message it answers once it has read everything sent before it, and waits
458
- ## up to [param waits] ms for the answer, polling for it since the game's loop is over.
523
+ ## up to [param waits] ms for the answer. It polls for it since the game's loop is over.
459
524
  func _debugger_answered(waits: int) -> bool:
460
525
  _profile_received = false
461
526
  EngineDebugger.send_message(PROFILE_SENT, [])
@@ -629,7 +694,7 @@ func _start_server() -> void:
629
694
 
630
695
 
631
696
  ## The scene [param given] names for the engine to run, as a `res://` path, when it lies under one
632
- ## of [param prefixes]; otherwise empty. The engine takes any bare argument ending in a scene's
697
+ ## of [param prefixes], and otherwise empty. The engine takes any bare argument ending in a scene's
633
698
  ## extension for the scene, relative to the project or absolute, so each is read as it would.
634
699
  static func quiet_scene_in(given: PackedStringArray, prefixes: Variant) -> String:
635
700
  # A PackedStringArray as project.godot writes one, or an Array of strings as a hand might.
@@ -689,7 +754,7 @@ func _announce(bind_address: String) -> void:
689
754
  if made != OK and made != ERR_ALREADY_EXISTS:
690
755
  push_warning(
691
756
  (
692
- "[gdharness] cannot create %s (%s); the server will not find this game"
757
+ "[gdharness] cannot create %s (%s), so the server will not find this game"
693
758
  % [directory, made]
694
759
  )
695
760
  )
@@ -704,7 +769,7 @@ func _announce(bind_address: String) -> void:
704
769
  if file == null:
705
770
  push_warning(
706
771
  (
707
- "[gdharness] cannot write %s (%s); the server will not find this game"
772
+ "[gdharness] cannot write %s (%s), so the server will not find this game"
708
773
  % [staged, FileAccess.get_open_error()]
709
774
  )
710
775
  )
@@ -712,14 +777,14 @@ func _announce(bind_address: String) -> void:
712
777
  var written: bool = file.store_string(JSON.stringify(_identity(bind_address)))
713
778
  file.close()
714
779
  if not written:
715
- push_warning("[gdharness] cannot write %s; the server will not find this game" % staged)
780
+ push_warning("[gdharness] cannot write %s, so the server will not find this game" % staged)
716
781
  _unstage(staged)
717
782
  return
718
783
  var moved: Error = DirAccess.rename_absolute(staged, path)
719
784
  if moved != OK:
720
785
  push_warning(
721
786
  (
722
- "[gdharness] cannot move %s to %s (%s); the server will not find this game"
787
+ "[gdharness] cannot move %s to %s (%s), so the server will not find this game"
723
788
  % [staged, path, moved]
724
789
  )
725
790
  )
@@ -729,7 +794,7 @@ func _announce(bind_address: String) -> void:
729
794
 
730
795
 
731
796
  ## Takes a staged announcement down after a write or a move that failed. The answer is taken
732
- ## rather than dropped, since these scripts compile under the game's own warning levels; a
797
+ ## rather than dropped, since these scripts compile under the game's own warning levels. A
733
798
  ## staged file that will not go either is nothing any server reads.
734
799
  func _unstage(staged: String) -> void:
735
800
  var _removed: Error = DirAccess.remove_absolute(staged)
@@ -834,7 +899,7 @@ func _ping(_params: Dictionary) -> Dictionary:
834
899
 
835
900
 
836
901
  ## The game asked to close, as a player's close asks it: answered at once and delivered once the
837
- ## answer has gone out. `acceptsQuit` says whether the tree quits on the request by itself; a game
902
+ ## answer has gone out. `acceptsQuit` says whether the tree quits on the request by itself. A game
838
903
  ## that turned that off decides for itself, and may ask the player first.
839
904
  func _close_game(_params: Dictionary) -> Dictionary:
840
905
  _close_asked = true
@@ -884,6 +949,12 @@ func _cleanup() -> void:
884
949
  _clients.clear()
885
950
  _pending.clear()
886
951
  _outgoing.clear()
952
+ # A threaded request nobody takes leaves its load token behind, which the engine reports as a
953
+ # leaked instance at exit. Taken here, which waits for that one module to finish compiling.
954
+ _queued.clear()
955
+ if not _in_flight.is_empty():
956
+ var _taken: Resource = ResourceLoader.load_threaded_get(_in_flight)
957
+ _in_flight = ""
887
958
 
888
959
  if _server:
889
960
  _server.stop()
@@ -896,7 +967,7 @@ func _cleanup() -> void:
896
967
  if removed != OK:
897
968
  push_warning(
898
969
  (
899
- "[gdharness] cannot remove %s; the server will find a game that is gone"
970
+ "[gdharness] cannot remove %s, so the server will find a game that is gone"
900
971
  % _announcement
901
972
  )
902
973
  )
@@ -74,7 +74,7 @@ func capture_viewport(params: Dictionary) -> Dictionary:
74
74
  # and the game's own mode put back after it.
75
75
  var sub: SubViewport = viewport
76
76
  var mode: SubViewport.UpdateMode = sub.render_target_update_mode
77
- # Nothing is drawn without a window, so the frame waited for below may never come; the capture
77
+ # Nothing is drawn without a window, so the frame waited for below may never come. The capture
78
78
  # refuses that state itself.
79
79
  if mode == SubViewport.UPDATE_ALWAYS or not _host.get_tree().root.can_draw():
80
80
  return await _capture(viewport, params)
@@ -144,7 +144,7 @@ func _cut_asked(params: Dictionary, in_window: bool) -> Dictionary:
144
144
  GdhRead.as_int(params.get("width", 0)) > 0 or GdhRead.as_int(params.get("height", 0)) > 0
145
145
  )
146
146
  if region_given != null and not node_path.is_empty():
147
- return _refused("region and nodePath each name the part to capture; give one of them")
147
+ return _refused("region and nodePath each name the part to capture. Give one of them")
148
148
  if (region_given != null or not node_path.is_empty()) and not in_window:
149
149
  return _refused(
150
150
  (
@@ -160,7 +160,7 @@ func _cut_asked(params: Dictionary, in_window: bool) -> Dictionary:
160
160
  return _refused(
161
161
  (
162
162
  "zoom enlarges by a whole factor and keeps every pixel square, and width and "
163
- + "height scale to a size; give one or the other"
163
+ + "height scale to a size. Give one or the other"
164
164
  )
165
165
  )
166
166
  var asked: Dictionary = { "zoom": zoom }
@@ -239,7 +239,7 @@ static func drawn_rect_of(node: Node) -> Variant:
239
239
  item.get_viewport(), item.get_global_transform_with_canvas() * own
240
240
  )
241
241
  var no_extent: String = (
242
- "%s is a %s, which has a position on screen and no extent; give region around the point "
242
+ "%s is a %s, which has a position on screen and no extent. Give region around the point "
243
243
  + "runtime_inspect rect answers for it"
244
244
  )
245
245
  return no_extent % [node.get_path(), node.get_class()]
@@ -286,7 +286,7 @@ static func cut(image: Image, picture: Rect2i, zoom: int) -> Image:
286
286
  return part
287
287
 
288
288
 
289
- ## The server names the file, so a game cannot point it at a path of its own choosing; a call
289
+ ## The server names the file, so a game cannot point it at a path of its own choosing. A call
290
290
  ## with no path is a call the server did not make.
291
291
  func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false) -> Dictionary:
292
292
  var requested_path: String = str(params.get("output_path", ""))
@@ -324,7 +324,7 @@ func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false
324
324
  # The game announces itself before its first frame, and until a frame has been drawn the texture
325
325
  # holds nothing the game drew: blank on one machine, solid white on another, which a caller took
326
326
  # for the game flashing white on boot. Measured on 4.7.2 with both renderers, only the first
327
- # processed frame reads it so; the next one holds the scene.
327
+ # processed frame reads it so. The next one holds the scene.
328
328
  while Engine.get_frames_drawn() == 0:
329
329
  await _host.get_tree().process_frame
330
330
 
@@ -81,7 +81,7 @@ func set_property(params: Dictionary) -> Dictionary:
81
81
  return fitting
82
82
  var given: Variant = fitting["value"]
83
83
  # A component of a vector or a transform is written into a copy, which is put back where it was
84
- # read from, setter and all; the read back is then taken off the game again, since the copy here
84
+ # read from, setter and all. The read back is then taken off the game again, since the copy here
85
85
  # says only what was written into it.
86
86
  var chain: Array[Dictionary] = reached["chain"]
87
87
  var copied: bool = GdhPaths.composite(holder)
@@ -133,7 +133,7 @@ func set_property(params: Dictionary) -> Dictionary:
133
133
  why += (
134
134
  (
135
135
  ". %s is declared as %s, which is also how the engine names a script class "
136
- + "with no class_name; if it is one, the object given has the right class and "
136
+ + "with no class_name. If it is one, the object given has the right class and "
137
137
  + "another script: it is %s"
138
138
  )
139
139
  % [named, str(slot["class"]), GdhValues.class_of(refused_object)]
@@ -165,7 +165,7 @@ func set_property(params: Dictionary) -> Dictionary:
165
165
  answer["unchanged"] = true
166
166
  _keep_reported(answer, "errors", caught.raised)
167
167
  _keep_reported(answer, "warnings", caught.warned)
168
- # The engine stores a number past an export range, or one no enum value has, as given; only the
168
+ # The engine stores a number past an export range, or one no enum value has, as given. Only the
169
169
  # inspector holds a property to them, so the game now holds a value its own script never
170
170
  # expected.
171
171
  if holder is Object:
@@ -484,7 +484,7 @@ func call_method(params: Dictionary) -> Dictionary:
484
484
  var close: PackedStringArray = GdhRead.nearest(node_path, PackedStringArray(classes))
485
485
  standing["message"] = (
486
486
  (
487
- "%s. No global class is called %s either%s; a static function is called with "
487
+ "%s. No global class is called %s either%s. A static function is called with "
488
488
  + "its class's name or its script's res:// path as the path."
489
489
  )
490
490
  % [
@@ -631,7 +631,7 @@ func call_method(params: Dictionary) -> Dictionary:
631
631
  var _noted: bool = outside.append(stray)
632
632
 
633
633
  # Timed around the call alone, since the bridge's round trip is a second or more and swamps what
634
- # one click's work costs; the frame metrics read whichever frame is last, which is not this one.
634
+ # one click's work costs. The frame metrics read whichever frame is last, which is not this one.
635
635
  # Listened to as well, because a call the engine refused returns null like one that ran.
636
636
  var caught: GdhCallErrors = GdhCallErrors.new()
637
637
  OS.add_logger(caught)
@@ -639,7 +639,7 @@ func call_method(params: Dictionary) -> Dictionary:
639
639
  var result: Variant = holder.callv(named, deserialized_args)
640
640
  OS.remove_logger(caught)
641
641
  # A method that awaits hands back its state at the first await and its value only when it ends,
642
- # so the answer waits for the value; one that takes longer than the server waits is collected by
642
+ # so the answer waits for the value. One that takes longer than the server waits is collected by
643
643
  # its requestId like any slow call.
644
644
  var state: Object = result if result is Object else null
645
645
  var awaited: bool = state != null and state.get_class() == "GDScriptFunctionState"
@@ -660,7 +660,7 @@ func call_method(params: Dictionary) -> Dictionary:
660
660
  message += (
661
661
  (
662
662
  ". The parameter is declared as %s, which is also how the engine names a "
663
- + "script class with no class_name; if it is one, the object given has the "
663
+ + "script class with no class_name. If it is one, the object given has the "
664
664
  + "right class and another script: it is %s"
665
665
  )
666
666
  % checked
@@ -756,7 +756,7 @@ static func _reads_the_result(asked: Variant) -> bool:
756
756
  ##
757
757
  ## In the same answer because an object handed back is otherwise only its class and its instance
758
758
  ## id, and reading one field of it meant finding the same object again by a path into the game's
759
- ## private state, which works only where the caller already knows a key; a catalogue of records
759
+ ## private state, which works only where the caller already knows a key. A catalogue of records
760
760
  ## handed back as a list took one call per record. A name that is not there is listed rather than
761
761
  ## refusing the answer, since the call has run by then and its result is what was asked.
762
762
  func _read_off_result(result: Variant, asked: Variant) -> Dictionary:
@@ -809,7 +809,7 @@ func _read_off_result(result: Variant, asked: Variant) -> Dictionary:
809
809
 
810
810
 
811
811
  ## What was read off one element of a list or a dictionary a call answered with, or null for one
812
- ## that is no object; why a name was not there goes into [param missing], led by [param named].
812
+ ## that is no object. Why a name was not there goes into [param missing], led by [param named].
813
813
  func _read_off_element(
814
814
  element: Variant, named: String, asked: Variant, missing: Array[String]
815
815
  ) -> Variant:
@@ -900,7 +900,7 @@ static func _walk_from(object: Object, path: String) -> Dictionary:
900
900
  ## up to all of them, or any number past that for one taking varargs.
901
901
  static func _argument_count_refused(holder: Object, method: String, given: int) -> String:
902
902
  # A node's method list runs to hundreds of entries, and walking it costs a couple of
903
- # milliseconds of the game's frame; a call giving exactly the arguments the method declares
903
+ # milliseconds of the game's frame. A call giving exactly the arguments the method declares
904
904
  # needs no walk.
905
905
  if given == holder.get_method_argument_count(method):
906
906
  return ""
@@ -1,6 +1,6 @@
1
1
  extends RefCounted
2
2
  ## How long the game's last frames took, timed from the runtime's first frame. Apart from the
3
- ## queries that report it, because those are loaded at the first request and the frames before it
3
+ ## queries that report it, because those are compiled after that frame and the frames before them
4
4
  ## count too.
5
5
 
6
6
  ## How many frames the frame time is the mean of: two seconds at sixty frames a second.
@@ -39,8 +39,9 @@ func note_frame(now_usec: int) -> void:
39
39
  _last_physics_frames = stepped
40
40
 
41
41
 
42
- ## Leaves the frame in progress out of the timing, for one the runtime itself slowed: compiling a
43
- ## module at its first command takes up to half a second, which is gdharness's and not the game's,
44
- ## and would stand as the slowest frame for the next two seconds of readings.
42
+ ## Leaves the frame in progress out of the timing, for one the runtime itself slowed. A module a
43
+ ## command needs before its loading thread finished it is finished in the frame, which can take half
44
+ ## a second. That is gdharness's and not the game's, and would stand as the slowest frame for two
45
+ ## seconds of readings.
45
46
  func skip_this_frame() -> void:
46
47
  _last_frame_usec = 0
@@ -9,8 +9,7 @@ extends RefCounted
9
9
  ## it. An injected event never crosses anything, and on gdharness's own desktop no pointer ever
10
10
  ## enters a window, so nothing was hovered. A BaseButton takes a mouse press only while hovered, so
11
11
  ## every click on a button in such a window was dropped while the press itself was routed to it.
12
- ## Reported from Game C, whose strip window holds its first-run question, and traced through Godot
13
- ## 4.7.2's source.
12
+ ## Reported from Game C and traced through Godot 4.7.2's source.
14
13
  ##
15
14
  ## What is supplied is what the engine does once the window has been entered: the control its own
16
15
  ## search finds under the point, MOUSE_ENTER and mouse_entered on it and on each holder the pointer
@@ -45,7 +44,7 @@ func cannot_hover(window: Window) -> bool:
45
44
 
46
45
 
47
46
  ## Hovers what is under [param point], in [param viewport]'s own coordinates, when the engine could
48
- ## not; answers whether it did. Where the engine hovers by itself, any hover supplied before is
47
+ ## not. Answers whether it did. Where the engine hovers by itself, any hover supplied before is
49
48
  ## taken down first, so a control is never entered twice.
50
49
  func follow(viewport: Viewport, point: Vector2) -> bool:
51
50
  var window: Window = viewport as Window
@@ -246,7 +245,7 @@ func press_of(aim: Dictionary, viewport: Viewport, position: Vector2) -> Diction
246
245
 
247
246
  ## What a press at [param point], in [param viewport]'s own coordinates, goes to: the control the
248
247
  ## GUI picks, carried into a window embedded over the point and into what a SubViewportContainer
249
- ## shows, as the engine carries it; null when nothing there takes the pointer.
248
+ ## shows, as the engine carries it. Null when nothing there takes the pointer.
250
249
  func _pressed_at(viewport: Viewport, point: Vector2) -> Node:
251
250
  var windows: Array[Window] = viewport.get_embedded_subwindows()
252
251
  for index: int in range(windows.size() - 1, -1, -1):
@@ -20,7 +20,7 @@ const GdhSays = preload("runtime_says.gd")
20
20
  ## everything that process starts. Kept in step with `DESKTOP_VARIABLE` in src/desktop.ts.
21
21
  const DESKTOP_VARIABLE: String = "GDHARNESS_DESKTOP"
22
22
 
23
- ## Whether this run has had the pointer note in full; the game process is the run.
23
+ ## Whether this run has had the pointer note in full. The game process is the run.
24
24
  static var _pointer_explained: bool = false
25
25
  ## Whether this run has had the note on a supplied hover in full.
26
26
  static var _hover_explained: bool = false
@@ -76,7 +76,7 @@ static func _pointer_noted(answer: Dictionary) -> Dictionary:
76
76
  + "get_global_mouse_position() and get_local_mouse_position() on what is drawn there, "
77
77
  + "read a meaningless position rather than where this put it. What reads the event "
78
78
  + "itself gets this position: the control under the pointer, its hover and tooltip, "
79
- + "and the game's input handlers. Said once a run; pointer_on_hidden_desktop marks "
79
+ + "and the game's input handlers. Said once a run. pointer_on_hidden_desktop marks "
80
80
  + "every answer it applies to."
81
81
  )
82
82
  return answer
@@ -97,7 +97,7 @@ static func _hover_noted(answer: Dictionary, supplied: bool) -> Dictionary:
97
97
  + "engine would have: their mouse_entered and mouse_exited, and the hover a Button "
98
98
  + "presses on, and told the window the pointer is in it, so a motion reaches the "
99
99
  + "control under it. Viewport.gui_get_hovered_control() still answers null in this "
100
- + "window. Said once a run; hover_supplied marks every answer it applies to."
100
+ + "window. Said once a run. hover_supplied marks every answer it applies to."
101
101
  )
102
102
  return answer
103
103
 
@@ -539,7 +539,7 @@ func inject_mouse_drag(params: Dictionary) -> Dictionary:
539
539
  ## never pressed anything. The position is the control's centre carried into window pixels, so
540
540
  ## the caller never has to do that arithmetic.
541
541
  ##
542
- ## A control out of sight inside a ScrollContainer is scrolled to first; the answer says so under
542
+ ## A control out of sight inside a ScrollContainer is scrolled to first. The answer says so under
543
543
  ## `scrolled_into_view`, because the view having moved is a thing that happened to the screen and
544
544
  ## the caller is the only one who can tell whether that matters.
545
545
  ##
@@ -635,7 +635,7 @@ func click(params: Dictionary) -> Dictionary:
635
635
  #
636
636
  # Out of sight against what it is clipped to rather than against the viewport: a row scrolled
637
637
  # off the top of its container is inside the viewport, behind whatever is drawn up there, so
638
- # the click went to that instead and said so. Measured on a hall whose staff panel had been
638
+ # the click went to that instead and said so. Measured on a screen whose list had been
639
639
  # scrolled past: the button was at y 69 and its container started at y 166.
640
640
  var scrolled: bool = item_scrolled
641
641
  if not GdhScreen.in_sight(control, own, local):
@@ -745,7 +745,7 @@ func click(params: Dictionary) -> Dictionary:
745
745
  var _hearing: int = toggle.pressed.connect(heard_press)
746
746
  var press: InputEventMouseButton = await _click_button(viewport, position, button, double)
747
747
  # The release is what a button acts on, and a queue_free it causes lands at the end of
748
- # this frame; the frame passes so the answer describes the control as the click left it.
748
+ # this frame. The frame passes so the answer describes the control as the click left it.
749
749
  await _host.get_tree().process_frame
750
750
  if rich != null and is_instance_valid(rich) and rich.meta_clicked.is_connected(heard):
751
751
  rich.meta_clicked.disconnect(heard)
@@ -795,8 +795,8 @@ func click(params: Dictionary) -> Dictionary:
795
795
 
796
796
  ## Why a click at [param control]'s centre, or at the point [param aimed] names, would reach
797
797
  ## nothing, or "" when it would reach it: the point outside the viewport it is drawn in, at
798
- ## [param local]; outside the viewport showing that one on the screen, which [param reached] holds
799
- ## with the point there; or clipped away by a ScrollContainer holding it. A refusal names the
798
+ ## [param local], outside the viewport showing that one on the screen, which [param reached] holds
799
+ ## with the point there, or clipped away by a ScrollContainer holding it. A refusal names the
800
800
  ## viewport or container at fault and the point in that one's own space, since naming another
801
801
  ## printed a point inside the rect it was said to be outside. The note on a game with no window goes
802
802
  ## only on the viewport that is the window's.
@@ -162,7 +162,7 @@ static func _menu_title_size(menus: MenuBar, menu: int, style: StyleBox) -> Vect
162
162
 
163
163
 
164
164
  ## Scrolls [param control] until it draws [param item], and answers whether it asked anything to
165
- ## move. A tab bar and a tree have a call for it; a list is scrolled by its own bar, by as little as
165
+ ## move. A tab bar and a tree have a call for it. A list is scrolled by its own bar, by as little as
166
166
  ## brings the whole entry in.
167
167
  static func _scroll_to(control: Control, item: Variant) -> bool:
168
168
  var index: int = item if item is int else -1