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.
Files changed (36) hide show
  1. package/build/cli.js +11 -2
  2. package/build/godot/addons/auto_reload/auto_reload.gd +9 -2
  3. package/build/godot/addons/gdharness_editor/bridge_client.gd +11 -5
  4. package/build/godot/addons/gdharness_editor/plugin.gd +17 -4
  5. package/build/godot/addons/gdharness_editor/reading.gd +64 -0
  6. package/build/godot/addons/gdharness_editor/reading.gd.uid +1 -0
  7. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +30 -12
  8. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +5 -2
  9. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +3 -1
  10. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +50 -27
  11. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +92 -42
  12. package/build/godot/addons/gdharness_runtime/reading.gd +64 -0
  13. package/build/godot/addons/gdharness_runtime/reading.gd.uid +1 -0
  14. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +24 -10
  15. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +7 -5
  16. package/build/godot/addons/gdharness_runtime/runtime_input.gd +24 -22
  17. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +16 -13
  18. package/build/godot/addons/gdharness_runtime/runtime_values.gd +24 -8
  19. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +7 -4
  20. package/build/godot/operations/audio_buses.gd +7 -6
  21. package/build/godot/operations/class_cache.gd +14 -9
  22. package/build/godot/operations/classdb_queries.gd +4 -3
  23. package/build/godot/operations/dependencies.gd +19 -16
  24. package/build/godot/operations/file_walk.gd +4 -2
  25. package/build/godot/operations/gdscript_analysis.gd +2 -3
  26. package/build/godot/operations/gdscript_authoring.gd +20 -8
  27. package/build/godot/operations/godot_operations.gd +7 -2
  28. package/build/godot/operations/import_pipeline.gd +5 -4
  29. package/build/godot/operations/input_actions.gd +13 -9
  30. package/build/godot/operations/patterns.gd +22 -0
  31. package/build/godot/operations/plugins.gd +10 -6
  32. package/build/godot/operations/project_config.gd +3 -2
  33. package/build/godot/operations/reading.gd +66 -0
  34. package/build/godot/operations/serialisation.gd +41 -9
  35. package/build/index.js +8 -1
  36. 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 typeof(value) == TYPE_DICTIONARY:
93
- var tagged: Array = _parse_tagged_dictionary(value)
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(value, expected_type)
97
- if typeof(value) == TYPE_ARRAY:
98
- return _parse_array(value, expected_type)
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 [true, Vector3(value.get("x", 0), value.get("y", 0), value.get("z", 0))]
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 [true, Color(value.get("r", 1), value.get("g", 1), value.get("b", 1), value.get("a", 1))]
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 [true, Vector3i(value.get("x", 0), value.get("y", 0), value.get("z", 0))]
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(value.get("x", 0), value.get("y", 0), value.get("width", 0), value.get("height", 0))
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(x.get("x", 1), x.get("y", 0), x.get("z", 0)),
193
- Vector3(y.get("x", 0), y.get("y", 1), y.get("z", 0)),
194
- Vector3(z.get("x", 0), z.get("y", 0), z.get("z", 1))
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, Vector3(o.get("x", 0), o.get("y", 0), o.get("z", 0)))]
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(value.get("x", 0), value.get("y", 0))
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(value.get("x", 0), value.get("y", 0))
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(value.get("x", 0), value.get("y", 0), value.get("z", 0))
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(value.get("x", 0), value.get("y", 0), value.get("z", 0))
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(value.get("r", 1), value.get("g", 1), value.get("b", 1), value.get("a", 1))
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
- value.get("x", 0), value.get("y", 0), value.get("width", 0), value.get("height", 0)
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.get("path", ""))
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 int(prop.get("type", TYPE_NIL))
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 = String(raw)
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 = String(raw_properties)
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 (int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
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 = int(args.get("depth", -1))
443
- var include_properties: bool = bool(args.get("includeProperties", false))
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 = bool(args.get("includeDefaults", false))
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 = int(p.get("usage", 0))
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 = int(args.get("flags", 0)) | Object.CONNECT_PERSIST
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": int(conn.get("flags", 0))
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 bool(args.get("statusOnly", false)):
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.poll()
114
- if client.get_status() != StreamPeerTCP.STATUS_CONNECTED:
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
- _pending.erase(client)
128
- _outgoing.erase(client)
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(int(sent[1]))
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 bool(ProjectSettings.get_setting(SCRIPT_RUNS_SETTING, false)):
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 = int(ProjectSettings.get_setting(PORT_SETTING, 0))
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, params)
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 = String(params.get("viewportPath", ""))
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
- return _capture(node, params)
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 = String(params.get("output_path", ""))
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 = int(params.get("width", 0))
63
- var height: int = int(params.get("height", 0))
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 = String(params.get("action", ""))
44
- var held: bool = bool(params.get("pressed", true))
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 = float(params.get("strength", 1.0))
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 = bool(params.get("pressed", true))
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 = String(params.get("key_label", ""))
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 int(keycode_raw)
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 = bool(params.get("shift", false))
113
- event.ctrl_pressed = bool(params.get("ctrl", false))
114
- event.alt_pressed = bool(params.get("alt", false))
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 = String(params.get("text", ""))
165
- var over: bool = bool(params.get("replace", false))
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(float(params[x_key]), float(params[y_key]))
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(float(pair[0]), float(pair[1]))
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 = bool(params.get("pressed", true))
329
- var double: bool = bool(params.get("doubleClick", false))
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
- return await _click_in_the_world(node_path, node, params)
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 = bool(params.get("double", false))
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 = int(params.get("index", -1))
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) -> PackedStringArray:
691
- var said: PackedStringArray = PackedStringArray()
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 = bool(params.get("double", false))
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 int(raw)
844
+ return Read.as_int(raw, -1)