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 Node
2
-
3
2
  ## The runtime autoload: a TCP server inside the running game that the gdharness server asks
4
3
  ## about the scene tree, drives with input, and captures. This file is the server and the
5
4
  ## command table; what each command does lives in a sibling module.
@@ -13,10 +12,8 @@ extends Node
13
12
  const GdhErrorReport = preload("error_report.gd")
14
13
  const GdhFrames = preload("runtime_frames.gd")
15
14
  const GdhRead = preload("reading.gd")
16
-
17
15
  ## Where the modules are loaded from. Kept in step with the markers below.
18
16
  const ADDON_DIR: String = "res://addons/gdharness_runtime/"
19
-
20
17
  ## The modules that answer commands, loaded at the first command one answers rather than preloaded.
21
18
  ## A preload compiles as this script parses, and the modules are twelve thousand lines: compiling
22
19
  ## them took most of a second of every debug start, in games nobody asks anything, a bench's quiet
@@ -26,11 +23,11 @@ const CHANGES: String = "runtime_changes.gd"
26
23
  const INPUT: String = "runtime_input.gd"
27
24
  const TYPING: String = "runtime_typing.gd"
28
25
  const PAD: String = "runtime_pad.gd"
26
+ const TOUCH: String = "runtime_touch.gd"
29
27
  const CAPTURE: String = "runtime_capture.gd"
30
28
  const WAITS: String = "runtime_waits.gd"
31
29
  const ACCESSIBILITY: String = "runtime_accessibility.gd"
32
30
  const VALUES: String = "runtime_values.gd"
33
-
34
31
  ## Every module command, by the name a request uses, as the module and the method that answers it.
35
32
  const MODULE_COMMANDS: Dictionary = {
36
33
  "get_tree": [QUERIES, "get_tree"],
@@ -52,6 +49,8 @@ const MODULE_COMMANDS: Dictionary = {
52
49
  "inject_mouse_drag": [INPUT, "inject_mouse_drag"],
53
50
  "inject_joypad_button": [PAD, "inject_joypad_button"],
54
51
  "inject_joypad_motion": [PAD, "inject_joypad_motion"],
52
+ "inject_touch": [TOUCH, "inject_touch"],
53
+ "inject_touch_drag": [TOUCH, "inject_touch_drag"],
55
54
  "click": [INPUT, "click"],
56
55
  "choose": [TYPING, "choose"],
57
56
  "wait_frames": [WAITS, "wait_frames"],
@@ -60,18 +59,15 @@ const MODULE_COMMANDS: Dictionary = {
60
59
  "watch_recorded": [WAITS, "recorded"],
61
60
  "wait_until": [WAITS, "wait_until"],
62
61
  }
63
-
64
62
  ## The size the engine gives a headless game's root viewport. Kept in step with
65
63
  ## `HEADLESS_VIEWPORT` in runtime_targets.gd, which this script does not load as it starts.
66
64
  const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
67
-
68
65
  const PROTOCOL: int = 3
69
66
  const DEFAULT_BIND_ADDRESS: String = "127.0.0.1"
70
67
  const BIND_ADDRESS_SETTING: String = "gdharness/runtime/bind_address"
71
68
  ## 0 asks the operating system for a free port, which is the default and what the server
72
69
  ## expects. A fixed port is for a client that cannot read the announcement.
73
70
  const PORT_SETTING: String = "gdharness/runtime/port"
74
-
75
71
  ## The debugger messages that confirm a profile's totals arrived: the game sends the first after
76
72
  ## them, and the debugger answers with the second once it has read that far. Kept in step with
77
73
  ## `PROFILE_SENT` and `PROFILE_RECEIVED` in src/script-profile.ts and the editor addon's
@@ -79,68 +75,53 @@ const PORT_SETTING: String = "gdharness/runtime/port"
79
75
  const PROFILE_CAPTURE: String = "gdharness"
80
76
  const PROFILE_SENT: String = "gdharness:profile_sent"
81
77
  const PROFILE_RECEIVED: String = "gdharness:profile_received"
82
-
83
78
  ## The longest a quitting game waits for that answer from a debugger that may not give one, such as
84
79
  ## an editor without the gdharness addon, which holds every profiled exit this long.
85
80
  const PROFILE_CONFIRM_MS: int = 3000
86
-
87
81
  ## The longest it waits for a debugger gdharness gave it, which answers once it has read that far:
88
82
  ## long enough for one far behind, and an end for one that has hung.
89
83
  const PROFILE_ANSWERED_MS: int = 60000
90
-
91
84
  ## How many times the empty function is called to measure what the profiler costs per call: enough
92
85
  ## that the clock's microsecond steps are lost in the sum, and a few milliseconds of one frame.
93
86
  const PROFILER_PROBE_CALLS: int = 10000
94
-
95
87
  ## How many calls of `EngineDebugger.line_poll` read the debugger's messages once: it reads them
96
88
  ## on every 2048th call and only counts the others.
97
89
  const POLLS_PER_READ: int = 2048
98
-
99
90
  ## The most frames a close waits for its answer to leave: a client that stopped reading would
100
91
  ## otherwise hold the close for ever, and the server is past caring about the answer by then.
101
92
  const CLOSE_AFTER_FRAMES: int = 30
102
-
103
93
  ## Set in a game's environment by whatever starts it to be profiled from its start: the server for
104
94
  ## a run it spawns, the editor for a play. Kept in step with `PROFILE_FROM_START` in the server.
105
95
  const PROFILE_FROM_START: String = "GDHARNESS_PROFILE_FROM_START"
106
-
107
96
  ## How long such a game waits for its debugger to switch the profiler on before carrying on.
108
97
  const PROFILER_WAIT_MS: int = 5000
109
-
110
98
  ## How the engine is told to run a script instead of the game: `godot -s thing.gd`.
111
99
  const SCRIPT_FLAGS: PackedStringArray = ["-s", "--script"]
112
-
113
100
  ## Serve a script run anyway, for somebody driving a `-s` script rather than a game.
114
101
  const SCRIPT_RUNS_SETTING: String = "gdharness/runtime/serve_script_runs"
115
-
116
102
  ## The `res://` folders of scenes that are not the game, such as a bench: a run started on one is
117
103
  ## not served. Kept in step with `QUIET_SCENES_SETTING` in src/setup.ts.
118
104
  const QUIET_SCENES_SETTING: String = "gdharness/runtime/quiet_scenes_under"
119
-
120
105
  ## What every user argument `gdharness spread` hands its engines starts with. Kept in step with
121
106
  ## the constants of GdhShare in addons/gdharness_tests/share.gd.
122
107
  const SPREAD_ARGUMENTS: String = "--gdh-share"
123
-
124
108
  ## What the engine takes a bare argument for as the scene to run, by its extension.
125
109
  const SCENE_EXTENSIONS: PackedStringArray = ["tscn", "scn", "escn", "res", "tres"]
126
-
127
110
  ## The variable the editor addon puts into its own environment, which every game the editor plays
128
111
  ## inherits: it says which editor played this game, so a server can tell the editor's game from
129
112
  ## another of the same project that some other server started. Kept in step with
130
113
  ## `EDITOR_PID_VARIABLE` in the editor addon's bridge client.
131
114
  const EDITOR_PID_VARIABLE: String = "GDHARNESS_EDITOR_PID"
132
-
133
115
  ## The directory the keeper holding this run reads, once the game has exited, to learn whether it
134
116
  ## set a restart with OS.set_restart_on_exit. The engine starts the process it restarts into after
135
- ## the tree has gone, and from outside that looks like a worker the game started in its last moments,
136
- ## so only the game can say which it was. Kept in step with `EXIT_NOTES_VARIABLE` in src/keeper.ts.
117
+ ## the tree has gone, and from outside that looks like a worker the game started in its last
118
+ ## moments, so only the game can say which it was. Kept in step with `EXIT_NOTES_VARIABLE` in
119
+ ## src/keeper.ts.
137
120
  const EXIT_NOTES_VARIABLE: String = "GDHARNESS_EXIT_NOTES"
138
-
139
121
  ## The file the engine's error reports go to, beside the announcement and named the same way,
140
122
  ## which is where the server reads them for a run the editor plays. Kept in step with
141
123
  ## `errorReportPath` in src/runtime-client.ts.
142
124
  const ERROR_REPORT_NAME: String = "runtime-%d.log"
143
-
144
125
  ## Written beside this addon by setup and upgrade. Kept in step with `VERSION_MARKER` and
145
126
  ## `DIGEST_MARKER` in src/setup.ts.
146
127
  const VERSION_MARKER: String = "res://addons/gdharness_runtime/.gdharness-version"
@@ -160,7 +141,6 @@ var _frames: GdhFrames = GdhFrames.new()
160
141
  ## rather than by reference, and a module referenced only by the Callable answering one command
161
142
  ## would be freed once it answered.
162
143
  var _modules: Dictionary = {}
163
-
164
144
  var _server: TCPServer
165
145
  var _clients: Array[StreamPeerTCP] = []
166
146
  ## Bytes received from each client that do not yet end in a newline, keyed by the peer.
@@ -204,6 +184,98 @@ func _init() -> void:
204
184
  _wait_for_the_profiler()
205
185
 
206
186
 
187
+ func _ready() -> void:
188
+ name = "GdharnessRuntime"
189
+ # The TCP control loop runs in _process. With the default PROCESS_MODE_INHERIT it stops
190
+ # while the tree is paused, so the runtime silently goes unreachable and the game cannot
191
+ # even be un-paused over the socket. A debug server has to stay responsive while the game
192
+ # is frozen, to inspect, capture, inject or resume it.
193
+ process_mode = Node.PROCESS_MODE_ALWAYS
194
+ _size_a_windowless_game()
195
+ var watched: int = get_tree().root.tree_exiting.connect(_send_the_profile)
196
+ if watched != OK:
197
+ push_error("gdharness could not watch for the game quitting: %d" % watched)
198
+ if not OS.get_environment(EXIT_NOTES_VARIABLE).is_empty():
199
+ var noted: int = get_tree().root.tree_exited.connect(_note_the_exit)
200
+ if noted != OK:
201
+ push_error("gdharness could not watch for the game's tree ending: %d" % noted)
202
+ _report_errors()
203
+ _start_server()
204
+
205
+
206
+ func _exit_tree() -> void:
207
+ if _errors != null:
208
+ OS.remove_logger(_errors)
209
+ _errors.close()
210
+ _errors = null
211
+ _cleanup()
212
+
213
+
214
+ func _process(_delta: float) -> void:
215
+ if not _enabled or _server == null:
216
+ return
217
+ _frames.note_frame(Time.get_ticks_usec())
218
+ _calibrate_the_profiler()
219
+
220
+ # Every connection waiting rather than one a frame: the engine queues eight a game has not taken
221
+ # and refuses the rest, so calls sent together would be refused as if the game were not up.
222
+ while _server.is_connection_available():
223
+ var client: StreamPeerTCP = _server.take_connection()
224
+ if client == null:
225
+ break
226
+ _clients.append(client)
227
+ _send_welcome(client)
228
+ # Out before the request that came with it is read: a request can hold the game in a
229
+ # breakpoint before the frame's drain, and the server, never welcomed, took a game held
230
+ # inside its call for one that had not taken it. A client gone already is dropped below.
231
+ var _welcomed: bool = _drain(client)
232
+
233
+ var gone: Array[StreamPeerTCP] = []
234
+ for client: StreamPeerTCP in _clients:
235
+ # 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.
237
+ var polled: Error = client.poll()
238
+ if polled != OK or client.get_status() != StreamPeerTCP.STATUS_CONNECTED:
239
+ gone.append(client)
240
+ continue
241
+ var available: int = client.get_available_bytes()
242
+ if available > 0:
243
+ var received: Array = client.get_data(available)
244
+ var bytes: PackedByteArray = received[1]
245
+ _receive(client, bytes)
246
+ if not _drain(client):
247
+ gone.append(client)
248
+
249
+ for client: StreamPeerTCP in gone:
250
+ _clients.erase(client)
251
+ # Dictionary.erase answers with whether the key was there, and underscore-prefixed locals
252
+ # are how GDScript says an answer is deliberately unwanted: a client that had nothing
253
+ # pending or owed is as forgotten as one that had both.
254
+ var _had_pending: bool = _pending.erase(client)
255
+ var _had_outgoing: bool = _outgoing.erase(client)
256
+
257
+ # After the answer to the close has gone out: the close request reaches this node too, which
258
+ # then drops its clients and whatever they were still owed.
259
+ if _close_asked:
260
+ _close_frames += 1
261
+ if _close_frames >= CLOSE_AFTER_FRAMES or _outgoing.values().all(_is_empty):
262
+ _close_asked = false
263
+ _deliver_close()
264
+
265
+
266
+ ## Down with the window only when the close is taken. A game that asks before quitting turns
267
+ ## auto_accept_quit off and decides for itself, and one whose player answered No went on running
268
+ ## with no runtime and no announcement, so every later call said no game was running. A quit it
269
+ ## does take ends the tree, which [method _exit_tree] cleans up after.
270
+ ##
271
+ ## One line for every notification, and the last this node runs in a frame, after [method _process]:
272
+ ## a pause that stops in this addon is let go to a breakpoint here and stepped once into the game's
273
+ ## own script, rather than stepped through the frame a line at a time.
274
+ func _notification(what: int) -> void:
275
+ if what == NOTIFICATION_WM_CLOSE_REQUEST and _takes_the_close():
276
+ _cleanup()
277
+
278
+
207
279
  ## The script of the module in [param file], compiled at the first call that needs it.
208
280
  static func _loaded(file: String) -> GDScript:
209
281
  return load(ADDON_DIR + file) as GDScript
@@ -218,7 +290,7 @@ func _module(file: String) -> Object:
218
290
  match file:
219
291
  QUERIES:
220
292
  made = script.new(self, values, _frames)
221
- TYPING, PAD, ACCESSIBILITY:
293
+ TYPING, PAD, TOUCH, ACCESSIBILITY:
222
294
  made = script.new(self)
223
295
  _:
224
296
  made = script.new(self, values)
@@ -277,9 +349,10 @@ static func _window_dimension(axis: String, fallback: int) -> int:
277
349
 
278
350
  ## Holds the game's start until the profiler is on, when whatever started the game asked for a
279
351
  ## profile from its start. The debugger switches the profiler on with a message the game reads only
280
- ## on a poll, which comes every [constant POLLS_PER_READ] lines of script, so the main scene's _ready
281
- ## could run first: a game played on a macOS runner counted 25 calls of a function called 26 times,
282
- ## the first of them from _ready. Run at the autoload's construction, before the main scene loads.
352
+ ## on a poll, which comes every [constant POLLS_PER_READ] lines of script, so the main scene's
353
+ ## _ready could run first: a game played on a macOS runner counted 25 calls of a function called 26
354
+ ## times, the first of them from _ready. Run at the autoload's construction, before the main scene
355
+ ## loads.
283
356
  func _wait_for_the_profiler() -> void:
284
357
  if OS.get_environment(PROFILE_FROM_START).is_empty():
285
358
  return
@@ -295,8 +368,8 @@ func _wait_for_the_profiler() -> void:
295
368
  push_warning(
296
369
  (
297
370
  (
298
- "gdharness: this game was started to be profiled from its start, and the profiler"
299
- + " was not on %d ms later, so what ran before it is not counted"
371
+ "gdharness: this game was started to be profiled from its start, and the "
372
+ + "profiler was not on %d ms later, so what ran before it is not counted"
300
373
  )
301
374
  % PROFILER_WAIT_MS
302
375
  )
@@ -315,44 +388,20 @@ func serving() -> bool:
315
388
  return _enabled and _server != null and _server.is_listening()
316
389
 
317
390
 
318
- func _ready() -> void:
319
- name = "GdharnessRuntime"
320
- # The TCP control loop runs in _process. With the default PROCESS_MODE_INHERIT it stops
321
- # while the tree is paused, so the runtime silently goes unreachable and the game cannot
322
- # even be un-paused over the socket. A debug server has to stay responsive while the game
323
- # is frozen, to inspect, capture, inject or resume it.
324
- process_mode = Node.PROCESS_MODE_ALWAYS
325
- _size_a_windowless_game()
326
- var watched: int = get_tree().root.tree_exiting.connect(_send_the_profile)
327
- if watched != OK:
328
- push_error("gdharness could not watch for the game quitting: %d" % watched)
329
- if not OS.get_environment(EXIT_NOTES_VARIABLE).is_empty():
330
- var noted: int = get_tree().root.tree_exited.connect(_note_the_exit)
331
- if noted != OK:
332
- push_error("gdharness could not watch for the game's tree ending: %d" % noted)
333
- _report_errors()
334
- _start_server()
335
-
336
-
337
391
  ## Says whether this game restarts as it exits, once the whole tree has gone, so a game's own
338
392
  ## _exit_tree and every autoload's have had their say.
339
393
  func _note_the_exit() -> void:
340
- var path: String = OS.get_environment(EXIT_NOTES_VARIABLE).path_join("exit-%d.json" % OS.get_process_id())
394
+ var path: String = OS.get_environment(EXIT_NOTES_VARIABLE).path_join(
395
+ "exit-%d.json" % OS.get_process_id()
396
+ )
341
397
  var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
342
398
  if file == null:
343
399
  return
344
- var _written: bool = file.store_string(JSON.stringify({"restart": OS.is_restart_on_exit_set()}))
400
+ var state: String = JSON.stringify({ "restart": OS.is_restart_on_exit_set() })
401
+ var _written: bool = file.store_string(state)
345
402
  file.close()
346
403
 
347
404
 
348
- func _exit_tree() -> void:
349
- if _errors != null:
350
- OS.remove_logger(_errors)
351
- _errors.close()
352
- _errors = null
353
- _cleanup()
354
-
355
-
356
405
  ## A profiled run's totals, sent as the game quits.
357
406
  ##
358
407
  ## The engine sends its script profiler's totals only when the profiler is switched off, and at
@@ -378,13 +427,16 @@ func _exit_tree() -> void:
378
427
  ## The debugger is asked once before the profiler goes off as well, so the totals go into an empty
379
428
  ## queue. They name each function no frame did, one message apiece, and with the names of a large
380
429
  ## first frame still queued for a debugger behind on its reading, they did not fit and the engine
381
- ## dropped them. One that does not answer that is not asked again, as it would not answer the second.
430
+ ## dropped them. One that does not answer that is not asked again, as it would not answer the
431
+ ## second.
382
432
  func _send_the_profile() -> void:
383
433
  if not (EngineDebugger.is_active() and EngineDebugger.is_profiling("servers")):
384
434
  return
385
435
  if not EngineDebugger.has_capture(PROFILE_CAPTURE):
386
436
  EngineDebugger.register_message_capture(PROFILE_CAPTURE, _on_debugger_message)
387
- var answers: bool = _profiled_from_start or OS.get_environment(EDITOR_PID_VARIABLE).is_valid_int()
437
+ var answers: bool = (
438
+ _profiled_from_start or OS.get_environment(EDITOR_PID_VARIABLE).is_valid_int()
439
+ )
388
440
  var waits: int = PROFILE_ANSWERED_MS if answers else PROFILE_CONFIRM_MS
389
441
  var drained: bool = _debugger_answered(waits)
390
442
  EngineDebugger.profiler_enable("servers", false)
@@ -394,8 +446,8 @@ func _send_the_profile() -> void:
394
446
  print(
395
447
  (
396
448
  (
397
- "[gdharness] the debugger did not confirm the profiler's totals within %d ms, so the"
398
- + " profile may be the frames that arrived without them"
449
+ "[gdharness] the debugger did not confirm the profiler's totals within %d ms, "
450
+ + "so the profile may be the frames that arrived without them"
399
451
  )
400
452
  % waits
401
453
  )
@@ -420,9 +472,9 @@ func _debugger_answered(waits: int) -> bool:
420
472
  ## summed. Nothing of the game's own runs, which quitting the tree would have done.
421
473
  func _send_profile_now(_params: Dictionary) -> Dictionary:
422
474
  if not (EngineDebugger.is_active() and EngineDebugger.is_profiling("servers")):
423
- return {"sent": false}
475
+ return { "sent": false }
424
476
  _send_the_profile()
425
- return {"sent": true, "confirmed": _profile_received}
477
+ return { "sent": true, "confirmed": _profile_received }
426
478
 
427
479
 
428
480
  ## What the script profiler costs per call in this game, measured once each time it is switched on.
@@ -476,58 +528,6 @@ func _report_errors() -> void:
476
528
  OS.add_logger(_errors)
477
529
 
478
530
 
479
- func _process(_delta: float) -> void:
480
- if not _enabled or _server == null:
481
- return
482
- _frames.note_frame(Time.get_ticks_usec())
483
- _calibrate_the_profiler()
484
-
485
- # Every connection waiting rather than one a frame: the engine queues eight a game has not
486
- # taken and refuses the rest, so calls sent together would be refused as if the game were not up.
487
- while _server.is_connection_available():
488
- var client: StreamPeerTCP = _server.take_connection()
489
- if client == null:
490
- break
491
- _clients.append(client)
492
- _send_welcome(client)
493
- # Out before the request that came with it is read: a request can hold the game in a
494
- # breakpoint before the frame's drain, and the server, never welcomed, took a game held
495
- # inside its call for one that had not taken it. A client gone already is dropped below.
496
- var _welcomed: bool = _drain(client)
497
-
498
- var gone: Array[StreamPeerTCP] = []
499
- for client: StreamPeerTCP in _clients:
500
- # A poll that fails is a client that has already gone, which the status check below is
501
- # about to find; the answer is read so it is not dropped, not because it adds anything.
502
- var polled: Error = client.poll()
503
- if polled != OK or client.get_status() != StreamPeerTCP.STATUS_CONNECTED:
504
- gone.append(client)
505
- continue
506
- var available: int = client.get_available_bytes()
507
- if available > 0:
508
- var received: Array = client.get_data(available)
509
- var bytes: PackedByteArray = received[1]
510
- _receive(client, bytes)
511
- if not _drain(client):
512
- gone.append(client)
513
-
514
- for client: StreamPeerTCP in gone:
515
- _clients.erase(client)
516
- # Dictionary.erase answers with whether the key was there, and underscore-prefixed locals
517
- # are how GDScript says an answer is deliberately unwanted: a client that had nothing
518
- # pending or owed is as forgotten as one that had both.
519
- var _had_pending: bool = _pending.erase(client)
520
- var _had_outgoing: bool = _outgoing.erase(client)
521
-
522
- # After the answer to the close has gone out: the close request reaches this node too, which then
523
- # drops its clients and whatever they were still owed.
524
- if _close_asked:
525
- _close_frames += 1
526
- if _close_frames >= CLOSE_AFTER_FRAMES or _outgoing.values().all(_is_empty):
527
- _close_asked = false
528
- _deliver_close()
529
-
530
-
531
531
  ## Hands the socket as much of what it is owed as it will take, and says whether it is still worth
532
532
  ## talking to. Reading comes first in the frame, so a client that asked and left is noticed here
533
533
  ## rather than blocking on a write that can never finish.
@@ -572,7 +572,10 @@ func _start_server() -> void:
572
572
  # own path: a gate that starts sixteen engines at once announces sixteen games that are not
573
573
  # games, and a client asking the runtime anything while they run can be answered by whichever
574
574
  # of them replies first. Measured on two projects before it was written here.
575
- if _script_run() and not GdhRead.as_bool(ProjectSettings.get_setting(SCRIPT_RUNS_SETTING, false)):
575
+ if (
576
+ _script_run()
577
+ and not GdhRead.as_bool(ProjectSettings.get_setting(SCRIPT_RUNS_SETTING, false))
578
+ ):
576
579
  _enabled = false
577
580
  return
578
581
 
@@ -593,14 +596,19 @@ func _start_server() -> void:
593
596
 
594
597
  _server = TCPServer.new()
595
598
  # listen() defaults bind_address to "*", which exposes the game to the whole network.
596
- var bind_address: String = str(ProjectSettings.get_setting(BIND_ADDRESS_SETTING, DEFAULT_BIND_ADDRESS))
599
+ var bind_address: String = str(
600
+ ProjectSettings.get_setting(BIND_ADDRESS_SETTING, DEFAULT_BIND_ADDRESS)
601
+ )
597
602
  var wanted_port: int = GdhRead.as_int(ProjectSettings.get_setting(PORT_SETTING, 0))
598
603
  var error: Error = _server.listen(wanted_port, bind_address)
599
604
  if error != OK:
600
605
  # A warning, not an error: callers treat any ERROR line on stderr as a failed
601
606
  # operation, and a game without a runtime server is a handled condition.
602
607
  push_warning(
603
- "[gdharness] runtime port %d is unavailable (%s), running without a server" % [wanted_port, error]
608
+ (
609
+ "[gdharness] runtime port %d is unavailable (%s), running without a server"
610
+ % [wanted_port, error]
611
+ )
604
612
  )
605
613
  _enabled = false
606
614
  return
@@ -608,7 +616,8 @@ func _start_server() -> void:
608
616
  _port = _server.get_local_port()
609
617
  _announce(bind_address)
610
618
  var listening: String = (
611
- "[gdharness] runtime listening on %s:%d, announced at %s" % [bind_address, _port, _announcement]
619
+ "[gdharness] runtime listening on %s:%d, announced at %s"
620
+ % [bind_address, _port, _announcement]
612
621
  )
613
622
  # The line the server tells the two streams apart by: it takes the prints before it from the
614
623
  # debug adapter and everything from it on from the file, which has it first. After the
@@ -619,8 +628,8 @@ func _start_server() -> void:
619
628
  print(listening)
620
629
 
621
630
 
622
- ## The scene [param given] names for the engine to run, as a `res://` path, when it lies under one of
623
- ## [param prefixes]; otherwise empty. The engine takes any bare argument ending in a scene's
631
+ ## 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
624
633
  ## extension for the scene, relative to the project or absolute, so each is read as it would.
625
634
  static func quiet_scene_in(given: PackedStringArray, prefixes: Variant) -> String:
626
635
  # A PackedStringArray as project.godot writes one, or an Array of strings as a hand might.
@@ -633,7 +642,10 @@ static func quiet_scene_in(given: PackedStringArray, prefixes: Variant) -> Strin
633
642
  if prefix is String:
634
643
  var _kept: bool = folders.append(str(prefix))
635
644
  for argument: String in given:
636
- if argument.begins_with("-") or not SCENE_EXTENSIONS.has(argument.get_extension().to_lower()):
645
+ if (
646
+ argument.begins_with("-")
647
+ or not SCENE_EXTENSIONS.has(argument.get_extension().to_lower())
648
+ ):
637
649
  continue
638
650
  var scene: String = ProjectSettings.localize_path(argument)
639
651
  for folder: String in folders:
@@ -676,7 +688,10 @@ func _announce(bind_address: String) -> void:
676
688
  var made: Error = DirAccess.make_dir_recursive_absolute(directory)
677
689
  if made != OK and made != ERR_ALREADY_EXISTS:
678
690
  push_warning(
679
- "[gdharness] cannot create %s (%s); the server will not find this game" % [directory, made]
691
+ (
692
+ "[gdharness] cannot create %s (%s); the server will not find this game"
693
+ % [directory, made]
694
+ )
680
695
  )
681
696
  return
682
697
  var path: String = directory.path_join("runtime-%d.json" % OS.get_process_id())
@@ -731,7 +746,7 @@ func _identity(bind_address: String) -> Dictionary:
731
746
  "project":
732
747
  {
733
748
  "name": str(ProjectSettings.get_setting("application/config/name", "")),
734
- "path": ProjectSettings.globalize_path("res://").rstrip("/")
749
+ "path": ProjectSettings.globalize_path("res://").rstrip("/"),
735
750
  },
736
751
  "godot": Engine.get_version_info().get("string", ""),
737
752
  # Where user:// is for this run, which only the engine can say: it follows the project's
@@ -804,7 +819,7 @@ func _execute_command(command: String, params: Dictionary) -> Dictionary:
804
819
  if not handler.is_valid():
805
820
  return {
806
821
  "type": "error",
807
- "message": "Unknown command: %s. Commands: %s" % [command, ", ".join(_command_names())]
822
+ "message": "Unknown command: %s. Commands: %s" % [command, ", ".join(_command_names())],
808
823
  }
809
824
  # Only a waits module already loaded can have a trace running to record the call in.
810
825
  var waits: Object = _modules.get(WAITS)
@@ -815,7 +830,7 @@ func _execute_command(command: String, params: Dictionary) -> Dictionary:
815
830
 
816
831
 
817
832
  func _ping(_params: Dictionary) -> Dictionary:
818
- return {"type": "pong", "timestamp": Time.get_unix_time_from_system()}
833
+ return { "type": "pong", "timestamp": Time.get_unix_time_from_system() }
819
834
 
820
835
 
821
836
  ## The game asked to close, as a player's close asks it: answered at once and delivered once the
@@ -824,12 +839,12 @@ func _ping(_params: Dictionary) -> Dictionary:
824
839
  func _close_game(_params: Dictionary) -> Dictionary:
825
840
  _close_asked = true
826
841
  _close_frames = 0
827
- return {"type": "closing", "acceptsQuit": get_tree().auto_accept_quit}
842
+ return { "type": "closing", "acceptsQuit": get_tree().auto_accept_quit }
828
843
 
829
844
 
830
845
  ## What a player's close does: the close request reaches every node, and then the root window says
831
- ## it was asked to close, which the tree answers by quitting when it accepts quits. So the game's own
832
- ## close runs, a crash guard's included, where a stop that ended the process ran none of it.
846
+ ## it was asked to close, which the tree answers by quitting when it accepts quits. So the game's
847
+ ## own close runs, a crash guard's included, where a stop that ended the process ran none of it.
833
848
  func _deliver_close() -> void:
834
849
  var root: Window = get_tree().root
835
850
  root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST)
@@ -856,20 +871,7 @@ func _send_response(client: StreamPeerTCP, data: Dictionary) -> void:
856
871
 
857
872
  ## A request that could not be read far enough to find its id is answered with a null one.
858
873
  func _send_error(client: StreamPeerTCP, request_id: Variant, message: String) -> void:
859
- _send_response(client, {"type": "error", "message": message, "id": request_id})
860
-
861
-
862
- ## Down with the window only when the close is taken. A game that asks before quitting turns
863
- ## auto_accept_quit off and decides for itself, and one whose player answered No went on running
864
- ## with no runtime and no announcement, so every later call said no game was running. A quit it
865
- ## does take ends the tree, which [method _exit_tree] cleans up after.
866
- ##
867
- ## One line for every notification, and the last this node runs in a frame, after [method _process]:
868
- ## a pause that stops in this addon is let go to a breakpoint here and stepped once into the game's
869
- ## own script, rather than stepped through the frame a line at a time.
870
- func _notification(what: int) -> void:
871
- if what == NOTIFICATION_WM_CLOSE_REQUEST and _takes_the_close():
872
- _cleanup()
874
+ _send_response(client, { "type": "error", "message": message, "id": request_id })
873
875
 
874
876
 
875
877
  func _takes_the_close() -> bool:
@@ -893,6 +895,9 @@ func _cleanup() -> void:
893
895
  var removed: Error = DirAccess.remove_absolute(_announcement)
894
896
  if removed != OK:
895
897
  push_warning(
896
- "[gdharness] cannot remove %s; the server will find a game that is gone" % _announcement
898
+ (
899
+ "[gdharness] cannot remove %s; the server will find a game that is gone"
900
+ % _announcement
901
+ )
897
902
  )
898
903
  _announcement = ""