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.
- package/build/cli.js +131 -47
- package/build/godot/addons/auto_reload/auto_reload.gd +18 -19
- package/build/godot/addons/gdharness_editor/animation_reads.gd +84 -35
- 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 +114 -39
- package/build/godot/addons/gdharness_editor/file_walk.gd +12 -5
- package/build/godot/addons/gdharness_editor/headless_check.gd +94 -41
- 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 +37 -12
- package/build/godot/addons/gdharness_editor/same_values.gd +4 -4
- package/build/godot/addons/gdharness_editor/scene_file.gd +136 -79
- 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 +41 -28
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +84 -50
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +70 -40
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +34 -30
- 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 +181 -107
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -9
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +75 -43
- 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 +66 -0
- package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +171 -62
- 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 +37 -12
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +84 -63
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +153 -148
- 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 +38 -20
- 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 +211 -0
- package/build/godot/addons/gdharness_runtime/runtime_touch.gd.uid +1 -0
- 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 +84 -35
- 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 +139 -93
- package/build/godot/operations/editor_tools.gd +9 -10
- package/build/godot/operations/export_presets.gd +100 -54
- package/build/godot/operations/file_reads.gd +114 -39
- package/build/godot/operations/file_walk.gd +12 -5
- package/build/godot/operations/gdscript_analysis.gd +105 -66
- package/build/godot/operations/gdscript_authoring.gd +138 -39
- 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 +63 -39
- package/build/godot/operations/patterns.gd +3 -1
- package/build/godot/operations/plugins.gd +36 -16
- package/build/godot/operations/project_config.gd +144 -27
- 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 +37 -12
- 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/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
|
|
136
|
-
## 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.
|
|
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
|
|
281
|
-
## could run first: a game played on a macOS runner counted 25 calls of a function called 26
|
|
282
|
-
## 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.
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
|
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(
|
|
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
|
-
|
|
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"
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
898
|
+
(
|
|
899
|
+
"[gdharness] cannot remove %s; the server will find a game that is gone"
|
|
900
|
+
% _announcement
|
|
901
|
+
)
|
|
897
902
|
)
|
|
898
903
|
_announcement = ""
|