gdharness 0.12.16 → 0.12.18
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 +11 -2
- 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 +3 -2
- package/build/godot/operations/reading.gd +66 -0
- package/build/godot/operations/serialisation.gd +41 -9
- package/build/index.js +8 -1
- package/package.json +1 -1
|
@@ -3,6 +3,8 @@ extends Node
|
|
|
3
3
|
|
|
4
4
|
## Scenes and their nodes, edited in the open editor and read back from what it holds.
|
|
5
5
|
|
|
6
|
+
const Read = preload("../reading.gd")
|
|
7
|
+
|
|
6
8
|
var _editor_plugin: EditorPlugin = null
|
|
7
9
|
|
|
8
10
|
|
|
@@ -89,13 +91,15 @@ func _find_node(root: Node, path: String) -> Node:
|
|
|
89
91
|
## declares, or a bare array positional for a vector. Each is its own function; asking all three
|
|
90
92
|
## in one was twenty-four exits deep and impossible to follow.
|
|
91
93
|
func _parse_value(value: Variant, expected_type: int = TYPE_NIL) -> Variant:
|
|
92
|
-
if
|
|
93
|
-
var
|
|
94
|
+
if value is Dictionary:
|
|
95
|
+
var fields: Dictionary = value
|
|
96
|
+
var tagged: Array = _parse_tagged_dictionary(fields)
|
|
94
97
|
if tagged[0]:
|
|
95
98
|
return tagged[1]
|
|
96
|
-
return _parse_shaped_dictionary(
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
+
return _parse_shaped_dictionary(fields, expected_type)
|
|
100
|
+
if value is Array:
|
|
101
|
+
var items: Array = value
|
|
102
|
+
return _parse_array(items, expected_type)
|
|
99
103
|
return value
|
|
100
104
|
|
|
101
105
|
|
|
@@ -114,26 +118,53 @@ func _parse_tagged_dictionary(value: Dictionary) -> Array:
|
|
|
114
118
|
|
|
115
119
|
match type_tag:
|
|
116
120
|
"Vector2":
|
|
117
|
-
return [true, Vector2(value.get("x", 0), value.get("y", 0))]
|
|
121
|
+
return [true, Vector2(Read.as_float(value.get("x", 0)), Read.as_float(value.get("y", 0)))]
|
|
118
122
|
"Vector3":
|
|
119
|
-
return [
|
|
123
|
+
return [
|
|
124
|
+
true,
|
|
125
|
+
Vector3(
|
|
126
|
+
Read.as_float(value.get("x", 0)),
|
|
127
|
+
Read.as_float(value.get("y", 0)),
|
|
128
|
+
Read.as_float(value.get("z", 0))
|
|
129
|
+
)
|
|
130
|
+
]
|
|
120
131
|
"Color":
|
|
121
|
-
return [
|
|
132
|
+
return [
|
|
133
|
+
true,
|
|
134
|
+
Color(
|
|
135
|
+
Read.as_float(value.get("r", 1), 1.0),
|
|
136
|
+
Read.as_float(value.get("g", 1), 1.0),
|
|
137
|
+
Read.as_float(value.get("b", 1), 1.0),
|
|
138
|
+
Read.as_float(value.get("a", 1), 1.0)
|
|
139
|
+
)
|
|
140
|
+
]
|
|
122
141
|
"Vector2i":
|
|
123
|
-
return [true, Vector2i(value.get("x", 0), value.get("y", 0))]
|
|
142
|
+
return [true, Vector2i(Read.as_int(value.get("x", 0)), Read.as_int(value.get("y", 0)))]
|
|
124
143
|
"Vector3i":
|
|
125
|
-
return [
|
|
144
|
+
return [
|
|
145
|
+
true,
|
|
146
|
+
Vector3i(
|
|
147
|
+
Read.as_int(value.get("x", 0)),
|
|
148
|
+
Read.as_int(value.get("y", 0)),
|
|
149
|
+
Read.as_int(value.get("z", 0))
|
|
150
|
+
)
|
|
151
|
+
]
|
|
126
152
|
"Rect2":
|
|
127
153
|
return [
|
|
128
154
|
true,
|
|
129
|
-
Rect2(
|
|
155
|
+
Rect2(
|
|
156
|
+
Read.as_float(value.get("x", 0)),
|
|
157
|
+
Read.as_float(value.get("y", 0)),
|
|
158
|
+
Read.as_float(value.get("width", 0)),
|
|
159
|
+
Read.as_float(value.get("height", 0))
|
|
160
|
+
)
|
|
130
161
|
]
|
|
131
162
|
"Transform2D":
|
|
132
163
|
return _parse_transform2d(value)
|
|
133
164
|
"Transform3D":
|
|
134
165
|
return _parse_transform3d(value)
|
|
135
166
|
"NodePath":
|
|
136
|
-
return [true, NodePath(value.get("path", ""))]
|
|
167
|
+
return [true, NodePath(str(value.get("path", "")))]
|
|
137
168
|
"Resource":
|
|
138
169
|
var resource_path: String = str(value.get("path", ""))
|
|
139
170
|
return [true, null if resource_path.is_empty() else load(resource_path)]
|
|
@@ -172,9 +203,9 @@ func _parse_transform2d(value: Dictionary) -> Array:
|
|
|
172
203
|
return [
|
|
173
204
|
true,
|
|
174
205
|
Transform2D(
|
|
175
|
-
Vector2(basis_x.get("x", 1), basis_x.get("y", 0)),
|
|
176
|
-
Vector2(basis_y.get("x", 0), basis_y.get("y", 1)),
|
|
177
|
-
Vector2(origin.get("x", 0), origin.get("y", 0))
|
|
206
|
+
Vector2(Read.as_float(basis_x.get("x", 1), 1.0), Read.as_float(basis_x.get("y", 0))),
|
|
207
|
+
Vector2(Read.as_float(basis_y.get("x", 0)), Read.as_float(basis_y.get("y", 1), 1.0)),
|
|
208
|
+
Vector2(Read.as_float(origin.get("x", 0)), Read.as_float(origin.get("y", 0)))
|
|
178
209
|
)
|
|
179
210
|
]
|
|
180
211
|
|
|
@@ -189,11 +220,18 @@ func _parse_transform3d(value: Dictionary) -> Array:
|
|
|
189
220
|
var y: Dictionary = b.get("y", {})
|
|
190
221
|
var z: Dictionary = b.get("z", {})
|
|
191
222
|
var basis: Basis = Basis(
|
|
192
|
-
Vector3(
|
|
193
|
-
|
|
194
|
-
|
|
223
|
+
Vector3(
|
|
224
|
+
Read.as_float(x.get("x", 1), 1.0), Read.as_float(x.get("y", 0)), Read.as_float(x.get("z", 0))
|
|
225
|
+
),
|
|
226
|
+
Vector3(
|
|
227
|
+
Read.as_float(y.get("x", 0)), Read.as_float(y.get("y", 1), 1.0), Read.as_float(y.get("z", 0))
|
|
228
|
+
),
|
|
229
|
+
Vector3(Read.as_float(z.get("x", 0)), Read.as_float(z.get("y", 0)), Read.as_float(z.get("z", 1), 1.0))
|
|
230
|
+
)
|
|
231
|
+
var origin: Vector3 = Vector3(
|
|
232
|
+
Read.as_float(o.get("x", 0)), Read.as_float(o.get("y", 0)), Read.as_float(o.get("z", 0))
|
|
195
233
|
)
|
|
196
|
-
return [true, Transform3D(basis,
|
|
234
|
+
return [true, Transform3D(basis, origin)]
|
|
197
235
|
|
|
198
236
|
|
|
199
237
|
## A dictionary with no tag, read against the type the property declares. Falls back to the
|
|
@@ -202,27 +240,37 @@ func _parse_shaped_dictionary(value: Dictionary, expected_type: int) -> Variant:
|
|
|
202
240
|
match expected_type:
|
|
203
241
|
TYPE_VECTOR2:
|
|
204
242
|
if value.has("x") and value.has("y"):
|
|
205
|
-
return Vector2(
|
|
243
|
+
return Vector2(Read.as_float(value["x"]), Read.as_float(value["y"]))
|
|
206
244
|
TYPE_VECTOR2I:
|
|
207
245
|
if value.has("x") and value.has("y"):
|
|
208
|
-
return Vector2i(
|
|
246
|
+
return Vector2i(Read.as_int(value["x"]), Read.as_int(value["y"]))
|
|
209
247
|
TYPE_VECTOR3:
|
|
210
248
|
if value.has("x") and value.has("y") and value.has("z"):
|
|
211
|
-
return Vector3(
|
|
249
|
+
return Vector3(
|
|
250
|
+
Read.as_float(value["x"]), Read.as_float(value["y"]), Read.as_float(value["z"])
|
|
251
|
+
)
|
|
212
252
|
TYPE_VECTOR3I:
|
|
213
253
|
if value.has("x") and value.has("y") and value.has("z"):
|
|
214
|
-
return Vector3i(
|
|
254
|
+
return Vector3i(Read.as_int(value["x"]), Read.as_int(value["y"]), Read.as_int(value["z"]))
|
|
215
255
|
TYPE_COLOR:
|
|
216
256
|
if value.has("r") and value.has("g") and value.has("b"):
|
|
217
|
-
return Color(
|
|
257
|
+
return Color(
|
|
258
|
+
Read.as_float(value["r"]),
|
|
259
|
+
Read.as_float(value["g"]),
|
|
260
|
+
Read.as_float(value["b"]),
|
|
261
|
+
Read.as_float(value.get("a", 1), 1.0)
|
|
262
|
+
)
|
|
218
263
|
TYPE_RECT2:
|
|
219
264
|
if value.has("x") and value.has("y") and value.has("width") and value.has("height"):
|
|
220
265
|
return Rect2(
|
|
221
|
-
|
|
266
|
+
Read.as_float(value["x"]),
|
|
267
|
+
Read.as_float(value["y"]),
|
|
268
|
+
Read.as_float(value["width"]),
|
|
269
|
+
Read.as_float(value["height"])
|
|
222
270
|
)
|
|
223
271
|
TYPE_NODE_PATH:
|
|
224
272
|
if value.has("path"):
|
|
225
|
-
return NodePath(value
|
|
273
|
+
return NodePath(str(value["path"]))
|
|
226
274
|
return value
|
|
227
275
|
|
|
228
276
|
|
|
@@ -231,23 +279,23 @@ func _parse_array(value: Array, expected_type: int) -> Variant:
|
|
|
231
279
|
match expected_type:
|
|
232
280
|
TYPE_VECTOR2:
|
|
233
281
|
if value.size() >= 2:
|
|
234
|
-
return Vector2(value[0], value[1])
|
|
282
|
+
return Vector2(Read.as_float(value[0]), Read.as_float(value[1]))
|
|
235
283
|
TYPE_VECTOR2I:
|
|
236
284
|
if value.size() >= 2:
|
|
237
|
-
return Vector2i(value[0], value[1])
|
|
285
|
+
return Vector2i(Read.as_int(value[0]), Read.as_int(value[1]))
|
|
238
286
|
TYPE_VECTOR3:
|
|
239
287
|
if value.size() >= 3:
|
|
240
|
-
return Vector3(value[0], value[1], value[2])
|
|
288
|
+
return Vector3(Read.as_float(value[0]), Read.as_float(value[1]), Read.as_float(value[2]))
|
|
241
289
|
TYPE_VECTOR3I:
|
|
242
290
|
if value.size() >= 3:
|
|
243
|
-
return Vector3i(value[0], value[1], value[2])
|
|
291
|
+
return Vector3i(Read.as_int(value[0]), Read.as_int(value[1]), Read.as_int(value[2]))
|
|
244
292
|
return value.map(func(item: Variant) -> Variant: return _parse_value(item))
|
|
245
293
|
|
|
246
294
|
|
|
247
295
|
func _get_property_type(node: Node, prop_name: String) -> int:
|
|
248
296
|
for prop: Dictionary in node.get_property_list():
|
|
249
297
|
if str(prop.get("name", "")) == prop_name:
|
|
250
|
-
return
|
|
298
|
+
return Read.as_int(prop.get("type", TYPE_NIL), TYPE_NIL)
|
|
251
299
|
return TYPE_NIL
|
|
252
300
|
|
|
253
301
|
|
|
@@ -316,7 +364,7 @@ func _set_node_properties(node: Node, properties: Dictionary) -> String:
|
|
|
316
364
|
# A resource-valued property takes the path of one, which is how a caller names a
|
|
317
365
|
# TileSet, a material or a theme: there is no other way to hand a tool a Resource.
|
|
318
366
|
if expected_type == TYPE_OBJECT and typeof(raw) == TYPE_STRING:
|
|
319
|
-
var path: String =
|
|
367
|
+
var path: String = str(raw)
|
|
320
368
|
# The project boundary is enforced here as well as on the server, because only the
|
|
321
369
|
# engine knows that this property is one holding a path: an absolute path and a
|
|
322
370
|
# user:// one both load, and neither names a file this project owns.
|
|
@@ -344,7 +392,7 @@ func _parse_properties_arg(raw_properties: Variant) -> Dictionary:
|
|
|
344
392
|
if typeof(raw_properties) == TYPE_DICTIONARY:
|
|
345
393
|
return raw_properties
|
|
346
394
|
if typeof(raw_properties) == TYPE_STRING:
|
|
347
|
-
var text: String =
|
|
395
|
+
var text: String = str(raw_properties)
|
|
348
396
|
if text.strip_edges().is_empty():
|
|
349
397
|
return {}
|
|
350
398
|
var parsed: Variant = JSON.parse_string(text)
|
|
@@ -356,7 +404,9 @@ func _parse_properties_arg(raw_properties: Variant) -> Dictionary:
|
|
|
356
404
|
func _ensure_parent_dir_for_scene(scene_path: String) -> void:
|
|
357
405
|
var base_dir: String = scene_path.get_base_dir()
|
|
358
406
|
if not DirAccess.dir_exists_absolute(base_dir):
|
|
359
|
-
DirAccess.make_dir_recursive_absolute(base_dir)
|
|
407
|
+
var made: Error = DirAccess.make_dir_recursive_absolute(base_dir)
|
|
408
|
+
if made != OK:
|
|
409
|
+
push_error("gdharness: could not create " + base_dir + ": " + error_string(made))
|
|
360
410
|
|
|
361
411
|
|
|
362
412
|
func _set_owner_recursive(node: Node, scene_owner: Node) -> void:
|
|
@@ -376,7 +426,7 @@ func _build_node_tree(
|
|
|
376
426
|
if include_properties:
|
|
377
427
|
var props: Dictionary = {}
|
|
378
428
|
for p: Dictionary in node.get_property_list():
|
|
379
|
-
if not (
|
|
429
|
+
if not (Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
380
430
|
continue
|
|
381
431
|
var pn: String = str(p.get("name", ""))
|
|
382
432
|
if pn.is_empty():
|
|
@@ -439,8 +489,8 @@ func create_scene(args: Dictionary) -> Dictionary:
|
|
|
439
489
|
func list_scene_nodes(args: Dictionary) -> Dictionary:
|
|
440
490
|
var project_path: String = str(args.get("projectPath", ""))
|
|
441
491
|
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
442
|
-
var depth: int =
|
|
443
|
-
var include_properties: bool =
|
|
492
|
+
var depth: int = Read.as_int(args.get("depth", -1), -1)
|
|
493
|
+
var include_properties: bool = Read.as_bool(args.get("includeProperties", false))
|
|
444
494
|
|
|
445
495
|
var loaded: Array = _load_scene(scene_path)
|
|
446
496
|
var refused: Dictionary = loaded[1]
|
|
@@ -652,7 +702,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
|
|
|
652
702
|
var project_path: String = str(args.get("projectPath", ""))
|
|
653
703
|
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
654
704
|
var node_path: String = str(args.get("nodePath", "."))
|
|
655
|
-
var include_defaults: bool =
|
|
705
|
+
var include_defaults: bool = Read.as_bool(args.get("includeDefaults", false))
|
|
656
706
|
|
|
657
707
|
var loaded: Array = _load_scene(scene_path)
|
|
658
708
|
var refused: Dictionary = loaded[1]
|
|
@@ -671,7 +721,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
|
|
|
671
721
|
|
|
672
722
|
var props: Dictionary = {}
|
|
673
723
|
for p: Dictionary in node.get_property_list():
|
|
674
|
-
var usage: int =
|
|
724
|
+
var usage: int = Read.as_int(p.get("usage", 0))
|
|
675
725
|
if not (usage & PROPERTY_USAGE_STORAGE):
|
|
676
726
|
continue
|
|
677
727
|
var prop_name: String = str(p.get("name", ""))
|
|
@@ -721,7 +771,7 @@ func connect_signal(args: Dictionary) -> Dictionary:
|
|
|
721
771
|
var method_name: String = str(args.get("methodName", ""))
|
|
722
772
|
# A connection without CONNECT_PERSIST is a runtime one, and PackedScene.pack drops those on
|
|
723
773
|
# the way out: without this the scene saves unchanged and this answers success over nothing.
|
|
724
|
-
var flags: int =
|
|
774
|
+
var flags: int = Read.as_int(args.get("flags", 0)) | Object.CONNECT_PERSIST
|
|
725
775
|
|
|
726
776
|
if (
|
|
727
777
|
source_node_path.is_empty()
|
|
@@ -854,7 +904,7 @@ func list_connections(args: Dictionary) -> Dictionary:
|
|
|
854
904
|
"signalName": signal_name,
|
|
855
905
|
"targetNodePath": target_path,
|
|
856
906
|
"methodName": str(callable.get_method()),
|
|
857
|
-
"flags":
|
|
907
|
+
"flags": Read.as_int(conn.get("flags", 0))
|
|
858
908
|
}
|
|
859
909
|
)
|
|
860
910
|
|
|
@@ -877,7 +927,7 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
|
|
|
877
927
|
return {"ok": false, "error": "Editor plugin unavailable"}
|
|
878
928
|
|
|
879
929
|
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
880
|
-
if not
|
|
930
|
+
if not Read.as_bool(args.get("statusOnly", false)):
|
|
881
931
|
filesystem.scan()
|
|
882
932
|
|
|
883
933
|
# Importing is reported separately from scanning, and a class is not registered until
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
# A value out of a request that arrived as JSON, as the type the caller wants.
|
|
4
|
+
#
|
|
5
|
+
# `int(value)`, `float(value)` and `bool(value)` take a Variant, and a project holding
|
|
6
|
+
# `unsafe_call_argument` at error level will not compile a script that hands one over. The
|
|
7
|
+
# conversion is the same; what changes is that the type is established first, on a typed local, and
|
|
8
|
+
# the conversion happens from that.
|
|
9
|
+
#
|
|
10
|
+
# A copy rather than a preload of the one beside the operations, because an addon is installed as a
|
|
11
|
+
# directory and has to hold everything it uses. This one ships inside exported games, so it must not
|
|
12
|
+
# reach into the editor addon, which does not.
|
|
13
|
+
#
|
|
14
|
+
# `fallback` answers a value that is there and is null, which is a caller sending JSON null where a
|
|
15
|
+
# number was expected. Absent keys never reach here: the callers pass their own default to
|
|
16
|
+
# `Dictionary.get`, so what arrives is that default.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
static func as_int(value: Variant, fallback: int = 0) -> int:
|
|
20
|
+
if value is int:
|
|
21
|
+
var already: int = value
|
|
22
|
+
return already
|
|
23
|
+
if value is float:
|
|
24
|
+
var real: float = value
|
|
25
|
+
return int(real)
|
|
26
|
+
if value is bool:
|
|
27
|
+
var flag: bool = value
|
|
28
|
+
return 1 if flag else 0
|
|
29
|
+
if value is String:
|
|
30
|
+
var text: String = value
|
|
31
|
+
return text.to_int()
|
|
32
|
+
return fallback
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
static func as_float(value: Variant, fallback: float = 0.0) -> float:
|
|
36
|
+
if value is float:
|
|
37
|
+
var already: float = value
|
|
38
|
+
return already
|
|
39
|
+
if value is int:
|
|
40
|
+
var whole: int = value
|
|
41
|
+
return float(whole)
|
|
42
|
+
if value is bool:
|
|
43
|
+
var flag: bool = value
|
|
44
|
+
return 1.0 if flag else 0.0
|
|
45
|
+
if value is String:
|
|
46
|
+
var text: String = value
|
|
47
|
+
return text.to_float()
|
|
48
|
+
return fallback
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
static func as_bool(value: Variant, fallback: bool = false) -> bool:
|
|
52
|
+
if value is bool:
|
|
53
|
+
var already: bool = value
|
|
54
|
+
return already
|
|
55
|
+
if value is int:
|
|
56
|
+
var whole: int = value
|
|
57
|
+
return whole != 0
|
|
58
|
+
if value is float:
|
|
59
|
+
var real: float = value
|
|
60
|
+
return not is_zero_approx(real)
|
|
61
|
+
if value is String:
|
|
62
|
+
var text: String = value
|
|
63
|
+
return not text.is_empty()
|
|
64
|
+
return fallback
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://ch24s3ttpunch
|
|
@@ -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)
|