gdharness 0.13.5 → 0.13.7
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 +64 -13
- package/build/godot/addons/gdharness_editor/property_values.gd +294 -0
- package/build/godot/addons/gdharness_editor/property_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +106 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +9 -26
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +11 -53
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +3 -168
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +10 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +4 -90
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +41 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +106 -0
- package/build/godot/operations/serialisation.gd +106 -0
- package/build/index.js +64 -13
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ extends Node
|
|
|
3
3
|
|
|
4
4
|
## Resource files, shaders, tilesets and themes, written through the open editor.
|
|
5
5
|
|
|
6
|
+
const PropertyValues = preload("../property_values.gd")
|
|
6
7
|
const Read = preload("../reading.gd")
|
|
7
8
|
|
|
8
9
|
# Shader templates. Written as real multi-line source rather than escaped one-liners so that
|
|
@@ -48,6 +49,7 @@ void fragment() {
|
|
|
48
49
|
"""
|
|
49
50
|
|
|
50
51
|
var _editor_plugin: EditorPlugin = null
|
|
52
|
+
var _properties: PropertyValues = PropertyValues.new()
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
@@ -70,57 +72,9 @@ func _refresh_filesystem() -> void:
|
|
|
70
72
|
EditorInterface.get_resource_filesystem().scan()
|
|
71
73
|
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if fields.has("type") or fields.has("_type"):
|
|
77
|
-
var t: Variant = fields.get("type", fields.get("_type", ""))
|
|
78
|
-
match t:
|
|
79
|
-
"Vector2":
|
|
80
|
-
return Vector2(Read.as_float(fields.get("x", 0)), Read.as_float(fields.get("y", 0)))
|
|
81
|
-
"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
|
-
)
|
|
87
|
-
"Color":
|
|
88
|
-
return Color(
|
|
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)
|
|
93
|
-
)
|
|
94
|
-
"Vector2i":
|
|
95
|
-
return Vector2i(Read.as_int(fields.get("x", 0)), Read.as_int(fields.get("y", 0)))
|
|
96
|
-
"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
|
-
)
|
|
102
|
-
"Rect2":
|
|
103
|
-
return Rect2(
|
|
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))
|
|
108
|
-
)
|
|
109
|
-
"NodePath":
|
|
110
|
-
return NodePath(str(fields.get("path", "")))
|
|
111
|
-
if typeof(value) == TYPE_ARRAY:
|
|
112
|
-
var result: Array = []
|
|
113
|
-
for item: Variant in value:
|
|
114
|
-
result.append(_parse_value(item))
|
|
115
|
-
return result
|
|
116
|
-
return value
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
func _set_resource_properties(resource: Resource, properties: Variant) -> void:
|
|
120
|
-
var props: Dictionary = _parse_properties_dict(properties)
|
|
121
|
-
for key: Variant in props:
|
|
122
|
-
var named: String = str(key)
|
|
123
|
-
resource.set(named, _parse_value(props[key]))
|
|
75
|
+
## Set each property, answering with what went wrong or "" when nothing did.
|
|
76
|
+
func _set_resource_properties(resource: Resource, properties: Variant) -> String:
|
|
77
|
+
return _properties.write_all(resource, _parse_properties_dict(properties))
|
|
124
78
|
|
|
125
79
|
|
|
126
80
|
func _parse_properties_dict(raw: Variant) -> Dictionary:
|
|
@@ -175,7 +129,9 @@ func create_resource(args: Dictionary) -> Dictionary:
|
|
|
175
129
|
resource.set_script(script_obj)
|
|
176
130
|
|
|
177
131
|
if args.has("properties"):
|
|
178
|
-
_set_resource_properties(resource, args.get("properties"))
|
|
132
|
+
var refused: String = _set_resource_properties(resource, args.get("properties"))
|
|
133
|
+
if not refused.is_empty():
|
|
134
|
+
return {"ok": false, "error": refused}
|
|
179
135
|
|
|
180
136
|
var save_result: Error = ResourceSaver.save(resource, res_path)
|
|
181
137
|
if save_result != OK:
|
|
@@ -194,7 +150,9 @@ func modify_resource(args: Dictionary) -> Dictionary:
|
|
|
194
150
|
if resource == null:
|
|
195
151
|
return {"ok": false, "error": "Resource not found", "resourcePath": res_path}
|
|
196
152
|
|
|
197
|
-
_set_resource_properties(resource, args.get("properties", ""))
|
|
153
|
+
var refused: String = _set_resource_properties(resource, args.get("properties", ""))
|
|
154
|
+
if not refused.is_empty():
|
|
155
|
+
return {"ok": false, "error": refused}
|
|
198
156
|
var save_result: Error = ResourceSaver.save(resource, res_path)
|
|
199
157
|
if save_result != OK:
|
|
200
158
|
return {"ok": false, "error": "Failed to save resource", "code": save_result}
|
|
@@ -4,10 +4,12 @@ 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 PropertyValues = preload("../property_values.gd")
|
|
7
8
|
const Serialisation = preload("../serialisation.gd")
|
|
8
9
|
|
|
9
10
|
var _editor_plugin: EditorPlugin = null
|
|
10
11
|
var _values: Serialisation = Serialisation.new()
|
|
12
|
+
var _properties: PropertyValues = PropertyValues.new()
|
|
11
13
|
|
|
12
14
|
## How many scans the editor has finished, counted off its own signal, and what the count was when
|
|
13
15
|
## a scan was last asked for. A queued scan has not started, so the flags the engine offers read
|
|
@@ -103,176 +105,9 @@ func _find_node(root: Node, path: String) -> Node:
|
|
|
103
105
|
return root.get_node_or_null(path)
|
|
104
106
|
|
|
105
107
|
|
|
106
|
-
## Turns what arrived over the wire into the Godot value a property wants.
|
|
107
|
-
##
|
|
108
|
-
## Three separate questions, asked in order, because a caller may say what it means in three
|
|
109
|
-
## ways: a dictionary that names its own type, a dictionary shaped like the type the property
|
|
110
|
-
## declares, or a bare array positional for a vector. Each is its own function; asking all three
|
|
111
|
-
## in one was twenty-four exits deep and impossible to follow.
|
|
112
|
-
func _parse_value(value: Variant, expected_type: int = TYPE_NIL) -> Variant:
|
|
113
|
-
if value is Dictionary:
|
|
114
|
-
var fields: Dictionary = value
|
|
115
|
-
var tagged: Array = _parse_tagged_dictionary(fields)
|
|
116
|
-
if tagged[0]:
|
|
117
|
-
return tagged[1]
|
|
118
|
-
return _parse_shaped_dictionary(fields, expected_type)
|
|
119
|
-
if value is Array:
|
|
120
|
-
var items: Array = value
|
|
121
|
-
return _parse_array(items, expected_type)
|
|
122
|
-
return value
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
## A dictionary carrying its own type name, as the serialiser writes it.
|
|
126
|
-
##
|
|
127
|
-
## Answers [handled, value] rather than just the value, because a handled tag may legitimately
|
|
128
|
-
## produce null: a Resource with no path is "this is nothing", not "this is not mine". A tag
|
|
129
|
-
## that names a type but lacks the keys to build it is left unhandled on purpose, so the caller
|
|
130
|
-
## can still read it against the type the property declares.
|
|
131
|
-
func _parse_tagged_dictionary(value: Dictionary) -> Array:
|
|
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)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
## A tag naming a Resource class builds a fresh one, its other keys set as properties, so a
|
|
152
|
-
## NavigationRegion2D can arrive with its NavigationPolygon and an AnimationTree with its root
|
|
153
|
-
## state machine in the same add as any other property.
|
|
154
|
-
func _parse_new_resource(type_tag: String, value: Dictionary) -> Array:
|
|
155
|
-
if (
|
|
156
|
-
type_tag.is_empty()
|
|
157
|
-
or not ClassDB.class_exists(type_tag)
|
|
158
|
-
or not ClassDB.is_parent_class(type_tag, "Resource")
|
|
159
|
-
or not ClassDB.can_instantiate(type_tag)
|
|
160
|
-
):
|
|
161
|
-
return [false, null]
|
|
162
|
-
|
|
163
|
-
var built: Resource = ClassDB.instantiate(type_tag)
|
|
164
|
-
for key: Variant in value:
|
|
165
|
-
var property: String = str(key)
|
|
166
|
-
if property == "_type" or property == "type":
|
|
167
|
-
continue
|
|
168
|
-
built.set(property, _parse_value(value[key], typeof(built.get(property))))
|
|
169
|
-
return [true, built]
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
## A dictionary with no tag, read against the type the property declares. Falls back to the
|
|
173
|
-
## dictionary itself, since a property may genuinely want one.
|
|
174
|
-
func _parse_shaped_dictionary(value: Dictionary, expected_type: int) -> Variant:
|
|
175
|
-
match expected_type:
|
|
176
|
-
TYPE_VECTOR2:
|
|
177
|
-
if value.has("x") and value.has("y"):
|
|
178
|
-
return Vector2(Read.as_float(value["x"]), Read.as_float(value["y"]))
|
|
179
|
-
TYPE_VECTOR2I:
|
|
180
|
-
if value.has("x") and value.has("y"):
|
|
181
|
-
return Vector2i(Read.as_int(value["x"]), Read.as_int(value["y"]))
|
|
182
|
-
TYPE_VECTOR3:
|
|
183
|
-
if value.has("x") and value.has("y") and value.has("z"):
|
|
184
|
-
return Vector3(
|
|
185
|
-
Read.as_float(value["x"]), Read.as_float(value["y"]), Read.as_float(value["z"])
|
|
186
|
-
)
|
|
187
|
-
TYPE_VECTOR3I:
|
|
188
|
-
if value.has("x") and value.has("y") and value.has("z"):
|
|
189
|
-
return Vector3i(Read.as_int(value["x"]), Read.as_int(value["y"]), Read.as_int(value["z"]))
|
|
190
|
-
TYPE_COLOR:
|
|
191
|
-
if value.has("r") and value.has("g") and value.has("b"):
|
|
192
|
-
return Color(
|
|
193
|
-
Read.as_float(value["r"]),
|
|
194
|
-
Read.as_float(value["g"]),
|
|
195
|
-
Read.as_float(value["b"]),
|
|
196
|
-
Read.as_float(value.get("a", 1), 1.0)
|
|
197
|
-
)
|
|
198
|
-
TYPE_RECT2:
|
|
199
|
-
if value.has("x") and value.has("y") and value.has("width") and value.has("height"):
|
|
200
|
-
return Rect2(
|
|
201
|
-
Read.as_float(value["x"]),
|
|
202
|
-
Read.as_float(value["y"]),
|
|
203
|
-
Read.as_float(value["width"]),
|
|
204
|
-
Read.as_float(value["height"])
|
|
205
|
-
)
|
|
206
|
-
TYPE_NODE_PATH:
|
|
207
|
-
if value.has("path"):
|
|
208
|
-
return NodePath(str(value["path"]))
|
|
209
|
-
return value
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
## An array, either positional for a vector the property declares, or a list to parse per item.
|
|
213
|
-
func _parse_array(value: Array, expected_type: int) -> Variant:
|
|
214
|
-
match expected_type:
|
|
215
|
-
TYPE_VECTOR2:
|
|
216
|
-
if value.size() >= 2:
|
|
217
|
-
return Vector2(Read.as_float(value[0]), Read.as_float(value[1]))
|
|
218
|
-
TYPE_VECTOR2I:
|
|
219
|
-
if value.size() >= 2:
|
|
220
|
-
return Vector2i(Read.as_int(value[0]), Read.as_int(value[1]))
|
|
221
|
-
TYPE_VECTOR3:
|
|
222
|
-
if value.size() >= 3:
|
|
223
|
-
return Vector3(Read.as_float(value[0]), Read.as_float(value[1]), Read.as_float(value[2]))
|
|
224
|
-
TYPE_VECTOR3I:
|
|
225
|
-
if value.size() >= 3:
|
|
226
|
-
return Vector3i(Read.as_int(value[0]), Read.as_int(value[1]), Read.as_int(value[2]))
|
|
227
|
-
return value.map(func(item: Variant) -> Variant: return _parse_value(item))
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
func _get_property_type(node: Node, prop_name: String) -> int:
|
|
231
|
-
for prop: Dictionary in node.get_property_list():
|
|
232
|
-
if str(prop.get("name", "")) == prop_name:
|
|
233
|
-
return Read.as_int(prop.get("type", TYPE_NIL), TYPE_NIL)
|
|
234
|
-
return TYPE_NIL
|
|
235
|
-
|
|
236
|
-
|
|
237
108
|
## Set each property, answering with what went wrong or "" when nothing did.
|
|
238
|
-
##
|
|
239
|
-
## A property the node does not have, and a resource path nothing is at, are both refused: Object
|
|
240
|
-
## .set ignores an unknown name and stores null for a resource that would not load, so either one
|
|
241
|
-
## saves a scene that quietly did not change and reports it as a change that did.
|
|
242
109
|
func _set_node_properties(node: Node, properties: Dictionary) -> String:
|
|
243
|
-
|
|
244
|
-
var property: String = str(prop_name)
|
|
245
|
-
if not _has_property(node, property):
|
|
246
|
-
return "%s has no property %s" % [node.get_class(), property]
|
|
247
|
-
|
|
248
|
-
var expected_type: int = _get_property_type(node, property)
|
|
249
|
-
var raw: Variant = properties[prop_name]
|
|
250
|
-
|
|
251
|
-
# A resource-valued property takes the path of one, which is how a caller names a
|
|
252
|
-
# TileSet, a material or a theme: there is no other way to hand a tool a Resource.
|
|
253
|
-
if expected_type == TYPE_OBJECT and typeof(raw) == TYPE_STRING:
|
|
254
|
-
var path: String = str(raw)
|
|
255
|
-
# The project boundary is enforced here as well as on the server, because only the
|
|
256
|
-
# engine knows that this property is one holding a path: an absolute path and a
|
|
257
|
-
# user:// one both load, and neither names a file this project owns.
|
|
258
|
-
if not (path.begins_with("res://") or path.begins_with("uid://")):
|
|
259
|
-
return "%s takes a res:// or uid:// path, not %s" % [property, path]
|
|
260
|
-
if path.split("/").has(".."):
|
|
261
|
-
return "%s leaves the project: %s" % [property, path]
|
|
262
|
-
if not ResourceLoader.exists(path):
|
|
263
|
-
return "No resource at %s for %s" % [path, property]
|
|
264
|
-
node.set(property, load(path))
|
|
265
|
-
continue
|
|
266
|
-
|
|
267
|
-
node.set(property, _parse_value(raw, expected_type))
|
|
268
|
-
return ""
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
func _has_property(node: Node, prop_name: String) -> bool:
|
|
272
|
-
for prop: Dictionary in node.get_property_list():
|
|
273
|
-
if str(prop.get("name", "")) == prop_name:
|
|
274
|
-
return true
|
|
275
|
-
return false
|
|
110
|
+
return _properties.write_all(node, properties)
|
|
276
111
|
|
|
277
112
|
|
|
278
113
|
func _parse_properties_arg(raw_properties: Variant) -> Dictionary:
|
|
@@ -567,6 +567,16 @@ static func _can_hold(value: Variant) -> bool:
|
|
|
567
567
|
## is a list and an in-tray is a map, and a path that could not step into either stopped at the
|
|
568
568
|
## first one. An index reads the way a property does, and a negative one counts from the end the
|
|
569
569
|
## way GDScript's own does, so reading the last of something does not mean asking how many first.
|
|
570
|
+
## What is wrong with reading [param named] off [param holder], or "" when nothing is.
|
|
571
|
+
##
|
|
572
|
+
## Public because a wait asks the same question before it starts waiting, and used to answer it by
|
|
573
|
+
## waiting: a property nobody has spends the whole timeout and comes back "not met", which reads
|
|
574
|
+
## exactly like a game that never reached the state. The read refuses a mistyped name in one call
|
|
575
|
+
## and says so, and the two tools disagreeing about one typo is worth more than the duplication.
|
|
576
|
+
static func nothing_under(holder: Variant, named: String, called: String) -> String:
|
|
577
|
+
return "" if _can_read(holder, named) else _nothing_there(holder, named, called)
|
|
578
|
+
|
|
579
|
+
|
|
570
580
|
static func _can_read(holder: Variant, named: String) -> bool:
|
|
571
581
|
if holder is Array:
|
|
572
582
|
var items: Array = holder
|
|
@@ -11,27 +11,6 @@ extends RefCounted
|
|
|
11
11
|
const Read = preload("reading.gd")
|
|
12
12
|
const Serialisation = preload("serialisation.gd")
|
|
13
13
|
|
|
14
|
-
# What the engine carries from one to another without complaint, which is what an argument may
|
|
15
|
-
# arrive as. Numbers include bool because the engine counts it as one, and the three text types
|
|
16
|
-
# are one string wearing three hats.
|
|
17
|
-
const NUMBERS: Array[int] = [TYPE_BOOL, TYPE_INT, TYPE_FLOAT]
|
|
18
|
-
const TEXTS: Array[int] = [TYPE_STRING, TYPE_STRING_NAME, TYPE_NODE_PATH]
|
|
19
|
-
|
|
20
|
-
# The types that cross as a plain JSON list and have to be built back into themselves. A list is
|
|
21
|
-
# what a caller can write by hand and what the serialiser answers with, and the parameter or
|
|
22
|
-
# property being written is the only thing that knows which of these it wanted.
|
|
23
|
-
const LIST_TYPES: Array[int] = [
|
|
24
|
-
TYPE_PACKED_INT32_ARRAY,
|
|
25
|
-
TYPE_PACKED_INT64_ARRAY,
|
|
26
|
-
TYPE_PACKED_FLOAT32_ARRAY,
|
|
27
|
-
TYPE_PACKED_FLOAT64_ARRAY,
|
|
28
|
-
TYPE_PACKED_STRING_ARRAY,
|
|
29
|
-
TYPE_PACKED_VECTOR2_ARRAY,
|
|
30
|
-
TYPE_PACKED_VECTOR3_ARRAY,
|
|
31
|
-
TYPE_PACKED_VECTOR4_ARRAY,
|
|
32
|
-
TYPE_PACKED_COLOR_ARRAY,
|
|
33
|
-
]
|
|
34
|
-
|
|
35
14
|
var _shared: Serialisation = Serialisation.new()
|
|
36
15
|
|
|
37
16
|
|
|
@@ -77,49 +56,13 @@ static func node_at(root: Node, path: String) -> Dictionary:
|
|
|
77
56
|
|
|
78
57
|
|
|
79
58
|
## Whether two values can be compared without the comparison itself failing.
|
|
80
|
-
##
|
|
81
|
-
## GDScript's `==` is not total. An Object against a String is a hard error rather than false, and
|
|
82
|
-
## an error raised inside the game is not a wrong answer: it stops the game. A caller who asked
|
|
83
|
-
## `runtime_wait until` for the wrong kind of value got their game held at a debugger break, every
|
|
84
|
-
## later call answering "did not respond within 10000ms, it may be stuck in a long frame", and
|
|
85
|
-
## nothing anywhere naming the argument that did it. A wrong argument costs a refusal, never the
|
|
86
|
-
## session's game.
|
|
87
|
-
##
|
|
88
|
-
## Only an object is special. Every other pair the engine answers, false where they differ, and
|
|
89
|
-
## refusing those would refuse calls that work: an int against a float is the same number to a
|
|
90
|
-
## caller waiting for 1 on a property holding 1.0.
|
|
91
59
|
static func comparable(one: Variant, other: Variant) -> bool:
|
|
92
|
-
|
|
93
|
-
var right: int = typeof(other)
|
|
94
|
-
if left != TYPE_OBJECT and right != TYPE_OBJECT:
|
|
95
|
-
return true
|
|
96
|
-
# An object compares with another object and with null. Anything else is the error above.
|
|
97
|
-
return (left == TYPE_OBJECT or left == TYPE_NIL) and (right == TYPE_OBJECT or right == TYPE_NIL)
|
|
60
|
+
return Serialisation.comparable(one, other)
|
|
98
61
|
|
|
99
62
|
|
|
100
63
|
## Whether a value can be handed to a parameter declared as [param type] without the call failing.
|
|
101
|
-
##
|
|
102
|
-
## The same reason as [method comparable]: `callv` raises inside the game when an argument cannot
|
|
103
|
-
## be converted, and an error raised in a game somebody is playing holds it at a debugger break.
|
|
104
|
-
##
|
|
105
|
-
## A list of what is accepted rather than of what is refused, which is the way round that errs
|
|
106
|
-
## safely. The engine's own conversion table is not readable from GDScript, so either list is a
|
|
107
|
-
## guess at it; guessing narrow costs a caller a refusal naming both types, and guessing wide costs
|
|
108
|
-
## them the session. A refusal is the cheaper mistake, and it is the one they can act on.
|
|
109
64
|
static func acceptable(value: Variant, type: int) -> bool:
|
|
110
|
-
|
|
111
|
-
return true
|
|
112
|
-
var given: int = typeof(value)
|
|
113
|
-
if given == type:
|
|
114
|
-
return true
|
|
115
|
-
# null is a value for anything that holds an object, and for nothing else.
|
|
116
|
-
if given == TYPE_NIL:
|
|
117
|
-
return type == TYPE_OBJECT
|
|
118
|
-
if NUMBERS.has(given) and NUMBERS.has(type):
|
|
119
|
-
return true
|
|
120
|
-
if TEXTS.has(given) and TEXTS.has(type):
|
|
121
|
-
return true
|
|
122
|
-
return given == TYPE_ARRAY and LIST_TYPES.has(type)
|
|
65
|
+
return Serialisation.acceptable(value, type)
|
|
123
66
|
|
|
124
67
|
|
|
125
68
|
## The declared type of one parameter of a method, or TYPE_NIL when the method or the parameter
|
|
@@ -136,35 +79,6 @@ func parameter_type(object: Object, method: String, index: int) -> int:
|
|
|
136
79
|
return TYPE_NIL
|
|
137
80
|
|
|
138
81
|
|
|
139
|
-
## A value from the wire fitted to the type a property or parameter declares.
|
|
140
|
-
## as strings often enough, and callv refuses a "2.0" where a float is wanted, so a string that
|
|
141
|
-
## reads as the wanted scalar is read as one.
|
|
142
|
-
##
|
|
143
|
-
## The conversion is also what restores a packed array's exact type. Those cross as the plain JSON
|
|
144
|
-
## lists they fit into, so what comes back from the wire is an Array, and the parameter is what says
|
|
145
|
-
## it was a PackedStringArray.
|
|
82
|
+
## A value from the wire fitted to the type a property or parameter declares.
|
|
146
83
|
func fitted(value: Variant, type: int) -> Variant:
|
|
147
|
-
|
|
148
|
-
if type == TYPE_NIL or typeof(rebuilt) == type:
|
|
149
|
-
return rebuilt
|
|
150
|
-
|
|
151
|
-
if rebuilt is Array and LIST_TYPES.has(type):
|
|
152
|
-
return type_convert(rebuilt, type)
|
|
153
|
-
|
|
154
|
-
var simple: Array[int] = [TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING]
|
|
155
|
-
if not simple.has(type) or not simple.has(typeof(rebuilt)):
|
|
156
|
-
return rebuilt
|
|
157
|
-
|
|
158
|
-
if rebuilt is String and type != TYPE_STRING:
|
|
159
|
-
var text: String = rebuilt
|
|
160
|
-
var parsed: Variant = JSON.parse_string(text)
|
|
161
|
-
# A string that does not read as the type wanted is handed back as the string it is, so
|
|
162
|
-
# whoever asked can refuse it. type_convert answers 0 for "not an index" and true for any
|
|
163
|
-
# text at all, and a caller who sent a word got back a number they never sent: get_child
|
|
164
|
-
# answered about child 0 with no refusal and no note, which is a halt traded for a plausible
|
|
165
|
-
# wrong answer, and the wrong answer is the worse of the two.
|
|
166
|
-
if typeof(parsed) == TYPE_NIL or typeof(parsed) == TYPE_STRING:
|
|
167
|
-
return rebuilt
|
|
168
|
-
rebuilt = parsed
|
|
169
|
-
|
|
170
|
-
return type_convert(rebuilt, type)
|
|
84
|
+
return _shared.fitted(value, type)
|
|
@@ -97,6 +97,23 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
97
97
|
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
98
98
|
if standing.has("message"):
|
|
99
99
|
return standing
|
|
100
|
+
# Two different questions, not two ways of asking one: says looks for words anywhere under the
|
|
101
|
+
# path, a property compares one value on one node. Taking says and dropping the other left a
|
|
102
|
+
# caller watching a screen while believing they were watching a property, and the answer says
|
|
103
|
+
# which of them it is about only if you already know that says wins.
|
|
104
|
+
if not says.is_empty() and (not property.is_empty() or params.has("value")):
|
|
105
|
+
var other: String = property if not property.is_empty() else "value"
|
|
106
|
+
return {
|
|
107
|
+
"type": "error",
|
|
108
|
+
"message":
|
|
109
|
+
(
|
|
110
|
+
(
|
|
111
|
+
'A wait takes says or a property, not both: this one has says "%s" and %s as well.'
|
|
112
|
+
+ " Ask for one of them."
|
|
113
|
+
)
|
|
114
|
+
% [says, other]
|
|
115
|
+
)
|
|
116
|
+
}
|
|
100
117
|
if not says.is_empty():
|
|
101
118
|
return await _wait_until_said(node_path, says, timeout_ms)
|
|
102
119
|
if property.is_empty():
|
|
@@ -106,6 +123,14 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
106
123
|
|
|
107
124
|
var node: Node = standing["node"]
|
|
108
125
|
|
|
126
|
+
# A property the node has not got, refused here rather than waited on. Waiting answered "not
|
|
127
|
+
# met" after the whole timeout, which is what a game that never reached the state answers too,
|
|
128
|
+
# so a mistyped name and a condition that did not happen read the same. The property read
|
|
129
|
+
# refuses that typo in one call, and two tools disagreeing about it is the fault.
|
|
130
|
+
var missing: String = Queries.nothing_under(node, property, node_path)
|
|
131
|
+
if not missing.is_empty():
|
|
132
|
+
return {"type": "error", "message": missing}
|
|
133
|
+
|
|
109
134
|
var current: Variant = node.get(property)
|
|
110
135
|
var wanted: Variant = _values.fitted(params["value"], typeof(current))
|
|
111
136
|
# Refused before anything is evaluated, because the evaluation is what does the damage: an
|
|
@@ -124,6 +149,22 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
124
149
|
% [node_path, property, type_string(typeof(current)), type_string(typeof(wanted))]
|
|
125
150
|
)
|
|
126
151
|
}
|
|
152
|
+
# And a value that cannot become what the property holds, which lands worse than the halt: a
|
|
153
|
+
# word where a number goes became 0.0, 0.0 already equalled the property, and the wait answered
|
|
154
|
+
# met the instant it started, about a state nobody asked about, with the real value beside it.
|
|
155
|
+
# A refusal is read and a met is acted on.
|
|
156
|
+
if not Values.acceptable(wanted, typeof(current)):
|
|
157
|
+
return {
|
|
158
|
+
"type": "error",
|
|
159
|
+
"message":
|
|
160
|
+
(
|
|
161
|
+
(
|
|
162
|
+
"%s.%s holds %s and the value to wait for is %s, which cannot become one:"
|
|
163
|
+
+ " waiting on it would answer about a state nobody asked for."
|
|
164
|
+
)
|
|
165
|
+
% [node_path, property, type_string(typeof(current)), type_string(typeof(wanted))]
|
|
166
|
+
)
|
|
167
|
+
}
|
|
127
168
|
|
|
128
169
|
var started: int = Time.get_ticks_msec()
|
|
129
170
|
# The type is checked every time round, not only at the top: a property that holds an object a
|
|
@@ -10,6 +10,27 @@ extends RefCounted
|
|
|
10
10
|
|
|
11
11
|
const Read = preload("reading.gd")
|
|
12
12
|
|
|
13
|
+
# What the engine carries from one to another without complaint, which is what a value written to a
|
|
14
|
+
# property or handed to a parameter may arrive as. Numbers include bool because the engine counts it
|
|
15
|
+
# as one, and the three text types are one string wearing three hats.
|
|
16
|
+
const NUMBERS: Array[int] = [TYPE_BOOL, TYPE_INT, TYPE_FLOAT]
|
|
17
|
+
const TEXTS: Array[int] = [TYPE_STRING, TYPE_STRING_NAME, TYPE_NODE_PATH]
|
|
18
|
+
|
|
19
|
+
# The types that cross as a plain JSON list and have to be built back into themselves. A list is
|
|
20
|
+
# what a caller can write by hand and what the serialiser answers with, and the parameter or
|
|
21
|
+
# property being written is the only thing that knows which of these it wanted.
|
|
22
|
+
const LIST_TYPES: Array[int] = [
|
|
23
|
+
TYPE_PACKED_INT32_ARRAY,
|
|
24
|
+
TYPE_PACKED_INT64_ARRAY,
|
|
25
|
+
TYPE_PACKED_FLOAT32_ARRAY,
|
|
26
|
+
TYPE_PACKED_FLOAT64_ARRAY,
|
|
27
|
+
TYPE_PACKED_STRING_ARRAY,
|
|
28
|
+
TYPE_PACKED_VECTOR2_ARRAY,
|
|
29
|
+
TYPE_PACKED_VECTOR3_ARRAY,
|
|
30
|
+
TYPE_PACKED_VECTOR4_ARRAY,
|
|
31
|
+
TYPE_PACKED_COLOR_ARRAY,
|
|
32
|
+
]
|
|
33
|
+
|
|
13
34
|
# What each Godot type becomes on the wire, as one table you can read rather than an order you
|
|
14
35
|
# have to trust. Keyed on typeof() rather than written as a chain of `is` tests, because the
|
|
15
36
|
# order mattered: Resource had to be tested before Object or every resource came back as a bare
|
|
@@ -163,6 +184,91 @@ func deserialize_value(value: Variant) -> Variant:
|
|
|
163
184
|
return call(builder, fields)
|
|
164
185
|
|
|
165
186
|
|
|
187
|
+
## Whether two values can be compared without the comparison itself failing.
|
|
188
|
+
##
|
|
189
|
+
## GDScript's `==` is not total. An Object against a String is a hard error rather than false, and
|
|
190
|
+
## an error raised inside a game is not a wrong answer: it stops the game. A caller who asked
|
|
191
|
+
## `runtime_wait until` for the wrong kind of value got their game held at a debugger break, every
|
|
192
|
+
## later call answering "did not respond within 10000ms, it may be stuck in a long frame", and
|
|
193
|
+
## nothing anywhere naming the argument that did it. A wrong argument costs a refusal, never the
|
|
194
|
+
## session's game.
|
|
195
|
+
##
|
|
196
|
+
## Only an object is special. Every other pair the engine answers, false where they differ, and
|
|
197
|
+
## refusing those would refuse calls that work: an int against a float is the same number to a
|
|
198
|
+
## caller waiting for 1 on a property holding 1.0.
|
|
199
|
+
static func comparable(one: Variant, other: Variant) -> bool:
|
|
200
|
+
var left: int = typeof(one)
|
|
201
|
+
var right: int = typeof(other)
|
|
202
|
+
if left != TYPE_OBJECT and right != TYPE_OBJECT:
|
|
203
|
+
return true
|
|
204
|
+
# An object compares with another object and with null. Anything else is the error above.
|
|
205
|
+
return (left == TYPE_OBJECT or left == TYPE_NIL) and (right == TYPE_OBJECT or right == TYPE_NIL)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
## Whether [param value] can be given to something declared as [param type] without damage.
|
|
209
|
+
##
|
|
210
|
+
## Here rather than beside either caller because the running game and the editor ask it of the same
|
|
211
|
+
## values and must not answer differently. What it protects against differs by side and both are
|
|
212
|
+
## silent: `callv` and `==` raise inside a running game, which holds it at a debugger break, while
|
|
213
|
+
## `Object.set` in the editor converts instead and writes the result to the scene file. A word where
|
|
214
|
+
## an int goes is stored as 0, where a bool goes as true, where a Vector2 goes as (0, 0), and the
|
|
215
|
+
## tool reports the change as made. The property keeps a value nobody asked for and the scene is
|
|
216
|
+
## saved with it.
|
|
217
|
+
##
|
|
218
|
+
## A list of what is accepted rather than of what is refused, which is the way round that errs
|
|
219
|
+
## safely. The engine's own conversion table is not readable from GDScript, so either list is a
|
|
220
|
+
## guess at it; guessing narrow costs a caller a refusal naming both types, and guessing wide costs
|
|
221
|
+
## them the damage above. A refusal is the cheaper mistake, and it is the one they can act on.
|
|
222
|
+
static func acceptable(value: Variant, type: int) -> bool:
|
|
223
|
+
if type == TYPE_NIL:
|
|
224
|
+
return true
|
|
225
|
+
var given: int = typeof(value)
|
|
226
|
+
if given == type:
|
|
227
|
+
return true
|
|
228
|
+
# null is a value for anything that holds an object, and for nothing else.
|
|
229
|
+
if given == TYPE_NIL:
|
|
230
|
+
return type == TYPE_OBJECT
|
|
231
|
+
if NUMBERS.has(given) and NUMBERS.has(type):
|
|
232
|
+
return true
|
|
233
|
+
if TEXTS.has(given) and TEXTS.has(type):
|
|
234
|
+
return true
|
|
235
|
+
return given == TYPE_ARRAY and LIST_TYPES.has(type)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
## A value from the wire fitted to the type a property or parameter declares. Arguments arrive
|
|
239
|
+
## as strings often enough, and callv refuses a "2.0" where a float is wanted, so a string that
|
|
240
|
+
## reads as the wanted scalar is read as one.
|
|
241
|
+
##
|
|
242
|
+
## The conversion is also what restores a packed array's exact type. Those cross as the plain JSON
|
|
243
|
+
## lists they fit into, so what comes back from the wire is an Array, and the parameter is what says
|
|
244
|
+
## it was a PackedStringArray.
|
|
245
|
+
func fitted(value: Variant, type: int) -> Variant:
|
|
246
|
+
var rebuilt: Variant = deserialize_value(value)
|
|
247
|
+
if type == TYPE_NIL or typeof(rebuilt) == type:
|
|
248
|
+
return rebuilt
|
|
249
|
+
|
|
250
|
+
if rebuilt is Array and LIST_TYPES.has(type):
|
|
251
|
+
return type_convert(rebuilt, type)
|
|
252
|
+
|
|
253
|
+
var simple: Array[int] = [TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING]
|
|
254
|
+
if not simple.has(type) or not simple.has(typeof(rebuilt)):
|
|
255
|
+
return rebuilt
|
|
256
|
+
|
|
257
|
+
if rebuilt is String and type != TYPE_STRING:
|
|
258
|
+
var text: String = rebuilt
|
|
259
|
+
var parsed: Variant = JSON.parse_string(text)
|
|
260
|
+
# A string that does not read as the type wanted is handed back as the string it is, so
|
|
261
|
+
# whoever asked can refuse it. type_convert answers 0 for "not an index" and true for any
|
|
262
|
+
# text at all, and a caller who sent a word got back a number they never sent: get_child
|
|
263
|
+
# answered about child 0 with no refusal and no note, which is a halt traded for a plausible
|
|
264
|
+
# wrong answer, and the wrong answer is the worse of the two.
|
|
265
|
+
if typeof(parsed) == TYPE_NIL or typeof(parsed) == TYPE_STRING:
|
|
266
|
+
return rebuilt
|
|
267
|
+
rebuilt = parsed
|
|
268
|
+
|
|
269
|
+
return type_convert(rebuilt, type)
|
|
270
|
+
|
|
271
|
+
|
|
166
272
|
## Whether [param fields] carries every key of any one of [param alternatives].
|
|
167
273
|
##
|
|
168
274
|
## More than one spelling because a rectangle has two: the two corners it is answered with, and the
|