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.
@@ -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
- # What each Godot type becomes on the wire, keyed on typeof() rather than written as a chain of
9
- # `is` tests whose order has to be trusted: Resource had to be tested before Object, or every
10
- # resource came back as a bare class name with its path dropped.
11
- #
12
- # Method names rather than Callables or lambdas: a lambda spanning more than one line inside a
13
- # dictionary literal is where gdformat loses track of every comment in the file and writes them
14
- # all again into the lambda body, on every run.
15
- const SERIALISERS: Dictionary = {
16
- TYPE_NIL: "_serialize_nil",
17
- TYPE_VECTOR2: "_serialize_vector2",
18
- TYPE_VECTOR3: "_serialize_vector3",
19
- TYPE_VECTOR2I: "_serialize_vector2i",
20
- TYPE_VECTOR3I: "_serialize_vector3i",
21
- TYPE_COLOR: "_serialize_color",
22
- TYPE_NODE_PATH: "_serialize_node_path",
23
- TYPE_ARRAY: "_serialize_array",
24
- TYPE_RECT2: "_serialize_rect2",
25
- TYPE_TRANSFORM2D: "_serialize_transform2d",
26
- TYPE_DICTIONARY: "_serialize_dictionary",
27
- TYPE_OBJECT: "_serialize_object",
28
- }
29
-
30
-
31
- ## Converts a Godot value into something JSON can carry. A type with no entry in the table
32
- ## passes through as itself, which is what the JSON-native ones want.
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
- if typeof(value) == TYPE_OBJECT and not is_instance_valid(value):
42
- return null
43
- var serialiser: String = SERIALISERS.get(typeof(value), "")
44
- return call(serialiser, value) if not serialiser.is_empty() else value
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
- func _serialize_nil(_value: Variant) -> Variant:
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
- ## `{"class": "RefCounted"}` was all of it, so a roster of twelve people came back as the word
147
- ## RefCounted twelve times: correct, useless, and indistinguishable from a list of twelve of
148
- ## anything else. The state worth looking at in a game is lists of these, so each one carries what
149
- ## it is (the `class_name` its script declares, or the script's path when it has no name), what it
150
- ## says about itself when the game has given it a `_to_string`, and an id that is at least
151
- ## different per object. None of it is invented: every field is left out when there is nothing to
152
- ## put in it, because a key holding "" is a worse answer than a key that is not there.
153
- func _describe_object(value: Object) -> Dictionary:
154
- var described: Dictionary = {"_type": "Object", "class": value.get_class()}
155
- # Asked as a Variant and narrowed, because get_script answers one and an object without a
156
- # script answers null: casting that to Script is the unsafe cast the engine refuses to compile.
157
- var attached: Variant = value.get_script()
158
- if attached is Script:
159
- var script: Script = attached
160
- var declared: String = String(script.get_global_name())
161
- if not declared.is_empty():
162
- described["script_class"] = declared
163
- elif not script.resource_path.is_empty():
164
- described["script"] = script.resource_path
165
- # Only when the game wrote one. The default is "<RefCounted#31098236>", which is the id again
166
- # in a costume and reads as if the object had been asked and had nothing to say.
167
- if value.has_method("_to_string"):
168
- described["says"] = value.to_string()
169
- described["id"] = value.get_instance_id()
170
- return described
171
-
172
-
173
- ## Rebuilds a Godot value from the shape serialize gave it.
174
- func deserialize(value: Variant) -> Variant:
175
- if value == null:
176
- return null
177
- if value is Array:
178
- var items: Array = value
179
- var rebuilt: Array = []
180
- for item: Variant in items:
181
- rebuilt.append(deserialize(item))
182
- return rebuilt
183
- if not value is Dictionary:
184
- return value
185
-
186
- var fields: Dictionary = value
187
- if not fields.has("_type"):
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
- while current != wanted and Time.get_ticks_msec() - started < timeout_ms:
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
  }