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
|
@@ -2,46 +2,47 @@ extends RefCounted
|
|
|
2
2
|
|
|
3
3
|
## What crosses the wire between the game and the server, in both directions: Godot values
|
|
4
4
|
## as JSON-safe dictionaries, and JSON back into the values a property or parameter wants.
|
|
5
|
+
##
|
|
6
|
+
## The conversion itself is the shared one, so a value read off a running game is spelled exactly
|
|
7
|
+
## as the same value read off the editor or off a headless engine. What is here is the part that is
|
|
8
|
+
## only true of a running game: reaching a node by path, and fitting an argument to the type the
|
|
9
|
+
## method it is going to declares.
|
|
5
10
|
|
|
6
11
|
const Read = preload("reading.gd")
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
##
|
|
34
|
-
## A method or property typed as a class answers a null with a Variant of type OBJECT that has
|
|
35
|
-
## nothing behind it, and so does one whose object has been freed. Asking either for its class is
|
|
36
|
-
## a script error, and a script error here costs far more than the one answer: the request is
|
|
37
|
-
## never replied to at all, and an editor playing the game stops it dead on the error, so every
|
|
38
|
-
## question after it times out as well. `find_child` for a name nothing has cost a whole session
|
|
39
|
-
## that way.
|
|
12
|
+
const Serialisation = preload("serialisation.gd")
|
|
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
|
+
var _shared: Serialisation = Serialisation.new()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Converts a Godot value into something JSON can carry.
|
|
40
39
|
func serialize(value: Variant) -> Variant:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
return _shared.serialize_value(value)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Rebuilds a Godot value from the shape [method serialize] gave it.
|
|
44
|
+
func deserialize(value: Variant) -> Variant:
|
|
45
|
+
return _shared.deserialize_value(value)
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
## The node [param path] names under [param root], or a refusal saying why there is not one.
|
|
@@ -75,148 +76,50 @@ static func node_at(root: Node, path: String) -> Dictionary:
|
|
|
75
76
|
return {"node": node}
|
|
76
77
|
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
return null
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
func _serialize_vector2(value: Vector2) -> Dictionary:
|
|
83
|
-
return {"_type": "Vector2", "x": value.x, "y": value.y}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
func _serialize_vector3(value: Vector3) -> Dictionary:
|
|
87
|
-
return {"_type": "Vector3", "x": value.x, "y": value.y, "z": value.z}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
func _serialize_vector2i(value: Vector2i) -> Dictionary:
|
|
91
|
-
return {"_type": "Vector2i", "x": value.x, "y": value.y}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
func _serialize_vector3i(value: Vector3i) -> Dictionary:
|
|
95
|
-
return {"_type": "Vector3i", "x": value.x, "y": value.y, "z": value.z}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
func _serialize_color(value: Color) -> Dictionary:
|
|
99
|
-
return {"_type": "Color", "r": value.r, "g": value.g, "b": value.b, "a": value.a}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
func _serialize_node_path(value: NodePath) -> Dictionary:
|
|
103
|
-
return {"_type": "NodePath", "path": str(value)}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
func _serialize_array(value: Array) -> Array:
|
|
107
|
-
return value.map(serialize)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
func _serialize_rect2(value: Rect2) -> Dictionary:
|
|
111
|
-
return {"_type": "Rect2", "position": serialize(value.position), "size": serialize(value.size)}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
func _serialize_transform2d(value: Transform2D) -> Dictionary:
|
|
115
|
-
return {
|
|
116
|
-
"_type": "Transform2D",
|
|
117
|
-
"origin": serialize(value.origin),
|
|
118
|
-
"x": serialize(value.x),
|
|
119
|
-
"y": serialize(value.y)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
func _serialize_dictionary(value: Dictionary) -> Dictionary:
|
|
124
|
-
var serialised: Dictionary = {}
|
|
125
|
-
for key: Variant in value:
|
|
126
|
-
serialised[str(key)] = serialize(value[key])
|
|
127
|
-
return serialised
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
## The one case that genuinely needs the class hierarchy, since a Resource is also an Object and
|
|
131
|
-
## its path is the half worth having. A node in the tree is answered with its path, so a
|
|
132
|
-
## property that points at one can be fed straight back to any tool that takes a node.
|
|
133
|
-
func _serialize_object(value: Object) -> Dictionary:
|
|
134
|
-
if value is Resource:
|
|
135
|
-
var resource: Resource = value
|
|
136
|
-
return {"_type": "Resource", "path": resource.resource_path, "class": resource.get_class()}
|
|
137
|
-
if value is Node:
|
|
138
|
-
var node: Node = value
|
|
139
|
-
if node.is_inside_tree():
|
|
140
|
-
return {"_type": "Node", "class": node.get_class(), "path": str(node.get_path())}
|
|
141
|
-
return _describe_object(value)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
## A plain object, said in enough detail to tell it from the one beside it.
|
|
79
|
+
## Whether two values can be compared without the comparison itself failing.
|
|
145
80
|
##
|
|
146
|
-
##
|
|
147
|
-
##
|
|
148
|
-
##
|
|
149
|
-
##
|
|
150
|
-
##
|
|
151
|
-
##
|
|
152
|
-
##
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
var
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
##
|
|
174
|
-
func
|
|
175
|
-
if
|
|
176
|
-
return
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return
|
|
183
|
-
if
|
|
184
|
-
return
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
var rebuilt: Dictionary = {}
|
|
189
|
-
for key: Variant in fields:
|
|
190
|
-
rebuilt[key] = deserialize(fields[key])
|
|
191
|
-
return rebuilt
|
|
192
|
-
|
|
193
|
-
match fields["_type"]:
|
|
194
|
-
"Vector2":
|
|
195
|
-
return Vector2(Read.as_float(fields.get("x", 0)), Read.as_float(fields.get("y", 0)))
|
|
196
|
-
"Vector3":
|
|
197
|
-
return Vector3(
|
|
198
|
-
Read.as_float(fields.get("x", 0)),
|
|
199
|
-
Read.as_float(fields.get("y", 0)),
|
|
200
|
-
Read.as_float(fields.get("z", 0))
|
|
201
|
-
)
|
|
202
|
-
"Vector2i":
|
|
203
|
-
return Vector2i(Read.as_int(fields.get("x", 0)), Read.as_int(fields.get("y", 0)))
|
|
204
|
-
"Vector3i":
|
|
205
|
-
return Vector3i(
|
|
206
|
-
Read.as_int(fields.get("x", 0)),
|
|
207
|
-
Read.as_int(fields.get("y", 0)),
|
|
208
|
-
Read.as_int(fields.get("z", 0))
|
|
209
|
-
)
|
|
210
|
-
"Color":
|
|
211
|
-
return Color(
|
|
212
|
-
Read.as_float(fields.get("r", 0)),
|
|
213
|
-
Read.as_float(fields.get("g", 0)),
|
|
214
|
-
Read.as_float(fields.get("b", 0)),
|
|
215
|
-
Read.as_float(fields.get("a", 1), 1.0)
|
|
216
|
-
)
|
|
217
|
-
"NodePath":
|
|
218
|
-
return NodePath(str(fields.get("path", "")))
|
|
219
|
-
return value
|
|
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
|
+
static func comparable(one: Variant, other: Variant) -> bool:
|
|
92
|
+
var left: int = typeof(one)
|
|
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)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
## 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
|
+
static func acceptable(value: Variant, type: int) -> bool:
|
|
110
|
+
if type == TYPE_NIL:
|
|
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)
|
|
220
123
|
|
|
221
124
|
|
|
222
125
|
## The declared type of one parameter of a method, or TYPE_NIL when the method or the parameter
|
|
@@ -236,11 +139,18 @@ func parameter_type(object: Object, method: String, index: int) -> int:
|
|
|
236
139
|
## A value from the wire fitted to the type a property or parameter declares. Arguments arrive
|
|
237
140
|
## as strings often enough, and callv refuses a "2.0" where a float is wanted, so a string that
|
|
238
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.
|
|
239
146
|
func fitted(value: Variant, type: int) -> Variant:
|
|
240
147
|
var rebuilt: Variant = deserialize(value)
|
|
241
148
|
if type == TYPE_NIL or typeof(rebuilt) == type:
|
|
242
149
|
return rebuilt
|
|
243
150
|
|
|
151
|
+
if rebuilt is Array and LIST_TYPES.has(type):
|
|
152
|
+
return type_convert(rebuilt, type)
|
|
153
|
+
|
|
244
154
|
var simple: Array[int] = [TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING]
|
|
245
155
|
if not simple.has(type) or not simple.has(typeof(rebuilt)):
|
|
246
156
|
return rebuilt
|
|
@@ -108,8 +108,31 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
108
108
|
|
|
109
109
|
var current: Variant = node.get(property)
|
|
110
110
|
var wanted: Variant = _values.fitted(params["value"], typeof(current))
|
|
111
|
+
# Refused before anything is evaluated, because the evaluation is what does the damage: an
|
|
112
|
+
# object compared against anything else is a hard error in GDScript, raised inside the game,
|
|
113
|
+
# which holds it at a debugger break. Both types are named because the caller cannot see either
|
|
114
|
+
# from where they are standing.
|
|
115
|
+
if not Values.comparable(current, wanted):
|
|
116
|
+
return {
|
|
117
|
+
"type": "error",
|
|
118
|
+
"message":
|
|
119
|
+
(
|
|
120
|
+
(
|
|
121
|
+
"%s.%s holds %s and the value to wait for is %s. Those cannot be compared, and"
|
|
122
|
+
+ " waiting on it would stop the game rather than answer about it."
|
|
123
|
+
)
|
|
124
|
+
% [node_path, property, type_string(typeof(current)), type_string(typeof(wanted))]
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
111
128
|
var started: int = Time.get_ticks_msec()
|
|
112
|
-
|
|
129
|
+
# The type is checked every time round, not only at the top: a property that holds an object a
|
|
130
|
+
# frame later is the same error arriving late.
|
|
131
|
+
while (
|
|
132
|
+
Values.comparable(current, wanted)
|
|
133
|
+
and current != wanted
|
|
134
|
+
and Time.get_ticks_msec() - started < timeout_ms
|
|
135
|
+
):
|
|
113
136
|
await _host.get_tree().process_frame
|
|
114
137
|
if not is_instance_valid(node):
|
|
115
138
|
return {"type": "error", "message": "%s was freed while waiting" % node_path}
|
|
@@ -119,7 +142,7 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
119
142
|
"type": "condition",
|
|
120
143
|
"path": node_path,
|
|
121
144
|
"property": property,
|
|
122
|
-
"met": current == wanted,
|
|
145
|
+
"met": Values.comparable(current, wanted) and current == wanted,
|
|
123
146
|
"value": _values.serialize(current),
|
|
124
147
|
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
125
148
|
}
|