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
package/build/cli.js CHANGED
@@ -7467,6 +7467,13 @@ function addonMismatch(addonVersion, serverVersion) {
7467
7467
  }
7468
7468
  return `${both} Restart it with editor_launch restart to pick the new one up.`;
7469
7469
  }
7470
+ function markIfStale(answer, addonVersion, serverVersion) {
7471
+ const staleNote = addonMismatch(addonVersion, serverVersion);
7472
+ if (staleNote === undefined || typeof answer !== "object" || answer === null || Array.isArray(answer)) {
7473
+ return answer;
7474
+ }
7475
+ return { ...answer, addonIsStale: true, staleNote };
7476
+ }
7470
7477
  var DEBUG_MODE, GODOT_DEBUG_MODE_DEFAULT, SERVER_VERSION, UNVERSIONED = "addon from before versions were reported";
7471
7478
  var init_server_version = __esm(() => {
7472
7479
  init_update_check();
@@ -38328,7 +38335,7 @@ class GodotServer {
38328
38335
  ]);
38329
38336
  }
38330
38337
  try {
38331
- return this.jsonTextResponse(await this.godotBridge.invokeTool(toolName, args));
38338
+ return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
38332
38339
  } catch (error) {
38333
38340
  return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
38334
38341
  }
@@ -39911,7 +39918,9 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
39911
39918
  running games. There is no ambient project.
39912
39919
  - \`editor_status\` says whether an editor is connected and whether its addon matches the server.
39913
39920
  \`addonIsStale\` means they differ, and \`staleNote\` says which half is behind: an editor that
39914
- needs restarting, or a server that needs reconnecting in your harness.
39921
+ needs restarting, or a server that needs reconnecting in your harness. Every answer that came
39922
+ from the editor carries the same two when they differ, so a stale addon cannot answer quietly:
39923
+ believe a restart over a diagnostic that arrived with them.
39915
39924
  - Start the game with \`editor_run start\`, never by spawning an engine. The editor plays it, so
39916
39925
  its debugger holds it, which is what gives the \`debug_*\` tools something to talk to. Its answer
39917
39926
  says under \`runtime\` whether the game can be talked to yet; when it cannot, \`mayYetAnnounce\`
@@ -16,7 +16,12 @@ var _watched_files: Dictionary = {}
16
16
  func _enter_tree() -> void:
17
17
  _timer = Timer.new()
18
18
  _timer.wait_time = CHECK_INTERVAL_SECONDS
19
- _timer.timeout.connect(_check_for_changes)
19
+ # int rather than Error: Signal.connect answers with a plain int, where Object.connect answers
20
+ # with the enum, and a project holding int_as_enum_without_cast at error level refuses the
21
+ # assignment that conflates them.
22
+ var watching: int = _timer.timeout.connect(_check_for_changes)
23
+ if watching != OK:
24
+ push_error("gdharness: could not start watching for changes on disk")
20
25
  add_child(_timer)
21
26
  _timer.start()
22
27
  _update_watched_files()
@@ -79,7 +84,9 @@ func _check_for_changes() -> void:
79
84
 
80
85
  func _reload_script(path: String) -> void:
81
86
  print("[gdharness] Script changed on disk, reloading: ", path)
82
- ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_REPLACE)
87
+ var reloaded: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_REPLACE)
88
+ if reloaded == null:
89
+ push_error("gdharness: could not reload " + path + " from disk")
83
90
 
84
91
 
85
92
  func _reload_scene(path: String) -> void:
@@ -8,6 +8,8 @@ signal connected
8
8
  signal disconnected
9
9
  signal tool_requested(request_id: String, tool_name: String, args: Dictionary)
10
10
 
11
+ const Read = preload("reading.gd")
12
+
11
13
  const DEFAULT_URL: String = "ws://127.0.0.1:6505/godot"
12
14
  ## Written beside the addon by the install, so it names the version this copy came from.
13
15
  const VERSION_MARKER: String = "res://addons/gdharness_editor/.gdharness-version"
@@ -88,7 +90,9 @@ func _ready() -> void:
88
90
 
89
91
  _reconnect_timer = Timer.new()
90
92
  _reconnect_timer.one_shot = true
91
- _reconnect_timer.timeout.connect(_on_reconnect_timer)
93
+ var waiting: int = _reconnect_timer.timeout.connect(_on_reconnect_timer)
94
+ if waiting != OK:
95
+ push_error("gdharness: could not arm the reconnect timer")
92
96
  add_child(_reconnect_timer)
93
97
 
94
98
  set_process(true)
@@ -227,9 +231,9 @@ func announced_url() -> String:
227
231
  return ""
228
232
 
229
233
  var announcement: Dictionary = said
230
- if int(announcement.get("protocol", 0)) != ANNOUNCE_PROTOCOL:
234
+ if Read.as_int(announcement.get("protocol", 0)) != ANNOUNCE_PROTOCOL:
231
235
  return ""
232
- var port: int = int(announcement.get("port", 0))
236
+ var port: int = Read.as_int(announcement.get("port", 0))
233
237
  if port < 1 or port > 65535:
234
238
  return ""
235
239
  var host: String = str(announcement.get("host", "127.0.0.1"))
@@ -332,7 +336,7 @@ func _serving(setting: String) -> int:
332
336
  var settings: EditorSettings = EditorInterface.get_editor_settings()
333
337
  if settings == null or not settings.has_setting(setting):
334
338
  return 0
335
- return int(settings.get_setting(setting))
339
+ return Read.as_int(settings.get_setting(setting))
336
340
 
337
341
 
338
342
  ## The version marker beside this addon, or "" when the copy was not installed by gdharness.
@@ -434,7 +438,9 @@ func send_tool_result(request_id: String, success: bool, result: Variant = null,
434
438
 
435
439
  func _send_message(message: Dictionary) -> void:
436
440
  if socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
437
- socket.send_text(JSON.stringify(message))
441
+ var sent: Error = socket.send_text(JSON.stringify(message))
442
+ if sent != OK:
443
+ push_error("gdharness: could not send to the server: " + error_string(sent))
438
444
 
439
445
 
440
446
  func is_connected_to_server() -> bool:
@@ -23,9 +23,9 @@ func _enter_tree() -> void:
23
23
  add_child(_tool_executor)
24
24
  _tool_executor.set_editor_plugin(self)
25
25
 
26
- _client.connected.connect(_on_connected)
27
- _client.disconnected.connect(_on_disconnected)
28
- _client.tool_requested.connect(_on_tool_requested)
26
+ _listen(_client.connected, _on_connected)
27
+ _listen(_client.disconnected, _on_disconnected)
28
+ _listen(_client.tool_requested, _on_tool_requested)
29
29
 
30
30
  _setup_status_indicator()
31
31
  _client.connect_to_server()
@@ -53,6 +53,18 @@ func _exit_tree() -> void:
53
53
  _status_label = null
54
54
 
55
55
 
56
+ # A failed connect is a mistake in this plugin rather than anything the editor did, so it goes to
57
+ # the error stream instead of being dropped: a project holding return_value_discarded at error level
58
+ # refuses to compile a script that throws the answer away.
59
+ func _listen(source: Signal, handler: Callable) -> void:
60
+ # int rather than Error: Signal.connect answers with a plain int, where Object.connect answers
61
+ # with the enum, and a project holding int_as_enum_without_cast at error level refuses the
62
+ # assignment that conflates them.
63
+ var joined: int = source.connect(handler)
64
+ if joined != OK:
65
+ push_error("gdharness: could not listen to " + source.get_name())
66
+
67
+
56
68
  func _setup_status_indicator() -> void:
57
69
  _status_label = Label.new()
58
70
  _status_label.text = "gdharness: connecting"
@@ -82,7 +94,8 @@ func _on_tool_requested(request_id: String, tool_name: String, args: Dictionary)
82
94
 
83
95
  if success:
84
96
  var payload: Dictionary = result.duplicate(true)
85
- payload.erase("ok")
97
+ if not payload.erase("ok"):
98
+ push_error("gdharness: a tool answered ok without an ok field to remove")
86
99
  _client.send_tool_result(request_id, true, payload, "")
87
100
  else:
88
101
  _client.send_tool_result(request_id, false, null, str(result.get("error", "Unknown error")))
@@ -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. Reaching into the runtime addon instead would tie an
12
+ # editor tool to a directory that ships inside exported games.
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://xl0fokpr7xfd
@@ -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: