gdharness 0.12.17 → 0.13.0
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 +174 -56
- package/build/godot/addons/auto_reload/auto_reload.gd +9 -2
- package/build/godot/addons/gdharness_editor/bridge_client.gd +11 -5
- package/build/godot/addons/gdharness_editor/plugin.gd +17 -4
- package/build/godot/addons/gdharness_editor/reading.gd +64 -0
- package/build/godot/addons/gdharness_editor/reading.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +30 -12
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +5 -2
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +3 -1
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +50 -27
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +92 -42
- package/build/godot/addons/gdharness_runtime/reading.gd +64 -0
- package/build/godot/addons/gdharness_runtime/reading.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +24 -10
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +7 -5
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +24 -22
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +16 -13
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +24 -8
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +7 -4
- package/build/godot/operations/audio_buses.gd +7 -6
- package/build/godot/operations/class_cache.gd +14 -9
- package/build/godot/operations/classdb_queries.gd +4 -3
- package/build/godot/operations/dependencies.gd +19 -16
- package/build/godot/operations/file_walk.gd +4 -2
- package/build/godot/operations/gdscript_analysis.gd +2 -3
- package/build/godot/operations/gdscript_authoring.gd +20 -8
- package/build/godot/operations/godot_operations.gd +7 -2
- package/build/godot/operations/import_pipeline.gd +5 -4
- package/build/godot/operations/input_actions.gd +13 -9
- package/build/godot/operations/patterns.gd +22 -0
- package/build/godot/operations/plugins.gd +10 -6
- package/build/godot/operations/project_config.gd +41 -3
- package/build/godot/operations/reading.gd +66 -0
- package/build/godot/operations/serialisation.gd +41 -9
- package/build/index.js +147 -53
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@ extends Node
|
|
|
13
13
|
const Capture = preload("runtime_capture.gd")
|
|
14
14
|
const InputCommands = preload("runtime_input.gd")
|
|
15
15
|
const Queries = preload("runtime_queries.gd")
|
|
16
|
+
const Read = preload("reading.gd")
|
|
16
17
|
const Values = preload("runtime_values.gd")
|
|
17
18
|
const Waits = preload("runtime_waits.gd")
|
|
18
19
|
|
|
@@ -110,8 +111,10 @@ func _process(_delta: float) -> void:
|
|
|
110
111
|
|
|
111
112
|
var gone: Array[StreamPeerTCP] = []
|
|
112
113
|
for client: StreamPeerTCP in _clients:
|
|
113
|
-
client
|
|
114
|
-
|
|
114
|
+
# A poll that fails is a client that has already gone, which the status check below is
|
|
115
|
+
# about to find; the answer is read so it is not dropped, not because it adds anything.
|
|
116
|
+
var polled: Error = client.poll()
|
|
117
|
+
if polled != OK or client.get_status() != StreamPeerTCP.STATUS_CONNECTED:
|
|
115
118
|
gone.append(client)
|
|
116
119
|
continue
|
|
117
120
|
var available: int = client.get_available_bytes()
|
|
@@ -124,8 +127,11 @@ func _process(_delta: float) -> void:
|
|
|
124
127
|
|
|
125
128
|
for client: StreamPeerTCP in gone:
|
|
126
129
|
_clients.erase(client)
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
# Dictionary.erase answers with whether the key was there, and underscore-prefixed locals
|
|
131
|
+
# are how GDScript says an answer is deliberately unwanted: a client that had nothing
|
|
132
|
+
# pending or owed is as forgotten as one that had both.
|
|
133
|
+
var _had_pending: bool = _pending.erase(client)
|
|
134
|
+
var _had_outgoing: bool = _outgoing.erase(client)
|
|
129
135
|
|
|
130
136
|
|
|
131
137
|
## Hands the socket as much of what it is owed as it will take, and says whether it is still worth
|
|
@@ -138,7 +144,7 @@ func _drain(client: StreamPeerTCP) -> bool:
|
|
|
138
144
|
var sent: Array = client.put_partial_data(owed)
|
|
139
145
|
if sent[0] != OK:
|
|
140
146
|
return false
|
|
141
|
-
_outgoing[client] = owed.slice(
|
|
147
|
+
_outgoing[client] = owed.slice(Read.as_int(sent[1]))
|
|
142
148
|
return true
|
|
143
149
|
|
|
144
150
|
|
|
@@ -172,14 +178,14 @@ func _start_server() -> void:
|
|
|
172
178
|
# own path: a gate that starts sixteen engines at once announces sixteen games that are not
|
|
173
179
|
# games, and a client asking the runtime anything while they run can be answered by whichever
|
|
174
180
|
# of them replies first. Measured on two projects before it was written here.
|
|
175
|
-
if _script_run() and not
|
|
181
|
+
if _script_run() and not Read.as_bool(ProjectSettings.get_setting(SCRIPT_RUNS_SETTING, false)):
|
|
176
182
|
_enabled = false
|
|
177
183
|
return
|
|
178
184
|
|
|
179
185
|
_server = TCPServer.new()
|
|
180
186
|
# listen() defaults bind_address to "*", which exposes the game to the whole network.
|
|
181
187
|
var bind_address: String = str(ProjectSettings.get_setting(BIND_ADDRESS_SETTING, DEFAULT_BIND_ADDRESS))
|
|
182
|
-
var wanted_port: int =
|
|
188
|
+
var wanted_port: int = Read.as_int(ProjectSettings.get_setting(PORT_SETTING, 0))
|
|
183
189
|
var error: Error = _server.listen(wanted_port, bind_address)
|
|
184
190
|
if error != OK:
|
|
185
191
|
# A warning, not an error: callers treat any ERROR line on stderr as a failed
|
|
@@ -233,8 +239,11 @@ func _announce(bind_address: String) -> void:
|
|
|
233
239
|
)
|
|
234
240
|
)
|
|
235
241
|
return
|
|
236
|
-
file.store_string(JSON.stringify(_identity(bind_address)))
|
|
242
|
+
var announced: bool = file.store_string(JSON.stringify(_identity(bind_address)))
|
|
237
243
|
file.close()
|
|
244
|
+
if not announced:
|
|
245
|
+
push_warning("[gdharness] cannot write %s; the server will not find this game" % path)
|
|
246
|
+
return
|
|
238
247
|
_announcement = path
|
|
239
248
|
|
|
240
249
|
|
|
@@ -286,11 +295,12 @@ func _handle_message(client: StreamPeerTCP, line: String) -> void:
|
|
|
286
295
|
if not params is Dictionary:
|
|
287
296
|
_send_error(client, request_id, "params must be an object")
|
|
288
297
|
return
|
|
298
|
+
var arguments: Dictionary = params
|
|
289
299
|
|
|
290
300
|
# A command that waits on the game (a frame, a signal, a condition) suspends here and answers
|
|
291
301
|
# when it is done, while _process keeps serving the other clients in the meantime. A client
|
|
292
302
|
# that hung up while its command waited is simply not written to.
|
|
293
|
-
var result: Dictionary = await _execute_command(command,
|
|
303
|
+
var result: Dictionary = await _execute_command(command, arguments)
|
|
294
304
|
result["id"] = request_id
|
|
295
305
|
if client.get_status() == StreamPeerTCP.STATUS_CONNECTED:
|
|
296
306
|
_send_response(client, result)
|
|
@@ -346,5 +356,9 @@ func _cleanup() -> void:
|
|
|
346
356
|
# The announcement is what tells the server this game exists, so it goes before the
|
|
347
357
|
# process does. A crash leaves it behind, and the server drops one whose process is gone.
|
|
348
358
|
if not _announcement.is_empty():
|
|
349
|
-
DirAccess.remove_absolute(_announcement)
|
|
359
|
+
var removed: Error = DirAccess.remove_absolute(_announcement)
|
|
360
|
+
if removed != OK:
|
|
361
|
+
push_warning(
|
|
362
|
+
"[gdharness] cannot remove %s; the server will find a game that is gone" % _announcement
|
|
363
|
+
)
|
|
350
364
|
_announcement = ""
|
|
@@ -2,6 +2,7 @@ extends RefCounted
|
|
|
2
2
|
|
|
3
3
|
## A picture of the running game, as a PNG written where the server asked for it.
|
|
4
4
|
|
|
5
|
+
const Read = preload("reading.gd")
|
|
5
6
|
const Values = preload("runtime_values.gd")
|
|
6
7
|
|
|
7
8
|
var _host: Node
|
|
@@ -16,7 +17,7 @@ func capture_screenshot(params: Dictionary) -> Dictionary:
|
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
func capture_viewport(params: Dictionary) -> Dictionary:
|
|
19
|
-
var viewport_path: String =
|
|
20
|
+
var viewport_path: String = str(params.get("viewportPath", ""))
|
|
20
21
|
if viewport_path.is_empty():
|
|
21
22
|
return capture_screenshot(params)
|
|
22
23
|
|
|
@@ -26,13 +27,14 @@ func capture_viewport(params: Dictionary) -> Dictionary:
|
|
|
26
27
|
var node: Node = standing["node"]
|
|
27
28
|
if not node is Viewport:
|
|
28
29
|
return {"type": "error", "message": "Node is not a Viewport: " + viewport_path}
|
|
29
|
-
|
|
30
|
+
var viewport: Viewport = node
|
|
31
|
+
return _capture(viewport, params)
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
## The server names the file, so a game cannot point it at a path of its own choosing; a call
|
|
33
35
|
## with no path is a call the server did not make.
|
|
34
36
|
func _capture(viewport: Viewport, params: Dictionary) -> Dictionary:
|
|
35
|
-
var requested_path: String =
|
|
37
|
+
var requested_path: String = str(params.get("output_path", ""))
|
|
36
38
|
if requested_path.is_empty():
|
|
37
39
|
return {"type": "error", "message": "output_path required"}
|
|
38
40
|
|
|
@@ -59,8 +61,8 @@ func _capture(viewport: Viewport, params: Dictionary) -> Dictionary:
|
|
|
59
61
|
if image == null:
|
|
60
62
|
return {"type": "error", "message": "Failed to capture viewport image"}
|
|
61
63
|
|
|
62
|
-
var width: int =
|
|
63
|
-
var height: int =
|
|
64
|
+
var width: int = Read.as_int(params.get("width", 0))
|
|
65
|
+
var height: int = Read.as_int(params.get("height", 0))
|
|
64
66
|
if width > 0 and height > 0:
|
|
65
67
|
image.resize(width, height)
|
|
66
68
|
|
|
@@ -9,6 +9,7 @@ const Values = preload("runtime_values.gd")
|
|
|
9
9
|
## query module rather than worked out again here, so the place this aims at and the place a rect
|
|
10
10
|
## reports are the same place by construction rather than by agreement.
|
|
11
11
|
const Queries = preload("runtime_queries.gd")
|
|
12
|
+
const Read = preload("reading.gd")
|
|
12
13
|
|
|
13
14
|
## The distance from a capital letter to its small one in Unicode. A keycode holds the capital.
|
|
14
15
|
const TO_SMALL: int = 32
|
|
@@ -40,10 +41,10 @@ func _init(host: Node, values: Values) -> void:
|
|
|
40
41
|
##
|
|
41
42
|
## `pressed` is how a caller says otherwise: true holds it down, false lets go of one being held.
|
|
42
43
|
func inject_action(params: Dictionary) -> Dictionary:
|
|
43
|
-
var action: String =
|
|
44
|
-
var held: bool =
|
|
44
|
+
var action: String = str(params.get("action", ""))
|
|
45
|
+
var held: bool = Read.as_bool(params.get("pressed", true), true)
|
|
45
46
|
var whole: bool = not params.has("pressed")
|
|
46
|
-
var strength: float =
|
|
47
|
+
var strength: float = Read.as_float(params.get("strength", 1.0), 1.0)
|
|
47
48
|
|
|
48
49
|
if action.is_empty():
|
|
49
50
|
return {"type": "error", "message": "Action name required"}
|
|
@@ -79,15 +80,15 @@ func _say_action(action: String, down: bool, strength: float) -> void:
|
|
|
79
80
|
## Presses one key, and lets go of it again unless asked to hold it. See [method inject_action].
|
|
80
81
|
func inject_key(params: Dictionary) -> Dictionary:
|
|
81
82
|
var keycode_raw: Variant = params.get("keycode", 0)
|
|
82
|
-
var held: bool =
|
|
83
|
+
var held: bool = Read.as_bool(params.get("pressed", true), true)
|
|
83
84
|
var whole: bool = not params.has("pressed")
|
|
84
|
-
var key_label: String =
|
|
85
|
+
var key_label: String = str(params.get("key_label", ""))
|
|
85
86
|
|
|
86
87
|
if keycode_raw is String:
|
|
87
88
|
var named: String = keycode_raw
|
|
88
89
|
if not named.is_empty() and key_label.is_empty():
|
|
89
90
|
key_label = named
|
|
90
|
-
var keycode: int = 0 if keycode_raw is String else
|
|
91
|
+
var keycode: int = 0 if keycode_raw is String else Read.as_int(keycode_raw)
|
|
91
92
|
|
|
92
93
|
var event: InputEventKey = InputEventKey.new()
|
|
93
94
|
event.pressed = whole or held
|
|
@@ -109,9 +110,9 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
109
110
|
event.physical_keycode = event.keycode
|
|
110
111
|
event.key_label = event.keycode
|
|
111
112
|
|
|
112
|
-
event.shift_pressed =
|
|
113
|
-
event.ctrl_pressed =
|
|
114
|
-
event.alt_pressed =
|
|
113
|
+
event.shift_pressed = Read.as_bool(params.get("shift", false))
|
|
114
|
+
event.ctrl_pressed = Read.as_bool(params.get("ctrl", false))
|
|
115
|
+
event.alt_pressed = Read.as_bool(params.get("alt", false))
|
|
115
116
|
|
|
116
117
|
# The fourth thing a real event carries, and the only one a text field reads: LineEdit and
|
|
117
118
|
# TextEdit insert `unicode` and never consult the keycode, so an injected key could press any
|
|
@@ -161,8 +162,8 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
161
162
|
## 2.1, and the answer said four characters had gone in. What the field holds afterwards is read
|
|
162
163
|
## back now rather than echoed from the request, so a call that typed somewhere unhelpful says so.
|
|
163
164
|
func inject_text(params: Dictionary) -> Dictionary:
|
|
164
|
-
var text: String =
|
|
165
|
-
var over: bool =
|
|
165
|
+
var text: String = str(params.get("text", ""))
|
|
166
|
+
var over: bool = Read.as_bool(params.get("replace", false))
|
|
166
167
|
if text.is_empty() and not over:
|
|
167
168
|
return {"type": "error", "message": "text needs something to type, or replace to empty a field"}
|
|
168
169
|
|
|
@@ -305,7 +306,7 @@ func _glyph_of(keycode: Key, shifted: bool) -> int:
|
|
|
305
306
|
## Answers a Vector2, or the String that says what was wrong with it.
|
|
306
307
|
func _read_point(params: Dictionary, x_key: String, y_key: String, pair_key: String) -> Variant:
|
|
307
308
|
if params.has(x_key) and params.has(y_key):
|
|
308
|
-
return Vector2(
|
|
309
|
+
return Vector2(Read.as_float(params[x_key]), Read.as_float(params[y_key]))
|
|
309
310
|
var raw: Variant = params.get(pair_key, Vector2.ZERO)
|
|
310
311
|
if raw is Vector2:
|
|
311
312
|
return raw
|
|
@@ -313,7 +314,7 @@ func _read_point(params: Dictionary, x_key: String, y_key: String, pair_key: Str
|
|
|
313
314
|
var pair: Array = raw
|
|
314
315
|
if pair.size() < 2:
|
|
315
316
|
return "%s array must contain [x, y]" % pair_key
|
|
316
|
-
return Vector2(
|
|
317
|
+
return Vector2(Read.as_float(pair[0]), Read.as_float(pair[1]))
|
|
317
318
|
return "%s must be Vector2 or [x, y]" % pair_key
|
|
318
319
|
|
|
319
320
|
|
|
@@ -325,8 +326,8 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
325
326
|
var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
|
|
326
327
|
if button < 0:
|
|
327
328
|
return _no_such_button(params.get("button"))
|
|
328
|
-
var pressed: bool =
|
|
329
|
-
var double: bool =
|
|
329
|
+
var pressed: bool = Read.as_bool(params.get("pressed", true), true)
|
|
330
|
+
var double: bool = Read.as_bool(params.get("doubleClick", false))
|
|
330
331
|
|
|
331
332
|
Input.parse_input_event(_button(position, button, pressed, double))
|
|
332
333
|
|
|
@@ -445,7 +446,8 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
445
446
|
return standing
|
|
446
447
|
var node: Node = standing["node"]
|
|
447
448
|
if node is Node3D:
|
|
448
|
-
|
|
449
|
+
var spatial: Node3D = node
|
|
450
|
+
return await _click_in_the_world(node_path, spatial, params)
|
|
449
451
|
if not node is Control:
|
|
450
452
|
return {
|
|
451
453
|
"type": "error",
|
|
@@ -495,7 +497,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
495
497
|
var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
|
|
496
498
|
if button < 0:
|
|
497
499
|
return _no_such_button(params.get("button"))
|
|
498
|
-
var double: bool =
|
|
500
|
+
var double: bool = Read.as_bool(params.get("double", false))
|
|
499
501
|
|
|
500
502
|
# Pushed into the viewport rather than through Input: Input accumulates events and flushes
|
|
501
503
|
# them at the next frame, so the hovered control read below would be the one from before
|
|
@@ -672,7 +674,7 @@ static func _menu_of(node: Node) -> PopupMenu:
|
|
|
672
674
|
## Minus one when neither names one that is there.
|
|
673
675
|
static func _wanted_item(menu: PopupMenu, params: Dictionary) -> int:
|
|
674
676
|
if params.has("index"):
|
|
675
|
-
var asked: int =
|
|
677
|
+
var asked: int = Read.as_int(params.get("index", -1), -1)
|
|
676
678
|
return asked if asked >= 0 and asked < menu.get_item_count() else -1
|
|
677
679
|
var wanted: String = str(params.get("text", ""))
|
|
678
680
|
if wanted.is_empty():
|
|
@@ -687,8 +689,8 @@ static func _wanted_item(menu: PopupMenu, params: Dictionary) -> int:
|
|
|
687
689
|
|
|
688
690
|
|
|
689
691
|
## What the menu says, for a refusal that names the choices rather than the miss.
|
|
690
|
-
static func _items_of(menu: PopupMenu) ->
|
|
691
|
-
var said:
|
|
692
|
+
static func _items_of(menu: PopupMenu) -> Array[String]:
|
|
693
|
+
var said: Array[String] = []
|
|
692
694
|
for index: int in menu.get_item_count():
|
|
693
695
|
said.append("%d: %s" % [index, menu.get_item_text(index)])
|
|
694
696
|
return said
|
|
@@ -758,7 +760,7 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
|
|
|
758
760
|
var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
|
|
759
761
|
if button < 0:
|
|
760
762
|
return _no_such_button(params.get("button"))
|
|
761
|
-
var double: bool =
|
|
763
|
+
var double: bool = Read.as_bool(params.get("double", false))
|
|
762
764
|
|
|
763
765
|
viewport.push_input(_motion(position, Vector2.ZERO))
|
|
764
766
|
# Read before the press, for the reason the Control click reads it: what the caller needs to
|
|
@@ -839,4 +841,4 @@ func _resolve_mouse_button(raw: Variant) -> int:
|
|
|
839
841
|
return MOUSE_BUTTON_WHEEL_DOWN
|
|
840
842
|
_:
|
|
841
843
|
return -1
|
|
842
|
-
return
|
|
844
|
+
return Read.as_int(raw, -1)
|
|
@@ -3,6 +3,7 @@ extends RefCounted
|
|
|
3
3
|
## What the server asks about the running tree: its shape, one node, the nodes matching a
|
|
4
4
|
## question, where one is on screen, a property set, a method called, the metrics.
|
|
5
5
|
|
|
6
|
+
const Read = preload("reading.gd")
|
|
6
7
|
const Values = preload("runtime_values.gd")
|
|
7
8
|
|
|
8
9
|
## The most nodes one find answers with, unless asked for fewer: enough for any real query and
|
|
@@ -31,8 +32,8 @@ func _init(host: Node, values: Values) -> void:
|
|
|
31
32
|
|
|
32
33
|
func get_tree(params: Dictionary) -> Dictionary:
|
|
33
34
|
var root_path: String = str(params.get("root", "/root"))
|
|
34
|
-
var max_depth: int =
|
|
35
|
-
var include_properties: bool =
|
|
35
|
+
var max_depth: int = Read.as_int(params.get("depth", 3), 3)
|
|
36
|
+
var include_properties: bool = Read.as_bool(params.get("include_properties", false))
|
|
36
37
|
|
|
37
38
|
var reached: Dictionary = Values.node_at(_host.get_tree().root, root_path)
|
|
38
39
|
if reached.has("message"):
|
|
@@ -65,8 +66,8 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
65
66
|
for filter: String in FILTERS:
|
|
66
67
|
wanted[filter] = str(params.get(filter, ""))
|
|
67
68
|
var wanted_property: String = str(params.get("property", ""))
|
|
68
|
-
var include_hidden: bool =
|
|
69
|
-
var limit: int = clampi(
|
|
69
|
+
var include_hidden: bool = Read.as_bool(params.get("include_hidden", true), true)
|
|
70
|
+
var limit: int = clampi(Read.as_int(params.get("limit", FIND_LIMIT), FIND_LIMIT), 1, FIND_LIMIT_CEILING)
|
|
70
71
|
|
|
71
72
|
if not _anything_asked(wanted):
|
|
72
73
|
return {"type": "error", "message": "find_nodes needs at least one of " + ", ".join(FILTERS)}
|
|
@@ -110,7 +111,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
110
111
|
pending.push_front(children[index])
|
|
111
112
|
|
|
112
113
|
var answer: Dictionary = {"type": "nodes", "count": found.size(), "truncated": truncated, "nodes": found}
|
|
113
|
-
var notes:
|
|
114
|
+
var notes: Array[String] = []
|
|
114
115
|
# Nothing found is the one answer that cannot be told apart from having asked the wrong
|
|
115
116
|
# question, and a name written without a wildcard is the way an agent writes "contains".
|
|
116
117
|
# Said only when it changes the answer, so a genuine nothing stays a plain nothing.
|
|
@@ -262,8 +263,8 @@ func _matches_apart_from_name(node: Node, wanted: Dictionary[String, String]) ->
|
|
|
262
263
|
## whose interesting half is below the cut says so in a number rather than in a flag.
|
|
263
264
|
func read_text(params: Dictionary) -> Dictionary:
|
|
264
265
|
var root_path: String = str(params.get("root", "/root"))
|
|
265
|
-
var include_hidden: bool =
|
|
266
|
-
var limit: int = clampi(
|
|
266
|
+
var include_hidden: bool = Read.as_bool(params.get("include_hidden", false))
|
|
267
|
+
var limit: int = clampi(Read.as_int(params.get("limit", READ_LIMIT), READ_LIMIT), 1, READ_LIMIT)
|
|
267
268
|
|
|
268
269
|
var reached: Dictionary = Values.node_at(_host.get_tree().root, root_path)
|
|
269
270
|
if reached.has("message"):
|
|
@@ -275,7 +276,7 @@ func read_text(params: Dictionary) -> Dictionary:
|
|
|
275
276
|
# player is being asked to answer below them reads exactly like a dialog that is not there,
|
|
276
277
|
# and a bare `truncated: true` was looked straight past twice before the limit was suspected.
|
|
277
278
|
# A number is what sends somebody back.
|
|
278
|
-
var lines:
|
|
279
|
+
var lines: Array[String] = []
|
|
279
280
|
var left_out: int = _read_into(root, include_hidden, limit, lines)
|
|
280
281
|
return {
|
|
281
282
|
"type": "text",
|
|
@@ -297,7 +298,7 @@ func read_text(params: Dictionary) -> Dictionary:
|
|
|
297
298
|
## until it is opened, and reading a closed menu would put every item on the screen.
|
|
298
299
|
## Fills [param into] up to [param most] lines and answers with how many further lines the rest of
|
|
299
300
|
## the subtree says, which is what the caller is told rather than left to infer.
|
|
300
|
-
func _read_into(node: Node, include_hidden: bool, most: int, into:
|
|
301
|
+
func _read_into(node: Node, include_hidden: bool, most: int, into: Array[String]) -> int:
|
|
301
302
|
if not include_hidden and not _drawn(node):
|
|
302
303
|
return 0
|
|
303
304
|
var left_out: int = 0
|
|
@@ -336,7 +337,7 @@ static func _drawn(node: Node) -> bool:
|
|
|
336
337
|
## the three of them come to disagree about a SpinBox.
|
|
337
338
|
static func said_by(node: Node) -> String:
|
|
338
339
|
for property: Dictionary in node.get_property_list():
|
|
339
|
-
if str(property.get("name", "")) == "text" and
|
|
340
|
+
if str(property.get("name", "")) == "text" and Read.as_int(property.get("type", 0)) == TYPE_STRING:
|
|
340
341
|
return str(node.get("text")).strip_edges()
|
|
341
342
|
return ""
|
|
342
343
|
|
|
@@ -374,7 +375,8 @@ func get_rect(params: Dictionary) -> Dictionary:
|
|
|
374
375
|
"window": _values.serialize(window_position),
|
|
375
376
|
}
|
|
376
377
|
if node is Node3D:
|
|
377
|
-
|
|
378
|
+
var spatial: Node3D = node
|
|
379
|
+
return _in_the_frame(node_path, spatial)
|
|
378
380
|
return {
|
|
379
381
|
"type": "error", "message": "%s is a %s, which has no place on screen" % [node_path, node.get_class()]
|
|
380
382
|
}
|
|
@@ -573,7 +575,8 @@ static func _can_read(holder: Variant, named: String) -> bool:
|
|
|
573
575
|
var map: Dictionary = holder
|
|
574
576
|
return map.has(named) or map.has(StringName(named))
|
|
575
577
|
if holder is Object:
|
|
576
|
-
|
|
578
|
+
var object: Object = holder
|
|
579
|
+
return _has_property(object, named)
|
|
577
580
|
return false
|
|
578
581
|
|
|
579
582
|
|
|
@@ -638,7 +641,7 @@ static func _nothing_there(holder: Variant, named: String, called: String) -> St
|
|
|
638
641
|
var keys: Array = map.keys()
|
|
639
642
|
# Eight of them, because a map keyed by something unexpected is told by the first few and a
|
|
640
643
|
# map of two hundred would bury the sentence saying which key was missing.
|
|
641
|
-
var some:
|
|
644
|
+
var some: Array[String] = []
|
|
642
645
|
for key: Variant in keys.slice(0, 8):
|
|
643
646
|
some.append(str(key))
|
|
644
647
|
var rest: String = ""
|
|
@@ -3,6 +3,8 @@ extends RefCounted
|
|
|
3
3
|
## What crosses the wire between the game and the server, in both directions: Godot values
|
|
4
4
|
## as JSON-safe dictionaries, and JSON back into the values a property or parameter wants.
|
|
5
5
|
|
|
6
|
+
const Read = preload("reading.gd")
|
|
7
|
+
|
|
6
8
|
# What each Godot type becomes on the wire, keyed on typeof() rather than written as a chain of
|
|
7
9
|
# `is` tests whose order has to be trusted: Resource had to be tested before Object, or every
|
|
8
10
|
# resource came back as a bare class name with its path dropped.
|
|
@@ -190,17 +192,30 @@ func deserialize(value: Variant) -> Variant:
|
|
|
190
192
|
|
|
191
193
|
match fields["_type"]:
|
|
192
194
|
"Vector2":
|
|
193
|
-
return Vector2(fields.get("x", 0), fields.get("y", 0))
|
|
195
|
+
return Vector2(Read.as_float(fields.get("x", 0)), Read.as_float(fields.get("y", 0)))
|
|
194
196
|
"Vector3":
|
|
195
|
-
return Vector3(
|
|
197
|
+
return Vector3(
|
|
198
|
+
Read.as_float(fields.get("x", 0)),
|
|
199
|
+
Read.as_float(fields.get("y", 0)),
|
|
200
|
+
Read.as_float(fields.get("z", 0))
|
|
201
|
+
)
|
|
196
202
|
"Vector2i":
|
|
197
|
-
return Vector2i(fields.get("x", 0), fields.get("y", 0))
|
|
203
|
+
return Vector2i(Read.as_int(fields.get("x", 0)), Read.as_int(fields.get("y", 0)))
|
|
198
204
|
"Vector3i":
|
|
199
|
-
return Vector3i(
|
|
205
|
+
return Vector3i(
|
|
206
|
+
Read.as_int(fields.get("x", 0)),
|
|
207
|
+
Read.as_int(fields.get("y", 0)),
|
|
208
|
+
Read.as_int(fields.get("z", 0))
|
|
209
|
+
)
|
|
200
210
|
"Color":
|
|
201
|
-
return Color(
|
|
211
|
+
return Color(
|
|
212
|
+
Read.as_float(fields.get("r", 0)),
|
|
213
|
+
Read.as_float(fields.get("g", 0)),
|
|
214
|
+
Read.as_float(fields.get("b", 0)),
|
|
215
|
+
Read.as_float(fields.get("a", 1), 1.0)
|
|
216
|
+
)
|
|
202
217
|
"NodePath":
|
|
203
|
-
return NodePath(fields.get("path", ""))
|
|
218
|
+
return NodePath(str(fields.get("path", "")))
|
|
204
219
|
return value
|
|
205
220
|
|
|
206
221
|
|
|
@@ -214,7 +229,7 @@ func parameter_type(object: Object, method: String, index: int) -> int:
|
|
|
214
229
|
if index < 0 or index >= params.size():
|
|
215
230
|
return TYPE_NIL
|
|
216
231
|
var parameter: Dictionary = params[index]
|
|
217
|
-
return
|
|
232
|
+
return Read.as_int(parameter.get("type", TYPE_NIL), TYPE_NIL)
|
|
218
233
|
return TYPE_NIL
|
|
219
234
|
|
|
220
235
|
|
|
@@ -231,7 +246,8 @@ func fitted(value: Variant, type: int) -> Variant:
|
|
|
231
246
|
return rebuilt
|
|
232
247
|
|
|
233
248
|
if rebuilt is String and type != TYPE_STRING:
|
|
234
|
-
var
|
|
249
|
+
var text: String = rebuilt
|
|
250
|
+
var parsed: Variant = JSON.parse_string(text)
|
|
235
251
|
if typeof(parsed) != TYPE_NIL and typeof(parsed) != TYPE_STRING:
|
|
236
252
|
rebuilt = parsed
|
|
237
253
|
|
|
@@ -4,6 +4,7 @@ extends RefCounted
|
|
|
4
4
|
## happened, or when the time ran out, so the caller never sleeps for a guessed length.
|
|
5
5
|
|
|
6
6
|
const Queries = preload("runtime_queries.gd")
|
|
7
|
+
const Read = preload("reading.gd")
|
|
7
8
|
const Values = preload("runtime_values.gd")
|
|
8
9
|
|
|
9
10
|
## The longest one wait may last, whatever the request says: past this the server has long
|
|
@@ -34,7 +35,7 @@ static func _out_of_range(named: String, asked: int, least: int, most: int) -> D
|
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
func wait_frames(params: Dictionary) -> Dictionary:
|
|
37
|
-
var frames: int =
|
|
38
|
+
var frames: int = Read.as_int(params.get("frames", 1), 1)
|
|
38
39
|
if frames < 1 or frames > MOST_FRAMES:
|
|
39
40
|
return _out_of_range("frames", frames, 1, MOST_FRAMES)
|
|
40
41
|
var started: int = Time.get_ticks_msec()
|
|
@@ -47,7 +48,7 @@ func wait_frames(params: Dictionary) -> Dictionary:
|
|
|
47
48
|
func wait_signal(params: Dictionary) -> Dictionary:
|
|
48
49
|
var node_path: String = str(params.get("path", ""))
|
|
49
50
|
var signal_name: String = str(params.get("signal", ""))
|
|
50
|
-
var timeout_ms: int =
|
|
51
|
+
var timeout_ms: int = Read.as_int(params.get("timeout_ms", 5000), 5000)
|
|
51
52
|
if timeout_ms < 1 or timeout_ms > CEILING_MSEC:
|
|
52
53
|
return _out_of_range("timeout_ms", timeout_ms, 1, CEILING_MSEC)
|
|
53
54
|
if node_path.is_empty() or signal_name.is_empty():
|
|
@@ -63,7 +64,9 @@ func wait_signal(params: Dictionary) -> Dictionary:
|
|
|
63
64
|
var catcher: SignalCatcher = SignalCatcher.new()
|
|
64
65
|
catcher.arity = _signal_arity(node, signal_name)
|
|
65
66
|
var callable: Callable = catcher._on_fired
|
|
66
|
-
node.connect(signal_name, callable, CONNECT_ONE_SHOT)
|
|
67
|
+
var listening: Error = node.connect(signal_name, callable, CONNECT_ONE_SHOT)
|
|
68
|
+
if listening != OK:
|
|
69
|
+
return {"type": "error", "message": "Could not listen to signal: " + signal_name}
|
|
67
70
|
var started: int = Time.get_ticks_msec()
|
|
68
71
|
while not catcher.fired and Time.get_ticks_msec() - started < timeout_ms:
|
|
69
72
|
await _host.get_tree().process_frame
|
|
@@ -86,7 +89,7 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
86
89
|
var node_path: String = str(params.get("path", ""))
|
|
87
90
|
var property: String = str(params.get("property", ""))
|
|
88
91
|
var says: String = str(params.get("says", ""))
|
|
89
|
-
var timeout_ms: int =
|
|
92
|
+
var timeout_ms: int = Read.as_int(params.get("timeout_ms", 5000), 5000)
|
|
90
93
|
if timeout_ms < 1 or timeout_ms > CEILING_MSEC:
|
|
91
94
|
return _out_of_range("timeout_ms", timeout_ms, 1, CEILING_MSEC)
|
|
92
95
|
if node_path.is_empty():
|
|
@@ -3,6 +3,7 @@ extends RefCounted
|
|
|
3
3
|
# The bus layout is a project resource the engine loads at startup and this process holds in
|
|
4
4
|
# memory, so every change is written back to that file or it is gone with the process.
|
|
5
5
|
|
|
6
|
+
const Read = preload("reading.gd")
|
|
6
7
|
const Log = preload("logger.gd")
|
|
7
8
|
|
|
8
9
|
var _log: Log
|
|
@@ -16,7 +17,7 @@ func create_audio_bus(params: Dictionary) -> Dictionary:
|
|
|
16
17
|
var bus_name: String = str(params.get("bus_name", ""))
|
|
17
18
|
if bus_name.is_empty():
|
|
18
19
|
return _log.failure("bus_name is required")
|
|
19
|
-
var parent_idx: int =
|
|
20
|
+
var parent_idx: int = Read.as_int(params.get("parent_bus_index", 0))
|
|
20
21
|
if parent_idx < 0 or parent_idx >= AudioServer.bus_count:
|
|
21
22
|
return _log.failure("No bus at index " + str(parent_idx))
|
|
22
23
|
|
|
@@ -40,10 +41,10 @@ func get_audio_buses(_params: Dictionary) -> Dictionary:
|
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
func set_audio_bus_effect(params: Dictionary) -> Dictionary:
|
|
43
|
-
var bus_idx: int =
|
|
44
|
-
var effect_idx: int =
|
|
44
|
+
var bus_idx: int = Read.as_int(params.get("bus_index", 0))
|
|
45
|
+
var effect_idx: int = Read.as_int(params.get("effect_index", 0))
|
|
45
46
|
var effect_type: String = str(params.get("effect_type", ""))
|
|
46
|
-
var enabled: bool =
|
|
47
|
+
var enabled: bool = Read.as_bool(params.get("enabled", true), true)
|
|
47
48
|
if bus_idx < 0 or bus_idx >= AudioServer.bus_count:
|
|
48
49
|
return _log.failure("No bus at index " + str(bus_idx))
|
|
49
50
|
|
|
@@ -65,8 +66,8 @@ func set_audio_bus_effect(params: Dictionary) -> Dictionary:
|
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
func set_audio_bus_volume(params: Dictionary) -> Dictionary:
|
|
68
|
-
var bus_idx: int =
|
|
69
|
-
var volume_db: float =
|
|
69
|
+
var bus_idx: int = Read.as_int(params.get("bus_index", 0))
|
|
70
|
+
var volume_db: float = Read.as_float(params.get("volume_db", 0.0))
|
|
70
71
|
if bus_idx < 0 or bus_idx >= AudioServer.bus_count:
|
|
71
72
|
return _log.failure("No bus at index " + str(bus_idx))
|
|
72
73
|
|
|
@@ -5,6 +5,8 @@ extends RefCounted
|
|
|
5
5
|
# A game started from a stale editor then cannot resolve any class_name written since, and the
|
|
6
6
|
# engine reads the same file headless, so the list is rebuilt here from the scripts themselves.
|
|
7
7
|
|
|
8
|
+
const Patterns = preload("patterns.gd")
|
|
9
|
+
const Read = preload("reading.gd")
|
|
8
10
|
const FileWalk = preload("file_walk.gd")
|
|
9
11
|
const Log = preload("logger.gd")
|
|
10
12
|
|
|
@@ -33,7 +35,11 @@ func refresh_class_cache(_params: Dictionary) -> Dictionary:
|
|
|
33
35
|
var after: Dictionary = _entries_by_class(entries)
|
|
34
36
|
var config: ConfigFile = ConfigFile.new()
|
|
35
37
|
config.set_value("", "list", entries)
|
|
36
|
-
DirAccess.make_dir_recursive_absolute(
|
|
38
|
+
var made: Error = DirAccess.make_dir_recursive_absolute(
|
|
39
|
+
ProjectSettings.globalize_path(CACHE_PATH.get_base_dir())
|
|
40
|
+
)
|
|
41
|
+
if made != OK and made != ERR_ALREADY_EXISTS:
|
|
42
|
+
return _log.failure("Could not make " + CACHE_PATH.get_base_dir() + ": " + error_string(made))
|
|
37
43
|
var err: Error = config.save(CACHE_PATH)
|
|
38
44
|
if err != OK:
|
|
39
45
|
return _log.failure("Failed to write " + CACHE_PATH + ": " + error_string(err))
|
|
@@ -80,8 +86,8 @@ func _entry_for(path: String, skipped: Array[Dictionary]) -> Dictionary:
|
|
|
80
86
|
"base": StringName(base),
|
|
81
87
|
"class": StringName(declared),
|
|
82
88
|
"icon": str(header.get("icon", "")),
|
|
83
|
-
"is_abstract":
|
|
84
|
-
"is_tool":
|
|
89
|
+
"is_abstract": Read.as_bool(header.get("abstract", false)),
|
|
90
|
+
"is_tool": Read.as_bool(header.get("tool", false)),
|
|
85
91
|
"language": StringName("GDScript"),
|
|
86
92
|
"path": path,
|
|
87
93
|
}
|
|
@@ -123,12 +129,11 @@ func _header_of(path: String) -> Dictionary:
|
|
|
123
129
|
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
124
130
|
if not file:
|
|
125
131
|
return header
|
|
126
|
-
var annotation: RegEx =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
var extends_line: RegEx =
|
|
131
|
-
extends_line.compile("^extends\\s+(\\S+)")
|
|
132
|
+
var annotation: RegEx = Patterns.compiled('^@([a-z_]+)(?:\\(\\s*(?:"([^"]*)")?[^)]*\\))?\\s*')
|
|
133
|
+
var class_line: RegEx = Patterns.compiled(
|
|
134
|
+
"^class_name\\s+([A-Za-z_][A-Za-z0-9_]*)(?:\\s+extends\\s+(\\S+))?"
|
|
135
|
+
)
|
|
136
|
+
var extends_line: RegEx = Patterns.compiled("^extends\\s+(\\S+)")
|
|
132
137
|
while not file.eof_reached():
|
|
133
138
|
var rest: String = file.get_line().strip_edges()
|
|
134
139
|
# Annotations may share a line with what they annotate, as in `@abstract class_name X`.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
+
const Read = preload("reading.gd")
|
|
3
4
|
const Log = preload("logger.gd")
|
|
4
5
|
|
|
5
6
|
# The base class each category name stands for.
|
|
@@ -27,7 +28,7 @@ func _init(p_log: Log) -> void:
|
|
|
27
28
|
func query_classes(params: Dictionary) -> Dictionary:
|
|
28
29
|
var filter: String = str(params.get("filter", ""))
|
|
29
30
|
var category: String = str(params.get("category", ""))
|
|
30
|
-
var instantiable_only: bool =
|
|
31
|
+
var instantiable_only: bool = Read.as_bool(params.get("instantiable_only", false))
|
|
31
32
|
|
|
32
33
|
_log.info(
|
|
33
34
|
(
|
|
@@ -82,7 +83,7 @@ func query_classes(params: Dictionary) -> Dictionary:
|
|
|
82
83
|
# Query detailed info about a specific class from ClassDB
|
|
83
84
|
func query_class_info(params: Dictionary) -> Dictionary:
|
|
84
85
|
var class_name_str: String = str(params.get("class_name", ""))
|
|
85
|
-
var include_inherited: bool =
|
|
86
|
+
var include_inherited: bool = Read.as_bool(params.get("include_inherited", false))
|
|
86
87
|
|
|
87
88
|
_log.info(
|
|
88
89
|
"Querying class info for: " + class_name_str + " (include_inherited: " + str(include_inherited) + ")"
|
|
@@ -220,7 +221,7 @@ func _properties_of(class_name_str: String, include_inherited: bool) -> Array[Di
|
|
|
220
221
|
|
|
221
222
|
for p: Dictionary in ClassDB.class_get_property_list(class_name_str, not include_inherited):
|
|
222
223
|
# A category, group or subgroup is an editor heading rather than a property.
|
|
223
|
-
var usage: int =
|
|
224
|
+
var usage: int = Read.as_int(p.get("usage", 0))
|
|
224
225
|
if usage & PROPERTY_USAGE_CATEGORY or usage & PROPERTY_USAGE_GROUP or usage & PROPERTY_USAGE_SUBGROUP:
|
|
225
226
|
continue
|
|
226
227
|
properties.append(
|