gdharness 1.3.42 → 1.3.44

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 (106) hide show
  1. package/build/cli.js +383 -243
  2. package/build/godot/addons/auto_reload/auto_reload.gd +18 -19
  3. package/build/godot/addons/gdharness_editor/animation_reads.gd +55 -33
  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 +83 -42
  7. package/build/godot/addons/gdharness_editor/file_walk.gd +12 -5
  8. package/build/godot/addons/gdharness_editor/headless_check.gd +68 -44
  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 +51 -17
  13. package/build/godot/addons/gdharness_editor/same_values.gd +4 -4
  14. package/build/godot/addons/gdharness_editor/scene_file.gd +127 -80
  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 +37 -27
  20. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +79 -49
  21. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +70 -40
  22. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +65 -42
  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 +166 -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 +68 -41
  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 +12 -5
  33. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +113 -67
  34. package/build/godot/addons/gdharness_editor/tools/script_paths.gd +20 -8
  35. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +11 -8
  36. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +47 -34
  37. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +77 -50
  38. package/build/godot/addons/gdharness_editor/value_shapes.gd +5 -6
  39. package/build/godot/addons/gdharness_runtime/call_errors.gd +1 -2
  40. package/build/godot/addons/gdharness_runtime/declared_values.gd +68 -32
  41. package/build/godot/addons/gdharness_runtime/error_report.gd +1 -2
  42. package/build/godot/addons/gdharness_runtime/reading.gd +51 -17
  43. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +84 -63
  44. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +149 -147
  45. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +64 -42
  46. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +184 -122
  47. package/build/godot/addons/gdharness_runtime/runtime_frames.gd +2 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +24 -20
  49. package/build/godot/addons/gdharness_runtime/runtime_input.gd +134 -92
  50. package/build/godot/addons/gdharness_runtime/runtime_items.gd +29 -13
  51. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +0 -1
  52. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +34 -17
  53. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +17 -13
  54. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +84 -46
  55. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +117 -79
  56. package/build/godot/addons/gdharness_runtime/runtime_says.gd +14 -17
  57. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +23 -16
  58. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +117 -66
  59. package/build/godot/addons/gdharness_runtime/runtime_touch.gd +15 -15
  60. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +44 -27
  61. package/build/godot/addons/gdharness_runtime/runtime_values.gd +108 -73
  62. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +111 -84
  63. package/build/godot/addons/gdharness_runtime/runtime_words.gd +10 -7
  64. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -51
  65. package/build/godot/addons/gdharness_runtime/value_shapes.gd +5 -6
  66. package/build/godot/addons/gdharness_tests/bench.gd +4 -5
  67. package/build/godot/addons/gdharness_tests/event.gd +10 -5
  68. package/build/godot/addons/gdharness_tests/share.gd +38 -18
  69. package/build/godot/addons/gdharness_tests/suites.gd +3 -3
  70. package/build/godot/addons/gdharness_tests/wait.gd +3 -2
  71. package/build/godot/loader/loader.gd +9 -9
  72. package/build/godot/operations/animation_reads.gd +55 -33
  73. package/build/godot/operations/audio_buses.gd +48 -19
  74. package/build/godot/operations/class_cache.gd +16 -11
  75. package/build/godot/operations/classdb_queries.gd +89 -44
  76. package/build/godot/operations/declared_values.gd +68 -32
  77. package/build/godot/operations/dependencies.gd +190 -109
  78. package/build/godot/operations/editor_tools.gd +10 -10
  79. package/build/godot/operations/export_presets.gd +100 -54
  80. package/build/godot/operations/file_reads.gd +83 -42
  81. package/build/godot/operations/file_walk.gd +12 -5
  82. package/build/godot/operations/gdscript_analysis.gd +86 -65
  83. package/build/godot/operations/gdscript_authoring.gd +83 -36
  84. package/build/godot/operations/godot_operations.gd +17 -11
  85. package/build/godot/operations/import_pipeline.gd +110 -59
  86. package/build/godot/operations/input_actions.gd +62 -39
  87. package/build/godot/operations/patterns.gd +3 -1
  88. package/build/godot/operations/plugins.gd +36 -16
  89. package/build/godot/operations/project_config.gd +61 -32
  90. package/build/godot/operations/project_diagnostics.gd +24 -11
  91. package/build/godot/operations/project_file.gd +16 -16
  92. package/build/godot/operations/project_validation.gd +71 -44
  93. package/build/godot/operations/property_paths.gd +22 -12
  94. package/build/godot/operations/property_values.gd +143 -78
  95. package/build/godot/operations/reading.gd +51 -17
  96. package/build/godot/operations/rename_checks.gd +2 -2
  97. package/build/godot/operations/resource_files.gd +11 -9
  98. package/build/godot/operations/same_values.gd +4 -4
  99. package/build/godot/operations/scene_connections.gd +27 -13
  100. package/build/godot/operations/script_check.gd +52 -49
  101. package/build/godot/operations/serialisation.gd +75 -51
  102. package/build/godot/operations/setting_reads.gd +7 -6
  103. package/build/godot/operations/value_shapes.gd +5 -6
  104. package/build/godot/tests/listed.gd +321 -268
  105. package/build/keeper.js +43 -43
  106. 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
@@ -31,7 +28,6 @@ const CAPTURE: String = "runtime_capture.gd"
31
28
  const WAITS: String = "runtime_waits.gd"
32
29
  const ACCESSIBILITY: String = "runtime_accessibility.gd"
33
30
  const VALUES: String = "runtime_values.gd"
34
-
35
31
  ## Every module command, by the name a request uses, as the module and the method that answers it.
36
32
  const MODULE_COMMANDS: Dictionary = {
37
33
  "get_tree": [QUERIES, "get_tree"],
@@ -63,18 +59,15 @@ const MODULE_COMMANDS: Dictionary = {
63
59
  "watch_recorded": [WAITS, "recorded"],
64
60
  "wait_until": [WAITS, "wait_until"],
65
61
  }
66
-
67
62
  ## The size the engine gives a headless game's root viewport. Kept in step with
68
63
  ## `HEADLESS_VIEWPORT` in runtime_targets.gd, which this script does not load as it starts.
69
64
  const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
70
-
71
65
  const PROTOCOL: int = 3
72
66
  const DEFAULT_BIND_ADDRESS: String = "127.0.0.1"
73
67
  const BIND_ADDRESS_SETTING: String = "gdharness/runtime/bind_address"
74
68
  ## 0 asks the operating system for a free port, which is the default and what the server
75
69
  ## expects. A fixed port is for a client that cannot read the announcement.
76
70
  const PORT_SETTING: String = "gdharness/runtime/port"
77
-
78
71
  ## The debugger messages that confirm a profile's totals arrived: the game sends the first after
79
72
  ## them, and the debugger answers with the second once it has read that far. Kept in step with
80
73
  ## `PROFILE_SENT` and `PROFILE_RECEIVED` in src/script-profile.ts and the editor addon's
@@ -82,68 +75,53 @@ const PORT_SETTING: String = "gdharness/runtime/port"
82
75
  const PROFILE_CAPTURE: String = "gdharness"
83
76
  const PROFILE_SENT: String = "gdharness:profile_sent"
84
77
  const PROFILE_RECEIVED: String = "gdharness:profile_received"
85
-
86
78
  ## The longest a quitting game waits for that answer from a debugger that may not give one, such as
87
79
  ## an editor without the gdharness addon, which holds every profiled exit this long.
88
80
  const PROFILE_CONFIRM_MS: int = 3000
89
-
90
81
  ## The longest it waits for a debugger gdharness gave it, which answers once it has read that far:
91
82
  ## long enough for one far behind, and an end for one that has hung.
92
83
  const PROFILE_ANSWERED_MS: int = 60000
93
-
94
84
  ## How many times the empty function is called to measure what the profiler costs per call: enough
95
85
  ## that the clock's microsecond steps are lost in the sum, and a few milliseconds of one frame.
96
86
  const PROFILER_PROBE_CALLS: int = 10000
97
-
98
87
  ## How many calls of `EngineDebugger.line_poll` read the debugger's messages once: it reads them
99
88
  ## on every 2048th call and only counts the others.
100
89
  const POLLS_PER_READ: int = 2048
101
-
102
90
  ## The most frames a close waits for its answer to leave: a client that stopped reading would
103
91
  ## otherwise hold the close for ever, and the server is past caring about the answer by then.
104
92
  const CLOSE_AFTER_FRAMES: int = 30
105
-
106
93
  ## Set in a game's environment by whatever starts it to be profiled from its start: the server for
107
94
  ## a run it spawns, the editor for a play. Kept in step with `PROFILE_FROM_START` in the server.
108
95
  const PROFILE_FROM_START: String = "GDHARNESS_PROFILE_FROM_START"
109
-
110
96
  ## How long such a game waits for its debugger to switch the profiler on before carrying on.
111
97
  const PROFILER_WAIT_MS: int = 5000
112
-
113
98
  ## How the engine is told to run a script instead of the game: `godot -s thing.gd`.
114
99
  const SCRIPT_FLAGS: PackedStringArray = ["-s", "--script"]
115
-
116
100
  ## Serve a script run anyway, for somebody driving a `-s` script rather than a game.
117
101
  const SCRIPT_RUNS_SETTING: String = "gdharness/runtime/serve_script_runs"
118
-
119
102
  ## The `res://` folders of scenes that are not the game, such as a bench: a run started on one is
120
103
  ## not served. Kept in step with `QUIET_SCENES_SETTING` in src/setup.ts.
121
104
  const QUIET_SCENES_SETTING: String = "gdharness/runtime/quiet_scenes_under"
122
-
123
105
  ## What every user argument `gdharness spread` hands its engines starts with. Kept in step with
124
106
  ## the constants of GdhShare in addons/gdharness_tests/share.gd.
125
107
  const SPREAD_ARGUMENTS: String = "--gdh-share"
126
-
127
108
  ## What the engine takes a bare argument for as the scene to run, by its extension.
128
109
  const SCENE_EXTENSIONS: PackedStringArray = ["tscn", "scn", "escn", "res", "tres"]
129
-
130
110
  ## The variable the editor addon puts into its own environment, which every game the editor plays
131
111
  ## inherits: it says which editor played this game, so a server can tell the editor's game from
132
112
  ## another of the same project that some other server started. Kept in step with
133
113
  ## `EDITOR_PID_VARIABLE` in the editor addon's bridge client.
134
114
  const EDITOR_PID_VARIABLE: String = "GDHARNESS_EDITOR_PID"
135
-
136
115
  ## The directory the keeper holding this run reads, once the game has exited, to learn whether it
137
116
  ## set a restart with OS.set_restart_on_exit. The engine starts the process it restarts into after
138
- ## the tree has gone, and from outside that looks like a worker the game started in its last moments,
139
- ## 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.
140
120
  const EXIT_NOTES_VARIABLE: String = "GDHARNESS_EXIT_NOTES"
141
-
142
121
  ## The file the engine's error reports go to, beside the announcement and named the same way,
143
122
  ## which is where the server reads them for a run the editor plays. Kept in step with
144
123
  ## `errorReportPath` in src/runtime-client.ts.
145
124
  const ERROR_REPORT_NAME: String = "runtime-%d.log"
146
-
147
125
  ## Written beside this addon by setup and upgrade. Kept in step with `VERSION_MARKER` and
148
126
  ## `DIGEST_MARKER` in src/setup.ts.
149
127
  const VERSION_MARKER: String = "res://addons/gdharness_runtime/.gdharness-version"
@@ -163,7 +141,6 @@ var _frames: GdhFrames = GdhFrames.new()
163
141
  ## rather than by reference, and a module referenced only by the Callable answering one command
164
142
  ## would be freed once it answered.
165
143
  var _modules: Dictionary = {}
166
-
167
144
  var _server: TCPServer
168
145
  var _clients: Array[StreamPeerTCP] = []
169
146
  ## Bytes received from each client that do not yet end in a newline, keyed by the peer.
@@ -207,6 +184,98 @@ func _init() -> void:
207
184
  _wait_for_the_profiler()
208
185
 
209
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
+
210
279
  ## The script of the module in [param file], compiled at the first call that needs it.
211
280
  static func _loaded(file: String) -> GDScript:
212
281
  return load(ADDON_DIR + file) as GDScript
@@ -280,9 +349,10 @@ static func _window_dimension(axis: String, fallback: int) -> int:
280
349
 
281
350
  ## Holds the game's start until the profiler is on, when whatever started the game asked for a
282
351
  ## profile from its start. The debugger switches the profiler on with a message the game reads only
283
- ## on a poll, which comes every [constant POLLS_PER_READ] lines of script, so the main scene's _ready
284
- ## could run first: a game played on a macOS runner counted 25 calls of a function called 26 times,
285
- ## 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.
286
356
  func _wait_for_the_profiler() -> void:
287
357
  if OS.get_environment(PROFILE_FROM_START).is_empty():
288
358
  return
@@ -298,8 +368,8 @@ func _wait_for_the_profiler() -> void:
298
368
  push_warning(
299
369
  (
300
370
  (
301
- "gdharness: this game was started to be profiled from its start, and the profiler"
302
- + " 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"
303
373
  )
304
374
  % PROFILER_WAIT_MS
305
375
  )
@@ -318,44 +388,20 @@ func serving() -> bool:
318
388
  return _enabled and _server != null and _server.is_listening()
319
389
 
320
390
 
321
- func _ready() -> void:
322
- name = "GdharnessRuntime"
323
- # The TCP control loop runs in _process. With the default PROCESS_MODE_INHERIT it stops
324
- # while the tree is paused, so the runtime silently goes unreachable and the game cannot
325
- # even be un-paused over the socket. A debug server has to stay responsive while the game
326
- # is frozen, to inspect, capture, inject or resume it.
327
- process_mode = Node.PROCESS_MODE_ALWAYS
328
- _size_a_windowless_game()
329
- var watched: int = get_tree().root.tree_exiting.connect(_send_the_profile)
330
- if watched != OK:
331
- push_error("gdharness could not watch for the game quitting: %d" % watched)
332
- if not OS.get_environment(EXIT_NOTES_VARIABLE).is_empty():
333
- var noted: int = get_tree().root.tree_exited.connect(_note_the_exit)
334
- if noted != OK:
335
- push_error("gdharness could not watch for the game's tree ending: %d" % noted)
336
- _report_errors()
337
- _start_server()
338
-
339
-
340
391
  ## Says whether this game restarts as it exits, once the whole tree has gone, so a game's own
341
392
  ## _exit_tree and every autoload's have had their say.
342
393
  func _note_the_exit() -> void:
343
- 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
+ )
344
397
  var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
345
398
  if file == null:
346
399
  return
347
- 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)
348
402
  file.close()
349
403
 
350
404
 
351
- func _exit_tree() -> void:
352
- if _errors != null:
353
- OS.remove_logger(_errors)
354
- _errors.close()
355
- _errors = null
356
- _cleanup()
357
-
358
-
359
405
  ## A profiled run's totals, sent as the game quits.
360
406
  ##
361
407
  ## The engine sends its script profiler's totals only when the profiler is switched off, and at
@@ -381,13 +427,16 @@ func _exit_tree() -> void:
381
427
  ## The debugger is asked once before the profiler goes off as well, so the totals go into an empty
382
428
  ## queue. They name each function no frame did, one message apiece, and with the names of a large
383
429
  ## first frame still queued for a debugger behind on its reading, they did not fit and the engine
384
- ## 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.
385
432
  func _send_the_profile() -> void:
386
433
  if not (EngineDebugger.is_active() and EngineDebugger.is_profiling("servers")):
387
434
  return
388
435
  if not EngineDebugger.has_capture(PROFILE_CAPTURE):
389
436
  EngineDebugger.register_message_capture(PROFILE_CAPTURE, _on_debugger_message)
390
- 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
+ )
391
440
  var waits: int = PROFILE_ANSWERED_MS if answers else PROFILE_CONFIRM_MS
392
441
  var drained: bool = _debugger_answered(waits)
393
442
  EngineDebugger.profiler_enable("servers", false)
@@ -397,8 +446,8 @@ func _send_the_profile() -> void:
397
446
  print(
398
447
  (
399
448
  (
400
- "[gdharness] the debugger did not confirm the profiler's totals within %d ms, so the"
401
- + " 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"
402
451
  )
403
452
  % waits
404
453
  )
@@ -423,9 +472,9 @@ func _debugger_answered(waits: int) -> bool:
423
472
  ## summed. Nothing of the game's own runs, which quitting the tree would have done.
424
473
  func _send_profile_now(_params: Dictionary) -> Dictionary:
425
474
  if not (EngineDebugger.is_active() and EngineDebugger.is_profiling("servers")):
426
- return {"sent": false}
475
+ return { "sent": false }
427
476
  _send_the_profile()
428
- return {"sent": true, "confirmed": _profile_received}
477
+ return { "sent": true, "confirmed": _profile_received }
429
478
 
430
479
 
431
480
  ## What the script profiler costs per call in this game, measured once each time it is switched on.
@@ -479,58 +528,6 @@ func _report_errors() -> void:
479
528
  OS.add_logger(_errors)
480
529
 
481
530
 
482
- func _process(_delta: float) -> void:
483
- if not _enabled or _server == null:
484
- return
485
- _frames.note_frame(Time.get_ticks_usec())
486
- _calibrate_the_profiler()
487
-
488
- # Every connection waiting rather than one a frame: the engine queues eight a game has not
489
- # taken and refuses the rest, so calls sent together would be refused as if the game were not up.
490
- while _server.is_connection_available():
491
- var client: StreamPeerTCP = _server.take_connection()
492
- if client == null:
493
- break
494
- _clients.append(client)
495
- _send_welcome(client)
496
- # Out before the request that came with it is read: a request can hold the game in a
497
- # breakpoint before the frame's drain, and the server, never welcomed, took a game held
498
- # inside its call for one that had not taken it. A client gone already is dropped below.
499
- var _welcomed: bool = _drain(client)
500
-
501
- var gone: Array[StreamPeerTCP] = []
502
- for client: StreamPeerTCP in _clients:
503
- # A poll that fails is a client that has already gone, which the status check below is
504
- # about to find; the answer is read so it is not dropped, not because it adds anything.
505
- var polled: Error = client.poll()
506
- if polled != OK or client.get_status() != StreamPeerTCP.STATUS_CONNECTED:
507
- gone.append(client)
508
- continue
509
- var available: int = client.get_available_bytes()
510
- if available > 0:
511
- var received: Array = client.get_data(available)
512
- var bytes: PackedByteArray = received[1]
513
- _receive(client, bytes)
514
- if not _drain(client):
515
- gone.append(client)
516
-
517
- for client: StreamPeerTCP in gone:
518
- _clients.erase(client)
519
- # Dictionary.erase answers with whether the key was there, and underscore-prefixed locals
520
- # are how GDScript says an answer is deliberately unwanted: a client that had nothing
521
- # pending or owed is as forgotten as one that had both.
522
- var _had_pending: bool = _pending.erase(client)
523
- var _had_outgoing: bool = _outgoing.erase(client)
524
-
525
- # After the answer to the close has gone out: the close request reaches this node too, which then
526
- # drops its clients and whatever they were still owed.
527
- if _close_asked:
528
- _close_frames += 1
529
- if _close_frames >= CLOSE_AFTER_FRAMES or _outgoing.values().all(_is_empty):
530
- _close_asked = false
531
- _deliver_close()
532
-
533
-
534
531
  ## Hands the socket as much of what it is owed as it will take, and says whether it is still worth
535
532
  ## talking to. Reading comes first in the frame, so a client that asked and left is noticed here
536
533
  ## rather than blocking on a write that can never finish.
@@ -575,7 +572,10 @@ func _start_server() -> void:
575
572
  # own path: a gate that starts sixteen engines at once announces sixteen games that are not
576
573
  # games, and a client asking the runtime anything while they run can be answered by whichever
577
574
  # of them replies first. Measured on two projects before it was written here.
578
- 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
+ ):
579
579
  _enabled = false
580
580
  return
581
581
 
@@ -596,14 +596,19 @@ func _start_server() -> void:
596
596
 
597
597
  _server = TCPServer.new()
598
598
  # listen() defaults bind_address to "*", which exposes the game to the whole network.
599
- 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
+ )
600
602
  var wanted_port: int = GdhRead.as_int(ProjectSettings.get_setting(PORT_SETTING, 0))
601
603
  var error: Error = _server.listen(wanted_port, bind_address)
602
604
  if error != OK:
603
605
  # A warning, not an error: callers treat any ERROR line on stderr as a failed
604
606
  # operation, and a game without a runtime server is a handled condition.
605
607
  push_warning(
606
- "[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
+ )
607
612
  )
608
613
  _enabled = false
609
614
  return
@@ -611,7 +616,8 @@ func _start_server() -> void:
611
616
  _port = _server.get_local_port()
612
617
  _announce(bind_address)
613
618
  var listening: String = (
614
- "[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]
615
621
  )
616
622
  # The line the server tells the two streams apart by: it takes the prints before it from the
617
623
  # debug adapter and everything from it on from the file, which has it first. After the
@@ -622,8 +628,8 @@ func _start_server() -> void:
622
628
  print(listening)
623
629
 
624
630
 
625
- ## The scene [param given] names for the engine to run, as a `res://` path, when it lies under one of
626
- ## [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
627
633
  ## extension for the scene, relative to the project or absolute, so each is read as it would.
628
634
  static func quiet_scene_in(given: PackedStringArray, prefixes: Variant) -> String:
629
635
  # A PackedStringArray as project.godot writes one, or an Array of strings as a hand might.
@@ -636,7 +642,10 @@ static func quiet_scene_in(given: PackedStringArray, prefixes: Variant) -> Strin
636
642
  if prefix is String:
637
643
  var _kept: bool = folders.append(str(prefix))
638
644
  for argument: String in given:
639
- 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
+ ):
640
649
  continue
641
650
  var scene: String = ProjectSettings.localize_path(argument)
642
651
  for folder: String in folders:
@@ -679,7 +688,10 @@ func _announce(bind_address: String) -> void:
679
688
  var made: Error = DirAccess.make_dir_recursive_absolute(directory)
680
689
  if made != OK and made != ERR_ALREADY_EXISTS:
681
690
  push_warning(
682
- "[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
+ )
683
695
  )
684
696
  return
685
697
  var path: String = directory.path_join("runtime-%d.json" % OS.get_process_id())
@@ -734,7 +746,7 @@ func _identity(bind_address: String) -> Dictionary:
734
746
  "project":
735
747
  {
736
748
  "name": str(ProjectSettings.get_setting("application/config/name", "")),
737
- "path": ProjectSettings.globalize_path("res://").rstrip("/")
749
+ "path": ProjectSettings.globalize_path("res://").rstrip("/"),
738
750
  },
739
751
  "godot": Engine.get_version_info().get("string", ""),
740
752
  # Where user:// is for this run, which only the engine can say: it follows the project's
@@ -807,7 +819,7 @@ func _execute_command(command: String, params: Dictionary) -> Dictionary:
807
819
  if not handler.is_valid():
808
820
  return {
809
821
  "type": "error",
810
- "message": "Unknown command: %s. Commands: %s" % [command, ", ".join(_command_names())]
822
+ "message": "Unknown command: %s. Commands: %s" % [command, ", ".join(_command_names())],
811
823
  }
812
824
  # Only a waits module already loaded can have a trace running to record the call in.
813
825
  var waits: Object = _modules.get(WAITS)
@@ -818,7 +830,7 @@ func _execute_command(command: String, params: Dictionary) -> Dictionary:
818
830
 
819
831
 
820
832
  func _ping(_params: Dictionary) -> Dictionary:
821
- return {"type": "pong", "timestamp": Time.get_unix_time_from_system()}
833
+ return { "type": "pong", "timestamp": Time.get_unix_time_from_system() }
822
834
 
823
835
 
824
836
  ## The game asked to close, as a player's close asks it: answered at once and delivered once the
@@ -827,12 +839,12 @@ func _ping(_params: Dictionary) -> Dictionary:
827
839
  func _close_game(_params: Dictionary) -> Dictionary:
828
840
  _close_asked = true
829
841
  _close_frames = 0
830
- return {"type": "closing", "acceptsQuit": get_tree().auto_accept_quit}
842
+ return { "type": "closing", "acceptsQuit": get_tree().auto_accept_quit }
831
843
 
832
844
 
833
845
  ## What a player's close does: the close request reaches every node, and then the root window says
834
- ## it was asked to close, which the tree answers by quitting when it accepts quits. So the game's own
835
- ## 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.
836
848
  func _deliver_close() -> void:
837
849
  var root: Window = get_tree().root
838
850
  root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST)
@@ -859,20 +871,7 @@ func _send_response(client: StreamPeerTCP, data: Dictionary) -> void:
859
871
 
860
872
  ## A request that could not be read far enough to find its id is answered with a null one.
861
873
  func _send_error(client: StreamPeerTCP, request_id: Variant, message: String) -> void:
862
- _send_response(client, {"type": "error", "message": message, "id": request_id})
863
-
864
-
865
- ## Down with the window only when the close is taken. A game that asks before quitting turns
866
- ## auto_accept_quit off and decides for itself, and one whose player answered No went on running
867
- ## with no runtime and no announcement, so every later call said no game was running. A quit it
868
- ## does take ends the tree, which [method _exit_tree] cleans up after.
869
- ##
870
- ## One line for every notification, and the last this node runs in a frame, after [method _process]:
871
- ## a pause that stops in this addon is let go to a breakpoint here and stepped once into the game's
872
- ## own script, rather than stepped through the frame a line at a time.
873
- func _notification(what: int) -> void:
874
- if what == NOTIFICATION_WM_CLOSE_REQUEST and _takes_the_close():
875
- _cleanup()
874
+ _send_response(client, { "type": "error", "message": message, "id": request_id })
876
875
 
877
876
 
878
877
  func _takes_the_close() -> bool:
@@ -896,6 +895,9 @@ func _cleanup() -> void:
896
895
  var removed: Error = DirAccess.remove_absolute(_announcement)
897
896
  if removed != OK:
898
897
  push_warning(
899
- "[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
+ )
900
902
  )
901
903
  _announcement = ""