gdharness 0.12.16 → 0.12.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +11 -2
- package/build/godot/addons/auto_reload/auto_reload.gd +9 -2
- package/build/godot/addons/gdharness_editor/bridge_client.gd +11 -5
- package/build/godot/addons/gdharness_editor/plugin.gd +17 -4
- package/build/godot/addons/gdharness_editor/reading.gd +64 -0
- package/build/godot/addons/gdharness_editor/reading.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +30 -12
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +5 -2
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +3 -1
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +50 -27
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +92 -42
- package/build/godot/addons/gdharness_runtime/reading.gd +64 -0
- package/build/godot/addons/gdharness_runtime/reading.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +24 -10
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +7 -5
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +24 -22
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +16 -13
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +24 -8
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +7 -4
- package/build/godot/operations/audio_buses.gd +7 -6
- package/build/godot/operations/class_cache.gd +14 -9
- package/build/godot/operations/classdb_queries.gd +4 -3
- package/build/godot/operations/dependencies.gd +19 -16
- package/build/godot/operations/file_walk.gd +4 -2
- package/build/godot/operations/gdscript_analysis.gd +2 -3
- package/build/godot/operations/gdscript_authoring.gd +20 -8
- package/build/godot/operations/godot_operations.gd +7 -2
- package/build/godot/operations/import_pipeline.gd +5 -4
- package/build/godot/operations/input_actions.gd +13 -9
- package/build/godot/operations/patterns.gd +22 -0
- package/build/godot/operations/plugins.gd +10 -6
- package/build/godot/operations/project_config.gd +3 -2
- package/build/godot/operations/reading.gd +66 -0
- package/build/godot/operations/serialisation.gd +41 -9
- package/build/index.js +8 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
234
|
+
if Read.as_int(announcement.get("protocol", 0)) != ANNOUNCE_PROTOCOL:
|
|
231
235
|
return ""
|
|
232
|
-
var port: int =
|
|
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
|
|
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
|
|
27
|
-
_client.disconnected
|
|
28
|
-
_client.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(
|
|
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),
|
|
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
|
|
97
|
-
|
|
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 =
|
|
196
|
+
anim.length = Read.as_float(args.get("length", 1.0), 1.0)
|
|
187
197
|
anim.loop_mode = loop_mode
|
|
188
|
-
anim.step =
|
|
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(
|
|
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
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
anim.track_insert_key(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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),
|
|
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(
|
|
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
|
|
110
|
-
resource.set(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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) ->
|
|
266
|
-
var ids:
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
341
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|