gdharness 0.13.4 → 0.13.6

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.
@@ -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
- for prop_name: Variant in properties:
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
@@ -685,7 +695,21 @@ func set_property(params: Dictionary) -> Dictionary:
685
695
  if not _can_read(holder, named):
686
696
  return {"type": "error", "message": _nothing_there(holder, named, str(reached["called"]))}
687
697
  var old_value: Variant = _read(holder, named)
688
- _write(holder, named, _values.fitted(value, typeof(old_value)))
698
+ # The same rule the call path holds: a value that cannot become what the property holds is
699
+ # refused rather than written. Writing it means the engine picks something, and what it picks
700
+ # for a word where a number goes is zero, which the answer then reports as the new value.
701
+ var wanted: int = typeof(old_value)
702
+ var given: Variant = _values.fitted(value, wanted)
703
+ if not Values.acceptable(given, wanted):
704
+ return {
705
+ "type": "error",
706
+ "message":
707
+ (
708
+ "%s.%s holds %s and the value given is %s, which cannot become one."
709
+ % [reached["called"], named, type_string(wanted), type_string(typeof(given))]
710
+ )
711
+ }
712
+ _write(holder, named, given)
689
713
 
690
714
  return {
691
715
  "type": "property_set",
@@ -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
- 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)
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
- 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)
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,29 +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. Arguments arrive
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
- var rebuilt: Variant = deserialize(value)
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
- if typeof(parsed) != TYPE_NIL and typeof(parsed) != TYPE_STRING:
162
- rebuilt = parsed
163
-
164
- 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
@@ -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