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.
Files changed (36) hide show
  1. package/build/cli.js +174 -56
  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 +41 -3
  33. package/build/godot/operations/reading.gd +66 -0
  34. package/build/godot/operations/serialisation.gd +41 -9
  35. package/build/index.js +147 -53
  36. package/package.json +1 -1
@@ -3,6 +3,8 @@ extends Node
3
3
 
4
4
  ## Animations and animation trees, edited in the open editor.
5
5
 
6
+ const Read = preload("../reading.gd")
7
+
6
8
  var _editor_plugin: EditorPlugin = null
7
9
 
8
10
 
@@ -75,12 +77,19 @@ func _parse_value(value: Variant) -> Variant:
75
77
  var t: Variant = fields.get("type", fields.get("_type", ""))
76
78
  match t:
77
79
  "Vector2":
78
- return Vector2(fields.get("x", 0), fields.get("y", 0))
80
+ return Vector2(Read.as_float(fields.get("x", 0)), Read.as_float(fields.get("y", 0)))
79
81
  "Vector3":
80
- return Vector3(fields.get("x", 0), fields.get("y", 0), fields.get("z", 0))
82
+ return Vector3(
83
+ Read.as_float(fields.get("x", 0)),
84
+ Read.as_float(fields.get("y", 0)),
85
+ Read.as_float(fields.get("z", 0))
86
+ )
81
87
  "Color":
82
88
  return Color(
83
- fields.get("r", 1), fields.get("g", 1), fields.get("b", 1), fields.get("a", 1)
89
+ Read.as_float(fields.get("r", 1), 1.0),
90
+ Read.as_float(fields.get("g", 1), 1.0),
91
+ Read.as_float(fields.get("b", 1), 1.0),
92
+ Read.as_float(fields.get("a", 1), 1.0)
84
93
  )
85
94
  if typeof(value) == TYPE_ARRAY:
86
95
  var result: Array = []
@@ -93,8 +102,9 @@ func _parse_value(value: Variant) -> Variant:
93
102
  func _parse_json_maybe(value: Variant) -> Variant:
94
103
  if typeof(value) != TYPE_STRING:
95
104
  return value
96
- var parsed: Variant = JSON.parse_string(value)
97
- if parsed == null and value != "null":
105
+ var text: String = value
106
+ var parsed: Variant = JSON.parse_string(text)
107
+ if parsed == null and text != "null":
98
108
  return value
99
109
  return parsed
100
110
 
@@ -183,9 +193,9 @@ func create_animation(args: Dictionary) -> Dictionary:
183
193
  loop_mode = Animation.LOOP_NONE
184
194
 
185
195
  var anim: Animation = Animation.new()
186
- anim.length = float(args.get("length", 1.0))
196
+ anim.length = Read.as_float(args.get("length", 1.0), 1.0)
187
197
  anim.loop_mode = loop_mode
188
- anim.step = float(args.get("step", 0.1))
198
+ anim.step = Read.as_float(args.get("step", 0.1), 0.1)
189
199
 
190
200
  var add_err: Error = anim_lib.add_animation(StringName(animation_name), anim)
191
201
  if add_err != OK:
@@ -262,7 +272,11 @@ func add_animation_track(args: Dictionary) -> Dictionary:
262
272
  var parsed_value: Variant = (
263
273
  _parse_json_maybe(raw_value) if typeof(raw_value) == TYPE_STRING else raw_value
264
274
  )
265
- anim.track_insert_key(track_idx, float(keyframe.get("time", 0.0)), _parse_value(parsed_value))
275
+ var placed: int = anim.track_insert_key(
276
+ track_idx, Read.as_float(keyframe.get("time", 0.0)), _parse_value(parsed_value)
277
+ )
278
+ if placed < 0:
279
+ push_error("gdharness: could not key " + prop_name + " on " + node_path_str)
266
280
 
267
281
  "method":
268
282
  var method_node_path: String = str(track.get("nodePath", ""))
@@ -276,10 +290,14 @@ func add_animation_track(args: Dictionary) -> Dictionary:
276
290
  if typeof(entry) != TYPE_DICTIONARY:
277
291
  continue
278
292
  var keyframe: Dictionary = entry
279
- var invocation: Dictionary = {
280
- "method": method_name, "args": _parse_method_args(keyframe.get("args", []))
281
- }
282
- anim.track_insert_key(track_idx, float(keyframe.get("time", 0.0)), invocation)
293
+ var given: Variant = keyframe.get("args", [])
294
+ var arguments: Array = given if given is Array else []
295
+ var invocation: Dictionary = {"method": method_name, "args": _parse_method_args(arguments)}
296
+ var placed: int = anim.track_insert_key(
297
+ track_idx, Read.as_float(keyframe.get("time", 0.0)), invocation
298
+ )
299
+ if placed < 0:
300
+ push_error("gdharness: could not key " + method_name + " on " + method_node_path)
283
301
 
284
302
  _:
285
303
  scene_root.queue_free()
@@ -34,7 +34,7 @@ func global_classes(_args: Dictionary) -> Dictionary:
34
34
  return {"ok": false, "error": "Editor plugin unavailable"}
35
35
 
36
36
  var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
37
- var names: PackedStringArray = []
37
+ var names: Array[String] = []
38
38
  _walk(filesystem.get_filesystem(), names)
39
39
  names.sort()
40
40
  return {
@@ -45,7 +45,10 @@ func global_classes(_args: Dictionary) -> Dictionary:
45
45
  }
46
46
 
47
47
 
48
- func _walk(directory: EditorFileSystemDirectory, into: PackedStringArray) -> void:
48
+ # Array[String] rather than PackedStringArray: appending to a packed array answers with whether it
49
+ # worked, and a project holding return_value_discarded at error level refuses a script that drops
50
+ # that answer. Both reach the caller as the same list of strings.
51
+ func _walk(directory: EditorFileSystemDirectory, into: Array[String]) -> void:
49
52
  if directory == null:
50
53
  return
51
54
  for index: int in range(directory.get_file_count()):
@@ -9,6 +9,8 @@ extends Node
9
9
  ## tools answer at all.
10
10
 
11
11
  ## What the display server calls itself when the engine was started with no display at all.
12
+ const Read = preload("../reading.gd")
13
+
12
14
  const HEADLESS_DISPLAY: String = "headless"
13
15
  ## Where Godot keeps the port its own debugger listens on for a game it is playing. One setting
14
16
  ## for every editor on the machine, and bound only while a game runs.
@@ -158,7 +160,7 @@ func playing_status(_args: Dictionary) -> Dictionary:
158
160
  # play, so the one from when this editor greeted the server is a number it has moved off.
159
161
  var settings: EditorSettings = EditorInterface.get_editor_settings()
160
162
  var debugger: int = (
161
- int(settings.get_setting(DEBUGGER_SETTING))
163
+ Read.as_int(settings.get_setting(DEBUGGER_SETTING))
162
164
  if settings != null and settings.has_setting(DEBUGGER_SETTING)
163
165
  else 0
164
166
  )
@@ -3,6 +3,8 @@ extends Node
3
3
 
4
4
  ## Resource files, shaders, tilesets and themes, written through the open editor.
5
5
 
6
+ const Read = preload("../reading.gd")
7
+
6
8
  # Shader templates. Written as real multi-line source rather than escaped one-liners so that
7
9
  # what ends up in the .gdshader can be read here. The %s is the shader type.
8
10
  const SHADER_EMPTY: String = """shader_type %s;
@@ -75,26 +77,37 @@ func _parse_value(value: Variant) -> Variant:
75
77
  var t: Variant = fields.get("type", fields.get("_type", ""))
76
78
  match t:
77
79
  "Vector2":
78
- return Vector2(fields.get("x", 0), fields.get("y", 0))
80
+ return Vector2(Read.as_float(fields.get("x", 0)), Read.as_float(fields.get("y", 0)))
79
81
  "Vector3":
80
- return Vector3(fields.get("x", 0), fields.get("y", 0), fields.get("z", 0))
82
+ return Vector3(
83
+ Read.as_float(fields.get("x", 0)),
84
+ Read.as_float(fields.get("y", 0)),
85
+ Read.as_float(fields.get("z", 0))
86
+ )
81
87
  "Color":
82
88
  return Color(
83
- fields.get("r", 1), fields.get("g", 1), fields.get("b", 1), fields.get("a", 1)
89
+ Read.as_float(fields.get("r", 1), 1.0),
90
+ Read.as_float(fields.get("g", 1), 1.0),
91
+ Read.as_float(fields.get("b", 1), 1.0),
92
+ Read.as_float(fields.get("a", 1), 1.0)
84
93
  )
85
94
  "Vector2i":
86
- return Vector2i(fields.get("x", 0), fields.get("y", 0))
95
+ return Vector2i(Read.as_int(fields.get("x", 0)), Read.as_int(fields.get("y", 0)))
87
96
  "Vector3i":
88
- return Vector3i(fields.get("x", 0), fields.get("y", 0), fields.get("z", 0))
97
+ return Vector3i(
98
+ Read.as_int(fields.get("x", 0)),
99
+ Read.as_int(fields.get("y", 0)),
100
+ Read.as_int(fields.get("z", 0))
101
+ )
89
102
  "Rect2":
90
103
  return Rect2(
91
- fields.get("x", 0),
92
- fields.get("y", 0),
93
- fields.get("width", 0),
94
- fields.get("height", 0)
104
+ Read.as_float(fields.get("x", 0)),
105
+ Read.as_float(fields.get("y", 0)),
106
+ Read.as_float(fields.get("width", 0)),
107
+ Read.as_float(fields.get("height", 0))
95
108
  )
96
109
  "NodePath":
97
- return NodePath(fields.get("path", ""))
110
+ return NodePath(str(fields.get("path", "")))
98
111
  if typeof(value) == TYPE_ARRAY:
99
112
  var result: Array = []
100
113
  for item: Variant in value:
@@ -106,16 +119,17 @@ func _parse_value(value: Variant) -> Variant:
106
119
  func _set_resource_properties(resource: Resource, properties: Variant) -> void:
107
120
  var props: Dictionary = _parse_properties_dict(properties)
108
121
  for key: Variant in props:
109
- var val: Variant = _parse_value(props[key])
110
- resource.set(key, val)
122
+ var named: String = str(key)
123
+ resource.set(named, _parse_value(props[key]))
111
124
 
112
125
 
113
126
  func _parse_properties_dict(raw: Variant) -> Dictionary:
114
127
  if typeof(raw) == TYPE_DICTIONARY:
115
128
  return raw
116
129
  if typeof(raw) == TYPE_STRING and raw != "":
130
+ var text: String = raw
117
131
  var json: JSON = JSON.new()
118
- if json.parse(raw) == OK and typeof(json.data) == TYPE_DICTIONARY:
132
+ if json.parse(text) == OK and typeof(json.data) == TYPE_DICTIONARY:
119
133
  return json.data
120
134
  return {}
121
135
 
@@ -212,8 +226,10 @@ func create_shader(args: Dictionary) -> Dictionary:
212
226
  var file: FileAccess = FileAccess.open(shader_path, FileAccess.WRITE)
213
227
  if file == null:
214
228
  return {"ok": false, "error": "Failed to open shader file for writing", "shaderPath": shader_path}
215
- file.store_string(code)
229
+ var written: bool = file.store_string(code)
216
230
  file.close()
231
+ if not written:
232
+ return {"ok": false, "error": "Failed to write the shader file", "shaderPath": shader_path}
217
233
 
218
234
  _refresh_filesystem()
219
235
  return {"ok": true, "shaderPath": shader_path, "shaderType": shader_type}
@@ -241,17 +257,21 @@ func create_tileset(args: Dictionary) -> Dictionary:
241
257
  atlas.texture = tex
242
258
 
243
259
  var tile_size: Dictionary = source.get("tileSize", {})
244
- atlas.texture_region_size = Vector2i(int(tile_size.get("x", 0)), int(tile_size.get("y", 0)))
260
+ atlas.texture_region_size = Vector2i(
261
+ Read.as_int(tile_size.get("x", 0)), Read.as_int(tile_size.get("y", 0))
262
+ )
245
263
 
246
264
  if source.has("separation"):
247
265
  var sep: Dictionary = source.get("separation", {})
248
- atlas.separation = Vector2i(int(sep.get("x", 0)), int(sep.get("y", 0)))
266
+ atlas.separation = Vector2i(Read.as_int(sep.get("x", 0)), Read.as_int(sep.get("y", 0)))
249
267
 
250
268
  if source.has("offset"):
251
269
  var off: Dictionary = source.get("offset", {})
252
- atlas.margins = Vector2i(int(off.get("x", 0)), int(off.get("y", 0)))
270
+ atlas.margins = Vector2i(Read.as_int(off.get("x", 0)), Read.as_int(off.get("y", 0)))
253
271
 
254
- tileset.add_source(atlas)
272
+ var added: int = tileset.add_source(atlas)
273
+ if added < 0:
274
+ return {"ok": false, "error": "Failed to add a tile source", "texture": tex_path}
255
275
 
256
276
  var save_result: Error = ResourceSaver.save(tileset, tileset_path)
257
277
  if save_result != OK:
@@ -262,8 +282,8 @@ func create_tileset(args: Dictionary) -> Dictionary:
262
282
 
263
283
 
264
284
  ## The source ids a tile set holds, for saying what a cell could have named instead.
265
- func _source_ids(tile_set: TileSet) -> PackedInt32Array:
266
- var ids: PackedInt32Array = PackedInt32Array()
285
+ func _source_ids(tile_set: TileSet) -> Array[int]:
286
+ var ids: Array[int] = []
267
287
  for index: int in tile_set.get_source_count():
268
288
  ids.append(tile_set.get_source_id(index))
269
289
  return ids
@@ -293,7 +313,7 @@ func set_tilemap_cells(args: Dictionary) -> Dictionary:
293
313
  root.queue_free()
294
314
  return {"ok": false, "error": "TileMap node not found", "tilemapNodePath": node_path}
295
315
 
296
- var layer: int = int(args.get("layer", 0))
316
+ var layer: int = Read.as_int(args.get("layer", 0))
297
317
  var cells: Variant = args.get("cells", [])
298
318
  # Refused rather than skipped: placing nothing and answering success is indistinguishable
299
319
  # from placing everything, and the caller only finds out by opening the scene.
@@ -322,7 +342,7 @@ func set_tilemap_cells(args: Dictionary) -> Dictionary:
322
342
  "ok": false, "error": "every cell must be an object with coords, sourceId and atlasCoords"
323
343
  }
324
344
  var cell: Dictionary = entry
325
- var source_id: int = int(cell.get("sourceId", -1))
345
+ var source_id: int = Read.as_int(cell.get("sourceId", -1), -1)
326
346
  if not tile_set.has_source(source_id):
327
347
  root.queue_free()
328
348
  return {
@@ -332,13 +352,13 @@ func set_tilemap_cells(args: Dictionary) -> Dictionary:
332
352
 
333
353
  var coords: Dictionary = cell.get("coords", {})
334
354
  var atlas_coords: Dictionary = cell.get("atlasCoords", {})
335
- var at: Vector2i = Vector2i(int(coords.get("x", 0)), int(coords.get("y", 0)))
355
+ var at: Vector2i = Vector2i(Read.as_int(coords.get("x", 0)), Read.as_int(coords.get("y", 0)))
336
356
  tilemap.set_cell(
337
357
  layer,
338
358
  at,
339
359
  source_id,
340
- Vector2i(int(atlas_coords.get("x", 0)), int(atlas_coords.get("y", 0))),
341
- int(cell.get("alternativeTile", 0))
360
+ Vector2i(Read.as_int(atlas_coords.get("x", 0)), Read.as_int(atlas_coords.get("y", 0))),
361
+ Read.as_int(cell.get("alternativeTile", 0))
342
362
  )
343
363
  if tilemap.get_cell_source_id(layer, at) != -1:
344
364
  written += 1
@@ -368,7 +388,10 @@ func set_theme_color(args: Dictionary) -> Dictionary:
368
388
 
369
389
  var c: Dictionary = args.get("color", {})
370
390
  var color: Color = Color(
371
- float(c.get("r", 1.0)), float(c.get("g", 1.0)), float(c.get("b", 1.0)), float(c.get("a", 1.0))
391
+ Read.as_float(c.get("r", 1.0), 1.0),
392
+ Read.as_float(c.get("g", 1.0), 1.0),
393
+ Read.as_float(c.get("b", 1.0), 1.0),
394
+ Read.as_float(c.get("a", 1.0), 1.0)
372
395
  )
373
396
  theme.set_color(color_name, control_type, color)
374
397
 
@@ -406,7 +429,7 @@ func set_theme_font_size(args: Dictionary) -> Dictionary:
406
429
  if theme == null:
407
430
  return {"ok": false, "error": "No Theme at %s. Create one there first." % theme_path}
408
431
 
409
- theme.set_font_size(font_size_name, control_type, int(args.get("size", 0)))
432
+ theme.set_font_size(font_size_name, control_type, Read.as_int(args.get("size", 0)))
410
433
 
411
434
  var save_result: Error = ResourceSaver.save(theme, theme_path)
412
435
  if save_result != OK:
@@ -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