gdharness 0.13.2 → 0.13.4
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 +229 -94
- package/build/godot/addons/gdharness_editor/reading.gd +14 -7
- package/build/godot/addons/gdharness_editor/serialisation.gd +560 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +8 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +59 -152
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +58 -0
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +14 -7
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +16 -1
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +88 -178
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +25 -2
- package/build/godot/addons/gdharness_runtime/serialisation.gd +560 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd.uid +1 -0
- package/build/godot/operations/reading.gd +6 -1
- package/build/godot/operations/serialisation.gd +448 -77
- package/build/index.js +226 -98
- package/package.json +1 -1
|
@@ -4,12 +4,31 @@ extends Node
|
|
|
4
4
|
## Scenes and their nodes, edited in the open editor and read back from what it holds.
|
|
5
5
|
|
|
6
6
|
const Read = preload("../reading.gd")
|
|
7
|
+
const Serialisation = preload("../serialisation.gd")
|
|
7
8
|
|
|
8
9
|
var _editor_plugin: EditorPlugin = null
|
|
10
|
+
var _values: Serialisation = Serialisation.new()
|
|
11
|
+
|
|
12
|
+
## How many scans the editor has finished, counted off its own signal, and what the count was when
|
|
13
|
+
## a scan was last asked for. A queued scan has not started, so the flags the engine offers read
|
|
14
|
+
## the same as one that is over; the count is what tells the two apart.
|
|
15
|
+
var _scans_finished: int = 0
|
|
16
|
+
var _scans_finished_at_request: int = 0
|
|
17
|
+
var _scan_pending: bool = false
|
|
9
18
|
|
|
10
19
|
|
|
11
20
|
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
12
21
|
_editor_plugin = plugin
|
|
22
|
+
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
23
|
+
if not filesystem.filesystem_changed.is_connected(_scan_finished):
|
|
24
|
+
var watched: int = filesystem.filesystem_changed.connect(_scan_finished)
|
|
25
|
+
if watched != OK:
|
|
26
|
+
push_error("gdharness could not watch for finished scans: %d" % watched)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
func _scan_finished() -> void:
|
|
30
|
+
_scans_finished += 1
|
|
31
|
+
_scan_pending = false
|
|
13
32
|
|
|
14
33
|
|
|
15
34
|
func _refresh_and_reload(scene_path: String) -> void:
|
|
@@ -110,66 +129,23 @@ func _parse_value(value: Variant, expected_type: int = TYPE_NIL) -> Variant:
|
|
|
110
129
|
## that names a type but lacks the keys to build it is left unhandled on purpose, so the caller
|
|
111
130
|
## can still read it against the type the property declares.
|
|
112
131
|
func _parse_tagged_dictionary(value: Dictionary) -> Array:
|
|
113
|
-
var type_tag: String = ""
|
|
114
|
-
if
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
]
|
|
131
|
-
"Color":
|
|
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
|
-
]
|
|
141
|
-
"Vector2i":
|
|
142
|
-
return [true, Vector2i(Read.as_int(value.get("x", 0)), Read.as_int(value.get("y", 0)))]
|
|
143
|
-
"Vector3i":
|
|
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
|
-
]
|
|
152
|
-
"Rect2":
|
|
153
|
-
return [
|
|
154
|
-
true,
|
|
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
|
-
)
|
|
161
|
-
]
|
|
162
|
-
"Transform2D":
|
|
163
|
-
return _parse_transform2d(value)
|
|
164
|
-
"Transform3D":
|
|
165
|
-
return _parse_transform3d(value)
|
|
166
|
-
"NodePath":
|
|
167
|
-
return [true, NodePath(str(value.get("path", "")))]
|
|
168
|
-
"Resource":
|
|
169
|
-
var resource_path: String = str(value.get("path", ""))
|
|
170
|
-
return [true, null if resource_path.is_empty() else load(resource_path)]
|
|
171
|
-
_:
|
|
172
|
-
return _parse_new_resource(type_tag, value)
|
|
132
|
+
var type_tag: String = str(value.get("_type", value.get("type", "")))
|
|
133
|
+
if type_tag == "Resource":
|
|
134
|
+
var resource_path: String = str(value.get("path", ""))
|
|
135
|
+
return [true, null if resource_path.is_empty() else load(resource_path)]
|
|
136
|
+
|
|
137
|
+
# The shared builder is the same one the answer was written by, so every shape it writes comes
|
|
138
|
+
# back as itself. `type` as the tag is read as `_type`: it is what this tool answered with
|
|
139
|
+
# before the serialisers were made one, and a caller who kept an answer can still send it.
|
|
140
|
+
var tagged: Dictionary = value
|
|
141
|
+
if not value.has("_type") and not type_tag.is_empty():
|
|
142
|
+
tagged = value.duplicate()
|
|
143
|
+
tagged["_type"] = type_tag
|
|
144
|
+
var rebuilt: Variant = _values.deserialize_value(tagged)
|
|
145
|
+
if not rebuilt is Dictionary:
|
|
146
|
+
return [true, rebuilt]
|
|
147
|
+
|
|
148
|
+
return _parse_new_resource(type_tag, value)
|
|
173
149
|
|
|
174
150
|
|
|
175
151
|
## A tag naming a Resource class builds a fresh one, its other keys set as properties, so a
|
|
@@ -193,47 +169,6 @@ func _parse_new_resource(type_tag: String, value: Dictionary) -> Array:
|
|
|
193
169
|
return [true, built]
|
|
194
170
|
|
|
195
171
|
|
|
196
|
-
func _parse_transform2d(value: Dictionary) -> Array:
|
|
197
|
-
if not (value.has("x") and value.has("y") and value.has("origin")):
|
|
198
|
-
return [false, null]
|
|
199
|
-
|
|
200
|
-
var basis_x: Dictionary = value["x"]
|
|
201
|
-
var basis_y: Dictionary = value["y"]
|
|
202
|
-
var origin: Dictionary = value["origin"]
|
|
203
|
-
return [
|
|
204
|
-
true,
|
|
205
|
-
Transform2D(
|
|
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)))
|
|
209
|
-
)
|
|
210
|
-
]
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
func _parse_transform3d(value: Dictionary) -> Array:
|
|
214
|
-
if not (value.has("basis") and value.has("origin")):
|
|
215
|
-
return [false, null]
|
|
216
|
-
|
|
217
|
-
var b: Dictionary = value["basis"]
|
|
218
|
-
var o: Dictionary = value["origin"]
|
|
219
|
-
var x: Dictionary = b.get("x", {})
|
|
220
|
-
var y: Dictionary = b.get("y", {})
|
|
221
|
-
var z: Dictionary = b.get("z", {})
|
|
222
|
-
var basis: Basis = Basis(
|
|
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))
|
|
233
|
-
)
|
|
234
|
-
return [true, Transform3D(basis, origin)]
|
|
235
|
-
|
|
236
|
-
|
|
237
172
|
## A dictionary with no tag, read against the type the property declares. Falls back to the
|
|
238
173
|
## dictionary itself, since a property may genuinely want one.
|
|
239
174
|
func _parse_shaped_dictionary(value: Dictionary, expected_type: int) -> Variant:
|
|
@@ -299,54 +234,6 @@ func _get_property_type(node: Node, prop_name: String) -> int:
|
|
|
299
234
|
return TYPE_NIL
|
|
300
235
|
|
|
301
236
|
|
|
302
|
-
func _serialize_value(value: Variant) -> Variant:
|
|
303
|
-
match typeof(value):
|
|
304
|
-
TYPE_VECTOR2:
|
|
305
|
-
return {"type": "Vector2", "x": value.x, "y": value.y}
|
|
306
|
-
TYPE_VECTOR3:
|
|
307
|
-
return {"type": "Vector3", "x": value.x, "y": value.y, "z": value.z}
|
|
308
|
-
TYPE_COLOR:
|
|
309
|
-
return {"type": "Color", "r": value.r, "g": value.g, "b": value.b, "a": value.a}
|
|
310
|
-
TYPE_VECTOR2I:
|
|
311
|
-
return {"type": "Vector2i", "x": value.x, "y": value.y}
|
|
312
|
-
TYPE_VECTOR3I:
|
|
313
|
-
return {"type": "Vector3i", "x": value.x, "y": value.y, "z": value.z}
|
|
314
|
-
TYPE_RECT2:
|
|
315
|
-
return {
|
|
316
|
-
"type": "Rect2",
|
|
317
|
-
"x": value.position.x,
|
|
318
|
-
"y": value.position.y,
|
|
319
|
-
"width": value.size.x,
|
|
320
|
-
"height": value.size.y
|
|
321
|
-
}
|
|
322
|
-
TYPE_NODE_PATH:
|
|
323
|
-
return {"type": "NodePath", "path": str(value)}
|
|
324
|
-
TYPE_TRANSFORM2D:
|
|
325
|
-
return {
|
|
326
|
-
"type": "Transform2D",
|
|
327
|
-
"x": {"x": value.x.x, "y": value.x.y},
|
|
328
|
-
"y": {"x": value.y.x, "y": value.y.y},
|
|
329
|
-
"origin": {"x": value.origin.x, "y": value.origin.y}
|
|
330
|
-
}
|
|
331
|
-
TYPE_TRANSFORM3D:
|
|
332
|
-
return {
|
|
333
|
-
"type": "Transform3D",
|
|
334
|
-
"basis":
|
|
335
|
-
{
|
|
336
|
-
"x": {"x": value.basis.x.x, "y": value.basis.x.y, "z": value.basis.x.z},
|
|
337
|
-
"y": {"x": value.basis.y.x, "y": value.basis.y.y, "z": value.basis.y.z},
|
|
338
|
-
"z": {"x": value.basis.z.x, "y": value.basis.z.y, "z": value.basis.z.z}
|
|
339
|
-
},
|
|
340
|
-
"origin": {"x": value.origin.x, "y": value.origin.y, "z": value.origin.z}
|
|
341
|
-
}
|
|
342
|
-
TYPE_OBJECT:
|
|
343
|
-
if value and value is Resource and value.resource_path:
|
|
344
|
-
return {"type": "Resource", "path": value.resource_path}
|
|
345
|
-
return null
|
|
346
|
-
_:
|
|
347
|
-
return value
|
|
348
|
-
|
|
349
|
-
|
|
350
237
|
## Set each property, answering with what went wrong or "" when nothing did.
|
|
351
238
|
##
|
|
352
239
|
## A property the node does not have, and a resource path nothing is at, are both refused: Object
|
|
@@ -431,7 +318,7 @@ func _build_node_tree(
|
|
|
431
318
|
var pn: String = str(p.get("name", ""))
|
|
432
319
|
if pn.is_empty():
|
|
433
320
|
continue
|
|
434
|
-
props[pn] =
|
|
321
|
+
props[pn] = _values.serialize_value(node.get(pn))
|
|
435
322
|
data["properties"] = props
|
|
436
323
|
|
|
437
324
|
if depth >= 0 and current_depth >= depth:
|
|
@@ -732,7 +619,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
|
|
|
732
619
|
var default_val: Variant = defaults.get(prop_name)
|
|
733
620
|
if current_val == default_val:
|
|
734
621
|
continue
|
|
735
|
-
props[prop_name] =
|
|
622
|
+
props[prop_name] = _values.serialize_value(current_val)
|
|
736
623
|
|
|
737
624
|
if defaults:
|
|
738
625
|
defaults.queue_free()
|
|
@@ -928,8 +815,28 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
|
|
|
928
815
|
|
|
929
816
|
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
930
817
|
if not Read.as_bool(args.get("statusOnly", false)):
|
|
818
|
+
_scans_finished_at_request = _scans_finished
|
|
819
|
+
_scan_pending = true
|
|
931
820
|
filesystem.scan()
|
|
932
821
|
|
|
933
822
|
# Importing is reported separately from scanning, and a class is not registered until
|
|
934
823
|
# both are done, so a caller watching only one of them can look too early.
|
|
935
|
-
|
|
824
|
+
#
|
|
825
|
+
# `pending` is the third state, and it is the one that was missing. A scan is queued rather
|
|
826
|
+
# than run, so for the first frames after asking, both flags are false and the editor has not
|
|
827
|
+
# started: a caller polling them reads "finished" off a scan that has not begun. Measured, a
|
|
828
|
+
# headless editor of 400 scripts takes 1619ms and answered honestly, while a downstream editor
|
|
829
|
+
# of 376 classes answered finished 296ms in and then wrote its own stale class list over the
|
|
830
|
+
# cache that had just been corrected. Whoever reads this is waiting for the scan to have
|
|
831
|
+
# landed, so "not yet" and "over" must not be spelled the same.
|
|
832
|
+
var scanning: bool = filesystem.is_scanning()
|
|
833
|
+
var importing: bool = filesystem.is_importing()
|
|
834
|
+
if scanning or importing or _scans_finished != _scans_finished_at_request:
|
|
835
|
+
_scan_pending = false
|
|
836
|
+
return {
|
|
837
|
+
"ok": true,
|
|
838
|
+
"scanning": scanning,
|
|
839
|
+
"importing": importing,
|
|
840
|
+
"pending": _scan_pending,
|
|
841
|
+
"scansFinished": _scans_finished,
|
|
842
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends Node
|
|
3
|
+
|
|
4
|
+
## Project settings read from the open editor, for a caller that asked for the editor's answer.
|
|
5
|
+
##
|
|
6
|
+
## The same question answered headless starts an engine, loads project.godot from disk and exits.
|
|
7
|
+
## This answers from the ProjectSettings the editor already has, which costs nothing and is not the
|
|
8
|
+
## same thing: an editor holds what it has been told, including changes nobody has saved yet. Which
|
|
9
|
+
## of the two a caller wants is theirs to say, so the server only comes here when asked, and the
|
|
10
|
+
## difference is the reason the argument exists rather than an inconvenience to paper over.
|
|
11
|
+
##
|
|
12
|
+
## Every value goes through the same serialiser the headless answer uses, because the two are one
|
|
13
|
+
## question asked of two places: a value spelled differently here would read as the editor and the
|
|
14
|
+
## file disagreeing about something they agree about.
|
|
15
|
+
|
|
16
|
+
const Read = preload("../reading.gd")
|
|
17
|
+
const Serialisation = preload("../serialisation.gd")
|
|
18
|
+
|
|
19
|
+
var _values: Serialisation = Serialisation.new()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
func get_project_setting(args: Dictionary) -> Dictionary:
|
|
23
|
+
var prefix: String = str(args.get("prefix", ""))
|
|
24
|
+
if not prefix.is_empty():
|
|
25
|
+
return _settings_under(prefix)
|
|
26
|
+
|
|
27
|
+
var setting: String = str(args.get("setting", ""))
|
|
28
|
+
if setting.is_empty():
|
|
29
|
+
return {"ok": false, "error": "setting is required, or prefix to read every setting under one"}
|
|
30
|
+
|
|
31
|
+
var exists: bool = ProjectSettings.has_setting(setting)
|
|
32
|
+
var result: Dictionary = {"ok": true, "setting_path": setting, "exists": exists, "value": null}
|
|
33
|
+
if exists:
|
|
34
|
+
result["value"] = _values.serialize_value(ProjectSettings.get_setting(setting))
|
|
35
|
+
else:
|
|
36
|
+
result["message"] = "Setting does not exist"
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## Every setting under [prefix] with the type the engine registers for each, which is the half a
|
|
41
|
+
## name hides: a family of levels can hold a setting that is a bool, and writing a level over it
|
|
42
|
+
## looks like it worked.
|
|
43
|
+
func _settings_under(prefix: String) -> Dictionary:
|
|
44
|
+
var found: Array[Dictionary] = []
|
|
45
|
+
for property: Dictionary in ProjectSettings.get_property_list():
|
|
46
|
+
var setting: String = str(property.get("name", ""))
|
|
47
|
+
if not setting.begins_with(prefix):
|
|
48
|
+
continue
|
|
49
|
+
var kind: int = Read.as_int(property.get("type", TYPE_NIL), TYPE_NIL)
|
|
50
|
+
var described: Dictionary = {
|
|
51
|
+
"setting": setting,
|
|
52
|
+
"type": type_string(kind),
|
|
53
|
+
"value": _values.serialize_value(ProjectSettings.get_setting(setting)),
|
|
54
|
+
}
|
|
55
|
+
found.append(described)
|
|
56
|
+
|
|
57
|
+
found.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["setting"] < b["setting"])
|
|
58
|
+
return {"ok": true, "prefix": prefix, "count": found.size(), "settings": found}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://ddk0cjbgdv1f4
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
-
# A value out of a
|
|
3
|
+
# A value out of a dictionary that came from JSON, as the type the caller wants.
|
|
4
|
+
#
|
|
5
|
+
# This file is the one beside the operations, and `bun run sync:gd` copies it into each addon,
|
|
6
|
+
# because an addon is installed as a directory and cannot preload out of one. The runtime addon's
|
|
7
|
+
# copy ships inside exported games and the editor addon's does not, which is the other reason
|
|
8
|
+
# neither of them reaches into the other.
|
|
4
9
|
#
|
|
5
10
|
# `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.
|
|
7
|
-
#
|
|
8
|
-
# the
|
|
11
|
+
# `unsafe_call_argument` at error level will not compile a script that hands one over. These
|
|
12
|
+
# scripts are compiled under the target project's warning levels rather than under this package's,
|
|
13
|
+
# because `--path` makes that project the loaded one and the file's living in the npm cache
|
|
14
|
+
# changes nothing: one project turning that warning on lost every headless operation at once.
|
|
9
15
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
16
|
+
# The conversion itself is the same. What changes is that the type is established first, on a
|
|
17
|
+
# typed local, and the conversion happens from that. Here rather than at each of the thirty-one
|
|
18
|
+
# places that read a parameter, because thirty-one is how many chances there are to write the old
|
|
19
|
+
# form again, and the error does not appear until somebody sets the warning.
|
|
13
20
|
#
|
|
14
21
|
# `fallback` answers a value that is there and is null, which is a caller sending JSON null where a
|
|
15
22
|
# number was expected. Absent keys never reach here: the callers pass their own default to
|
|
@@ -734,9 +734,24 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
734
734
|
if not holder.has_method(named):
|
|
735
735
|
return {"type": "error", "message": "%s has no method %s" % [reached["called"], named]}
|
|
736
736
|
|
|
737
|
+
# Checked before the call rather than left to it. `callv` raises inside the game when an
|
|
738
|
+
# argument cannot be converted, and an error raised in a game somebody is playing holds it at a
|
|
739
|
+
# debugger break: every later call then reports a game that is not responding, which points
|
|
740
|
+
# nowhere near the argument. A wrong argument costs a refusal, never the session's game.
|
|
737
741
|
var deserialized_args: Array = []
|
|
738
742
|
for index: int in args.size():
|
|
739
|
-
|
|
743
|
+
var wants: int = _values.parameter_type(holder, named, index)
|
|
744
|
+
var given: Variant = _values.fitted(args[index], wants)
|
|
745
|
+
if not Values.acceptable(given, wants):
|
|
746
|
+
return {
|
|
747
|
+
"type": "error",
|
|
748
|
+
"message":
|
|
749
|
+
(
|
|
750
|
+
"%s.%s takes %s as argument %d and was given %s, which cannot be converted."
|
|
751
|
+
% [reached["called"], named, type_string(wants), index + 1, type_string(typeof(given))]
|
|
752
|
+
)
|
|
753
|
+
}
|
|
754
|
+
deserialized_args.append(given)
|
|
740
755
|
|
|
741
756
|
var result: Variant = holder.callv(named, deserialized_args)
|
|
742
757
|
|