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.
- package/build/cli.js +383 -243
- package/build/godot/addons/auto_reload/auto_reload.gd +18 -19
- package/build/godot/addons/gdharness_editor/animation_reads.gd +55 -33
- package/build/godot/addons/gdharness_editor/bridge_client.gd +53 -58
- package/build/godot/addons/gdharness_editor/declared_values.gd +68 -32
- package/build/godot/addons/gdharness_editor/file_reads.gd +83 -42
- package/build/godot/addons/gdharness_editor/file_walk.gd +12 -5
- package/build/godot/addons/gdharness_editor/headless_check.gd +68 -44
- package/build/godot/addons/gdharness_editor/plugin.gd +2 -3
- package/build/godot/addons/gdharness_editor/property_paths.gd +22 -12
- package/build/godot/addons/gdharness_editor/property_values.gd +143 -78
- package/build/godot/addons/gdharness_editor/reading.gd +51 -17
- package/build/godot/addons/gdharness_editor/same_values.gd +4 -4
- package/build/godot/addons/gdharness_editor/scene_file.gd +127 -80
- package/build/godot/addons/gdharness_editor/scene_uids.gd +12 -6
- package/build/godot/addons/gdharness_editor/serialisation.gd +75 -51
- package/build/godot/addons/gdharness_editor/setting_reads.gd +7 -6
- package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -6
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +37 -27
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +79 -49
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +70 -40
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +65 -42
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +24 -22
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +49 -29
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +26 -21
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +40 -40
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +166 -107
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -9
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +68 -41
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +35 -20
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +28 -13
- package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +12 -5
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +113 -67
- package/build/godot/addons/gdharness_editor/tools/script_paths.gd +20 -8
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +11 -8
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +47 -34
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +77 -50
- package/build/godot/addons/gdharness_editor/value_shapes.gd +5 -6
- package/build/godot/addons/gdharness_runtime/call_errors.gd +1 -2
- package/build/godot/addons/gdharness_runtime/declared_values.gd +68 -32
- package/build/godot/addons/gdharness_runtime/error_report.gd +1 -2
- package/build/godot/addons/gdharness_runtime/reading.gd +51 -17
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +84 -63
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +149 -147
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +64 -42
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +184 -122
- package/build/godot/addons/gdharness_runtime/runtime_frames.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd +24 -20
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +134 -92
- package/build/godot/addons/gdharness_runtime/runtime_items.gd +29 -13
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd +0 -1
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +34 -17
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +17 -13
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +84 -46
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +117 -79
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +14 -17
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +23 -16
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +117 -66
- package/build/godot/addons/gdharness_runtime/runtime_touch.gd +15 -15
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +44 -27
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +108 -73
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +111 -84
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +10 -7
- package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -51
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +5 -6
- package/build/godot/addons/gdharness_tests/bench.gd +4 -5
- package/build/godot/addons/gdharness_tests/event.gd +10 -5
- package/build/godot/addons/gdharness_tests/share.gd +38 -18
- package/build/godot/addons/gdharness_tests/suites.gd +3 -3
- package/build/godot/addons/gdharness_tests/wait.gd +3 -2
- package/build/godot/loader/loader.gd +9 -9
- package/build/godot/operations/animation_reads.gd +55 -33
- package/build/godot/operations/audio_buses.gd +48 -19
- package/build/godot/operations/class_cache.gd +16 -11
- package/build/godot/operations/classdb_queries.gd +89 -44
- package/build/godot/operations/declared_values.gd +68 -32
- package/build/godot/operations/dependencies.gd +190 -109
- package/build/godot/operations/editor_tools.gd +10 -10
- package/build/godot/operations/export_presets.gd +100 -54
- package/build/godot/operations/file_reads.gd +83 -42
- package/build/godot/operations/file_walk.gd +12 -5
- package/build/godot/operations/gdscript_analysis.gd +86 -65
- package/build/godot/operations/gdscript_authoring.gd +83 -36
- package/build/godot/operations/godot_operations.gd +17 -11
- package/build/godot/operations/import_pipeline.gd +110 -59
- package/build/godot/operations/input_actions.gd +62 -39
- package/build/godot/operations/patterns.gd +3 -1
- package/build/godot/operations/plugins.gd +36 -16
- package/build/godot/operations/project_config.gd +61 -32
- package/build/godot/operations/project_diagnostics.gd +24 -11
- package/build/godot/operations/project_file.gd +16 -16
- package/build/godot/operations/project_validation.gd +71 -44
- package/build/godot/operations/property_paths.gd +22 -12
- package/build/godot/operations/property_values.gd +143 -78
- package/build/godot/operations/reading.gd +51 -17
- package/build/godot/operations/rename_checks.gd +2 -2
- package/build/godot/operations/resource_files.gd +11 -9
- package/build/godot/operations/same_values.gd +4 -4
- package/build/godot/operations/scene_connections.gd +27 -13
- package/build/godot/operations/script_check.gd +52 -49
- package/build/godot/operations/serialisation.gd +75 -51
- package/build/godot/operations/setting_reads.gd +7 -6
- package/build/godot/operations/value_shapes.gd +5 -6
- package/build/godot/tests/listed.gd +321 -268
- package/build/keeper.js +43 -43
- 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
|
|
139
|
-
## so only the game can say which it was. Kept in step with `EXIT_NOTES_VARIABLE` in
|
|
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
|
|
284
|
-
## could run first: a game played on a macOS runner counted 25 calls of a function called 26
|
|
285
|
-
## the first of them from _ready. Run at the autoload's construction, before the main scene
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
|
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(
|
|
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
|
-
|
|
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"
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
898
|
+
(
|
|
899
|
+
"[gdharness] cannot remove %s; the server will find a game that is gone"
|
|
900
|
+
% _announcement
|
|
901
|
+
)
|
|
900
902
|
)
|
|
901
903
|
_announcement = ""
|