gdharness 1.2.31 → 1.2.32
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 +7212 -6230
- package/build/godot/addons/gdharness_editor/bridge_client.gd +14 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +237 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/plugin.gd +27 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +38 -1
- package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +22 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +64 -1
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +4 -41
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +178 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +54 -120
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +39 -0
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd +285 -0
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +151 -106
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +112 -0
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd.uid +1 -0
- package/build/godot/operations/file_reads.gd +237 -0
- package/build/godot/operations/gdscript_authoring.gd +136 -19
- package/build/godot/operations/godot_operations.gd +24 -0
- package/build/godot/operations/property_values.gd +767 -0
- package/build/godot/operations/rename_checks.gd +5 -2
- package/build/index.js +5437 -4469
- package/build/keeper.js +184 -71
- package/package.json +1 -1
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends RefCounted
|
|
3
|
+
|
|
4
|
+
## What a caller writes for a property, read against the type that property declares, and the
|
|
5
|
+
## question of whether it can be written at all.
|
|
6
|
+
##
|
|
7
|
+
## One parser for the three tools that write properties into files. There were three, each with its
|
|
8
|
+
## own short list of tags it knew: scenes handled eighteen types, resources seven, animation tracks
|
|
9
|
+
## three. A Quaternion keyframe was stored as the dictionary it arrived as, a Rect2 written to a
|
|
10
|
+
## resource likewise, and both were saved that way, so the same value written through two tools
|
|
11
|
+
## came out as two different things and only one of them was the value.
|
|
12
|
+
##
|
|
13
|
+
## The tags themselves are the shared serialiser's, so every shape a read answers with is a shape a
|
|
14
|
+
## write accepts. What is here on top of it is the part only a property knows: the type wanted, so a
|
|
15
|
+
## dictionary shaped like a vector or a bare pair of numbers can be read as one.
|
|
16
|
+
|
|
17
|
+
const Read = preload("reading.gd")
|
|
18
|
+
const Serialisation = preload("serialisation.gd")
|
|
19
|
+
|
|
20
|
+
# The data types a Theme names its items by, as in "Button/colors/font_color", and what each holds.
|
|
21
|
+
const THEME_ITEMS: Dictionary[String, Array] = {
|
|
22
|
+
"colors": [TYPE_COLOR, ""],
|
|
23
|
+
"constants": [TYPE_INT, ""],
|
|
24
|
+
"font_sizes": [TYPE_INT, ""],
|
|
25
|
+
"fonts": [TYPE_OBJECT, "Font"],
|
|
26
|
+
"icons": [TYPE_OBJECT, "Texture2D"],
|
|
27
|
+
"styles": [TYPE_OBJECT, "StyleBox"],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# What each packed array holds, for the four whose elements are not types JSON has. A polygon is
|
|
31
|
+
# written as a list of pairs, and only the property knows that each pair is a Vector2 rather than a
|
|
32
|
+
# list of two numbers: without being told, every element was read as itself and the engine turned
|
|
33
|
+
# the lot into zeroes on the way into the property.
|
|
34
|
+
const PACKED_ELEMENTS: Dictionary = {
|
|
35
|
+
TYPE_PACKED_BYTE_ARRAY: TYPE_INT,
|
|
36
|
+
TYPE_PACKED_INT32_ARRAY: TYPE_INT,
|
|
37
|
+
TYPE_PACKED_INT64_ARRAY: TYPE_INT,
|
|
38
|
+
TYPE_PACKED_FLOAT32_ARRAY: TYPE_FLOAT,
|
|
39
|
+
TYPE_PACKED_FLOAT64_ARRAY: TYPE_FLOAT,
|
|
40
|
+
TYPE_PACKED_STRING_ARRAY: TYPE_STRING,
|
|
41
|
+
TYPE_PACKED_VECTOR2_ARRAY: TYPE_VECTOR2,
|
|
42
|
+
TYPE_PACKED_VECTOR3_ARRAY: TYPE_VECTOR3,
|
|
43
|
+
TYPE_PACKED_VECTOR4_ARRAY: TYPE_VECTOR4,
|
|
44
|
+
TYPE_PACKED_COLOR_ARRAY: TYPE_COLOR,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# The types made of floats, which a read back compares within float precision: a Vector2 property
|
|
48
|
+
# stores 32-bit components, so 0.1 comes back as 0.100000001.
|
|
49
|
+
const FLOAT_SHAPES: Array[int] = [
|
|
50
|
+
TYPE_VECTOR2,
|
|
51
|
+
TYPE_VECTOR3,
|
|
52
|
+
TYPE_VECTOR4,
|
|
53
|
+
TYPE_QUATERNION,
|
|
54
|
+
TYPE_COLOR,
|
|
55
|
+
TYPE_RECT2,
|
|
56
|
+
TYPE_TRANSFORM2D,
|
|
57
|
+
TYPE_BASIS,
|
|
58
|
+
TYPE_TRANSFORM3D,
|
|
59
|
+
TYPE_PLANE,
|
|
60
|
+
TYPE_AABB,
|
|
61
|
+
TYPE_PROJECTION,
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# Entries in a property list that only head a section of the inspector. Object.set ignores their
|
|
65
|
+
# names, so a write to "Visibility" or "Transform" changed nothing and was answered as made.
|
|
66
|
+
const HEADINGS: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
|
|
67
|
+
|
|
68
|
+
var _values: Serialisation = Serialisation.new()
|
|
69
|
+
|
|
70
|
+
# Why the value being parsed cannot be written, when the parse is what found out. A resource is
|
|
71
|
+
# built inside the parse and there is nowhere in a parsed value to put the reason it is wrong.
|
|
72
|
+
var _refused: String = ""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## Turns what arrived over the wire into the Godot value a property wants.
|
|
76
|
+
##
|
|
77
|
+
## Three separate questions, asked in order, because a caller may say what it means in three
|
|
78
|
+
## ways: a dictionary that names its own type, a dictionary shaped like the type the property
|
|
79
|
+
## declares, or a bare array positional for a vector. Each is its own function; asking all three
|
|
80
|
+
## in one was twenty-four exits deep and impossible to follow.
|
|
81
|
+
##
|
|
82
|
+
## [param elements] is what a typed Array or Dictionary holds, as [method elements_of] reads it.
|
|
83
|
+
func parse(value: Variant, expected_type: int = TYPE_NIL, elements: Array = []) -> Variant:
|
|
84
|
+
if value is Dictionary:
|
|
85
|
+
var fields: Dictionary = value
|
|
86
|
+
# A dictionary with keys that are not text arrives as its entries, and is read against the
|
|
87
|
+
# declared element types like any other.
|
|
88
|
+
if str(fields.get("_type", "")) == "Dictionary" and Serialisation.pairs(fields.get("entries")):
|
|
89
|
+
var built: Dictionary = _values.deserialize_value(fields)
|
|
90
|
+
return _parse_entries(built, elements)
|
|
91
|
+
var tagged: Array = _parse_tagged_dictionary(fields)
|
|
92
|
+
if tagged[0]:
|
|
93
|
+
return tagged[1]
|
|
94
|
+
return _parse_shaped_dictionary(fields, expected_type, elements)
|
|
95
|
+
if value is Array:
|
|
96
|
+
var items: Array = value
|
|
97
|
+
return _parse_array(items, expected_type, elements)
|
|
98
|
+
# The same fitting the running game does, so "2.5" written to a float property here and there
|
|
99
|
+
# mean the same thing. It leaves a value it cannot fit alone rather than casting it, which is
|
|
100
|
+
# what lets the write be refused instead of landing as whatever the cast produced.
|
|
101
|
+
return _values.fitted(value, expected_type)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
## Why [param property] on [param holder] cannot be given [param value], or "" when it can.
|
|
105
|
+
##
|
|
106
|
+
## Object.set converts rather than refuses, and the conversion is silent and lossy: a word written
|
|
107
|
+
## to an int property is stored as 0, to a bool as true, to a Vector2 as (0, 0). The file is then
|
|
108
|
+
## saved holding a value nobody asked for and the tool reports the change as made, which is worse
|
|
109
|
+
## than refusing and worse than failing, because there is nothing anywhere to read afterwards that
|
|
110
|
+
## says the value is not the one that was sent.
|
|
111
|
+
func cannot_hold(
|
|
112
|
+
holder: String, property: String, value: Variant, expected_type: int, elements: Array = []
|
|
113
|
+
) -> String:
|
|
114
|
+
var held: String = type_string(expected_type)
|
|
115
|
+
var cost: String = " Setting it would save a different value than the one asked for."
|
|
116
|
+
if not Serialisation.acceptable(value, expected_type):
|
|
117
|
+
var shape: String = "%s.%s is %s and the value given is %s, which cannot become one."
|
|
118
|
+
return shape % [holder, property, held, type_string(typeof(value))] + cost
|
|
119
|
+
|
|
120
|
+
# And the same question of each element, because a list is accepted for a packed or typed array
|
|
121
|
+
# whatever is in it: a polygon written as [{"x": 1}] passes the check above and lands as one zero
|
|
122
|
+
# vector, and [1.5, "x"] for a PackedInt32Array as [1, 0].
|
|
123
|
+
var element: int = Read.as_int(PACKED_ELEMENTS.get(expected_type, TYPE_NIL), TYPE_NIL)
|
|
124
|
+
if expected_type == TYPE_ARRAY and not elements.is_empty():
|
|
125
|
+
element = Read.as_int(elements[0], TYPE_NIL)
|
|
126
|
+
if element != TYPE_NIL and value is Array:
|
|
127
|
+
var items: Array = value
|
|
128
|
+
for index: int in items.size():
|
|
129
|
+
if not Serialisation.acceptable(items[index], element):
|
|
130
|
+
var shape: String = "%s.%s is %s and item %d of the list given is %s, not %s."
|
|
131
|
+
var named: Array = [
|
|
132
|
+
holder, property, held, index, type_string(typeof(items[index])), type_string(element)
|
|
133
|
+
]
|
|
134
|
+
return shape % named + cost
|
|
135
|
+
if expected_type == TYPE_DICTIONARY and elements.size() == 2 and value is Dictionary:
|
|
136
|
+
var entries: Dictionary = value
|
|
137
|
+
var key_type: int = Read.as_int(elements[0], TYPE_NIL)
|
|
138
|
+
var value_type: int = Read.as_int(elements[1], TYPE_NIL)
|
|
139
|
+
for key: Variant in entries:
|
|
140
|
+
if not Serialisation.acceptable(key, key_type):
|
|
141
|
+
var shape: String = "%s.%s has %s keys, and %s is not one."
|
|
142
|
+
return shape % [holder, property, type_string(key_type), shown(key)] + cost
|
|
143
|
+
var entry: Variant = entries[key]
|
|
144
|
+
if not Serialisation.acceptable(entry, value_type):
|
|
145
|
+
var shape: String = "%s.%s holds %s values, and the one given for %s is %s."
|
|
146
|
+
var named: Array = [holder, property, type_string(value_type), shown(key), shown(entry)]
|
|
147
|
+
return shape % named + cost
|
|
148
|
+
return ""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Writes each of [param properties] onto [param target], stopping at the first it cannot write and
|
|
152
|
+
## answering with why, or "" when every one of them went on and holds what was given.
|
|
153
|
+
##
|
|
154
|
+
## In the order they were given, because a property list is not fixed: a ShaderMaterial has no
|
|
155
|
+
## `shader_parameter/tint` until its `shader` is set, so a pass that checked them all before writing
|
|
156
|
+
## any would refuse the pair that works.
|
|
157
|
+
##
|
|
158
|
+
## Every value is read back, because a setter can refuse or change what it is given and say so only
|
|
159
|
+
## in the editor's log. Measured on 4.7.2: a Timer given a wait_time of 0 holds 1, a Sprite2D given
|
|
160
|
+
## 0 hframes holds 1, a Sprite2D given a Material for its texture holds nothing. A value a setter
|
|
161
|
+
## clamped against a property given later in the same call (a ProgressBar's value against its
|
|
162
|
+
## max_value) is set once more after the rest, so the order a caller wrote them in does not decide
|
|
163
|
+
## what is saved.
|
|
164
|
+
##
|
|
165
|
+
## A name may be a colon path into a property's components, "position:x" or "region_rect:size:y",
|
|
166
|
+
## spelled as the runtime tools spell it, so one axis can be written without restating the others.
|
|
167
|
+
##
|
|
168
|
+
## Nothing here saves, so a refusal partway leaves the change in memory and the file as it was. The
|
|
169
|
+
## caller returns the refusal instead of saving.
|
|
170
|
+
func write_all(target: Object, properties: Dictionary) -> String:
|
|
171
|
+
var given: Dictionary = {}
|
|
172
|
+
for key: Variant in properties:
|
|
173
|
+
var property: String = str(key)
|
|
174
|
+
var declared: Dictionary = declaration(target, property)
|
|
175
|
+
if declared.is_empty() and ":" in property:
|
|
176
|
+
declared = _component_slot(target, property)
|
|
177
|
+
if declared.has("refused"):
|
|
178
|
+
return str(declared["refused"])
|
|
179
|
+
if declared.is_empty():
|
|
180
|
+
return "%s has no property %s" % [target.get_class(), property]
|
|
181
|
+
var value: Array = value_for(target, declared, properties[key])
|
|
182
|
+
if not value[0]:
|
|
183
|
+
return str(value[1])
|
|
184
|
+
_write(target, property, value[1])
|
|
185
|
+
given[property] = value[1]
|
|
186
|
+
|
|
187
|
+
for property: String in given:
|
|
188
|
+
if not same(read_at(target, property), given[property]):
|
|
189
|
+
_write(target, property, given[property])
|
|
190
|
+
for property: String in given:
|
|
191
|
+
var held: Variant = read_at(target, property)
|
|
192
|
+
if not same(held, given[property]):
|
|
193
|
+
return (
|
|
194
|
+
(
|
|
195
|
+
"%s.%s was given %s and holds %s after it was set: the engine changed or refused the "
|
|
196
|
+
+ "value, so nothing was written."
|
|
197
|
+
)
|
|
198
|
+
% [target.get_class(), property, shown(given[property]), shown(held)]
|
|
199
|
+
)
|
|
200
|
+
return ""
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
## What [param target] holds under [param key], a property or a colon path into one's components.
|
|
204
|
+
func read_at(target: Object, key: String) -> Variant:
|
|
205
|
+
if ":" in key and declaration(target, key).is_empty():
|
|
206
|
+
return target.get_indexed(NodePath(key))
|
|
207
|
+
return target.get(key)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
## Writes [param value] under [param key], putting a changed component back into the property it
|
|
211
|
+
## was read out of, and that one into the one above it.
|
|
212
|
+
func _write(target: Object, key: String, value: Variant) -> void:
|
|
213
|
+
if ":" in key and declaration(target, key).is_empty():
|
|
214
|
+
target.set_indexed(NodePath(key), value)
|
|
215
|
+
else:
|
|
216
|
+
target.set(key, value)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
## The declaration the last step of [param path] would have, typed as the component it reaches, so
|
|
220
|
+
## a fraction for an integer vector's axis is refused as one for an int property is; or
|
|
221
|
+
## {"refused": why} for the first step that is not there.
|
|
222
|
+
func _component_slot(target: Object, path: String) -> Dictionary:
|
|
223
|
+
var steps: PackedStringArray = path.split(":")
|
|
224
|
+
var owner: String = target.get_class()
|
|
225
|
+
if declaration(target, steps[0]).is_empty():
|
|
226
|
+
return {"refused": "%s has no property %s" % [owner, steps[0]]}
|
|
227
|
+
var value: Variant = target.get(steps[0])
|
|
228
|
+
var walked: String = steps[0]
|
|
229
|
+
for index: int in range(1, steps.size()):
|
|
230
|
+
var step: String = steps[index]
|
|
231
|
+
var has: PackedStringArray = Serialisation.components_of(value)
|
|
232
|
+
var kind: String = type_string(typeof(value))
|
|
233
|
+
var sort: String = ("an " if kind.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + kind
|
|
234
|
+
if has.is_empty():
|
|
235
|
+
return {
|
|
236
|
+
"refused":
|
|
237
|
+
(
|
|
238
|
+
(
|
|
239
|
+
"%s.%s is %s, which a path does not step into: a path reaches the components of a "
|
|
240
|
+
+ "vector, a colour, a rectangle or a transform."
|
|
241
|
+
)
|
|
242
|
+
% [owner, walked, sort]
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
if not step in has:
|
|
246
|
+
return {
|
|
247
|
+
"refused":
|
|
248
|
+
"%s.%s is %s, so it has no %s; it has %s" % [owner, walked, sort, step, ", ".join(has)]
|
|
249
|
+
}
|
|
250
|
+
value = value[step]
|
|
251
|
+
walked += ":" + step
|
|
252
|
+
return {"name": path, "type": typeof(value), "usage": PROPERTY_USAGE_DEFAULT}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
## [param raw] as the value [param declared] takes, as [ok, value] or [false, why not].
|
|
256
|
+
func value_for(target: Object, declared: Dictionary, raw: Variant) -> Array:
|
|
257
|
+
var property: String = str(declared.get("name", ""))
|
|
258
|
+
var expected_type: int = Read.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
|
|
259
|
+
var hint: int = Read.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
260
|
+
if expected_type == TYPE_OBJECT and hint == PROPERTY_HINT_NODE_TYPE:
|
|
261
|
+
return _node_reference(target, property, raw)
|
|
262
|
+
|
|
263
|
+
# A resource-valued property takes the path of one, which is how a caller names a TileSet,
|
|
264
|
+
# a material or a theme: there is no other way to hand a tool a Resource.
|
|
265
|
+
var value: Variant = null
|
|
266
|
+
if expected_type == TYPE_OBJECT and typeof(raw) == TYPE_STRING:
|
|
267
|
+
var found: Array = _resource_at(str(raw), property)
|
|
268
|
+
if not found[0]:
|
|
269
|
+
return found
|
|
270
|
+
value = found[1]
|
|
271
|
+
else:
|
|
272
|
+
var elements: Array = elements_of(declared)
|
|
273
|
+
if _fraction(raw, expected_type, elements):
|
|
274
|
+
return [false, _fraction_refusal(target, property, raw)]
|
|
275
|
+
_refused = ""
|
|
276
|
+
value = parse(raw, expected_type, elements)
|
|
277
|
+
if not _refused.is_empty():
|
|
278
|
+
return [false, _refused]
|
|
279
|
+
var refusal: String = cannot_hold(target.get_class(), property, value, expected_type, elements)
|
|
280
|
+
if not refusal.is_empty():
|
|
281
|
+
return [false, refusal]
|
|
282
|
+
if value is Object:
|
|
283
|
+
var held: Object = value
|
|
284
|
+
var wrong: String = _wrong_class(held, declared)
|
|
285
|
+
if not wrong.is_empty():
|
|
286
|
+
return [false, wrong]
|
|
287
|
+
return [true, value]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
## A property holding a Node is given the node the path leads to from [param target], as the
|
|
291
|
+
## editor's inspector gives it; the scene is saved with the path. A res:// path could never name a
|
|
292
|
+
## node, so the remedy a caller is given is a node path.
|
|
293
|
+
func _node_reference(target: Object, property: String, raw: Variant) -> Array:
|
|
294
|
+
if raw == null:
|
|
295
|
+
return [true, null]
|
|
296
|
+
var node: Node = target as Node
|
|
297
|
+
if node == null:
|
|
298
|
+
return [false, "%s holds a node, and only a node can hold one." % property]
|
|
299
|
+
var rebuilt: Variant = _values.deserialize_value(raw)
|
|
300
|
+
var path: NodePath = NodePath()
|
|
301
|
+
if rebuilt is NodePath:
|
|
302
|
+
path = rebuilt
|
|
303
|
+
elif typeof(raw) == TYPE_STRING and not str(raw).contains("://"):
|
|
304
|
+
path = NodePath(str(raw))
|
|
305
|
+
else:
|
|
306
|
+
return [
|
|
307
|
+
false,
|
|
308
|
+
(
|
|
309
|
+
(
|
|
310
|
+
"%s holds a node, and takes the path to it from the node being set, such as "
|
|
311
|
+
+ '"../Player", not %s.'
|
|
312
|
+
)
|
|
313
|
+
% [property, shown(raw)]
|
|
314
|
+
)
|
|
315
|
+
]
|
|
316
|
+
var found: Node = node.get_node_or_null(path)
|
|
317
|
+
if found == null:
|
|
318
|
+
return [
|
|
319
|
+
false, "%s names %s, and there is no node at that path from %s." % [property, path, node.name]
|
|
320
|
+
]
|
|
321
|
+
return [true, found]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
## Why [param value] is not the class [param declared] holds, or "". Object.set does not refuse
|
|
325
|
+
## an object of the wrong class: measured on 4.7.2, a Sprite2D given a Material for its texture
|
|
326
|
+
## held nothing afterwards, and the scene was saved without the texture it had.
|
|
327
|
+
func _wrong_class(value: Object, declared: Dictionary) -> String:
|
|
328
|
+
var hint: int = Read.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
329
|
+
var wanted: String = str(declared.get("hint_string", ""))
|
|
330
|
+
if hint != PROPERTY_HINT_RESOURCE_TYPE or wanted.is_empty():
|
|
331
|
+
return ""
|
|
332
|
+
var names: PackedStringArray = wanted.split(",", false)
|
|
333
|
+
for name: String in names:
|
|
334
|
+
if value.is_class(name.strip_edges()):
|
|
335
|
+
return ""
|
|
336
|
+
var script: Script = value.get_script()
|
|
337
|
+
while script != null:
|
|
338
|
+
if str(script.get_global_name()) == name.strip_edges():
|
|
339
|
+
return ""
|
|
340
|
+
script = script.get_base_script()
|
|
341
|
+
return (
|
|
342
|
+
"%s holds %s, and the value given is a %s."
|
|
343
|
+
% [str(declared.get("name", "")), " or ".join(names), value.get_class()]
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
## The element types a typed Array or Dictionary property declares: [element] for an array,
|
|
348
|
+
## [key, value] for a dictionary, [] when nothing is declared.
|
|
349
|
+
##
|
|
350
|
+
## Read from the declaration because the value cannot say: a script's exported `Array[int]` sits
|
|
351
|
+
## behind a placeholder in the editor, and measured on 4.7.2 that placeholder stores [1.5, 2.0]
|
|
352
|
+
## as given, which the game then fails to load into the typed array.
|
|
353
|
+
static func elements_of(declared: Dictionary) -> Array:
|
|
354
|
+
var type: int = Read.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
|
|
355
|
+
var hint: int = Read.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
356
|
+
var wanted: String = str(declared.get("hint_string", ""))
|
|
357
|
+
if wanted.is_empty():
|
|
358
|
+
return []
|
|
359
|
+
if hint == PROPERTY_HINT_TYPE_STRING:
|
|
360
|
+
var parts: PackedStringArray = wanted.split(";")
|
|
361
|
+
if type == TYPE_ARRAY and parts.size() == 1:
|
|
362
|
+
return [_type_in_hint(parts[0])]
|
|
363
|
+
if type == TYPE_DICTIONARY and parts.size() == 2:
|
|
364
|
+
return [_type_in_hint(parts[0]), _type_in_hint(parts[1])]
|
|
365
|
+
return []
|
|
366
|
+
if hint == PROPERTY_HINT_ARRAY_TYPE and type == TYPE_ARRAY:
|
|
367
|
+
return [_type_named(wanted)]
|
|
368
|
+
if hint == PROPERTY_HINT_DICTIONARY_TYPE and type == TYPE_DICTIONARY:
|
|
369
|
+
var parts: PackedStringArray = wanted.split(";")
|
|
370
|
+
if parts.size() == 2:
|
|
371
|
+
return [_type_named(parts[0]), _type_named(parts[1])]
|
|
372
|
+
return []
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
## The type at the front of one entry of a type-string hint, "2:" or "24/17:Texture2D".
|
|
376
|
+
static func _type_in_hint(entry: String) -> int:
|
|
377
|
+
var number: String = entry.get_slice(":", 0).get_slice("/", 0)
|
|
378
|
+
return number.to_int() if number.is_valid_int() else TYPE_NIL
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
## The Variant type [param name] names, or TYPE_OBJECT for a class.
|
|
382
|
+
static func _type_named(name: String) -> int:
|
|
383
|
+
var named: String = name.get_slice(":", 0).strip_edges()
|
|
384
|
+
for type: int in TYPE_MAX:
|
|
385
|
+
if type_string(type) == named:
|
|
386
|
+
return type
|
|
387
|
+
return TYPE_OBJECT if not named.is_empty() else TYPE_NIL
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
## Whether [param value] holds a number with a fractional part where an integer is wanted, in a
|
|
391
|
+
## list or a dictionary as much as bare. The conversion would cut 2.7 to 2 without a word, and a
|
|
392
|
+
## dictionary key arrives as the text "2.7", which reads as the same number.
|
|
393
|
+
func _fraction(value: Variant, type: int, elements: Array = []) -> bool:
|
|
394
|
+
if type == TYPE_INT:
|
|
395
|
+
var number: Variant = value
|
|
396
|
+
if typeof(value) == TYPE_STRING:
|
|
397
|
+
number = Read.json_or_null(str(value))
|
|
398
|
+
if typeof(number) != TYPE_FLOAT:
|
|
399
|
+
return false
|
|
400
|
+
var whole: float = number
|
|
401
|
+
return is_finite(whole) and whole != floorf(whole)
|
|
402
|
+
var element: int = Read.as_int(PACKED_ELEMENTS.get(type, TYPE_NIL), TYPE_NIL)
|
|
403
|
+
if type == TYPE_ARRAY and not elements.is_empty():
|
|
404
|
+
element = Read.as_int(elements[0], TYPE_NIL)
|
|
405
|
+
if value is Array and element != TYPE_NIL:
|
|
406
|
+
var items: Array = value
|
|
407
|
+
return items.any(func(item: Variant) -> bool: return _fraction(item, element))
|
|
408
|
+
if type == TYPE_DICTIONARY and elements.size() == 2 and value is Dictionary:
|
|
409
|
+
var entries: Dictionary = value
|
|
410
|
+
var key_type: int = Read.as_int(elements[0], TYPE_NIL)
|
|
411
|
+
var value_type: int = Read.as_int(elements[1], TYPE_NIL)
|
|
412
|
+
for key: Variant in entries:
|
|
413
|
+
if _fraction(key, key_type) or _fraction(entries[key], value_type):
|
|
414
|
+
return true
|
|
415
|
+
return false
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
func _fraction_refusal(target: Object, property: String, raw: Variant) -> String:
|
|
419
|
+
return (
|
|
420
|
+
(
|
|
421
|
+
"%s.%s holds whole numbers, and %s would be cut to fit. Setting it would save a different "
|
|
422
|
+
+ "value than the one asked for."
|
|
423
|
+
)
|
|
424
|
+
% [target.get_class(), property, shown(raw)]
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
## The resource [param path] names, as [found, resource or why not].
|
|
429
|
+
##
|
|
430
|
+
## The project boundary is enforced here as well as on the server, because only the engine knows
|
|
431
|
+
## that this property is one holding a path: an absolute path and a user:// one both load, and
|
|
432
|
+
## neither names a file this project owns. A path nothing is at is refused too, since Object.set
|
|
433
|
+
## stores the null that load answers with and the file is saved having quietly lost the reference.
|
|
434
|
+
func _resource_at(path: String, property: String) -> Array:
|
|
435
|
+
if not (path.begins_with("res://") or path.begins_with("uid://")):
|
|
436
|
+
return [false, "%s takes a res:// or uid:// path, not %s" % [property, path]]
|
|
437
|
+
if path.split("/").has(".."):
|
|
438
|
+
return [false, "%s leaves the project: %s" % [property, path]]
|
|
439
|
+
if path.contains("::"):
|
|
440
|
+
return [
|
|
441
|
+
false,
|
|
442
|
+
(
|
|
443
|
+
(
|
|
444
|
+
"%s is a resource built into %s, and only that file can refer to it. Build a new one "
|
|
445
|
+
+ 'for %s with "_type" set to its class and its properties beside it.'
|
|
446
|
+
)
|
|
447
|
+
% [path, path.get_slice("::", 0), property]
|
|
448
|
+
)
|
|
449
|
+
]
|
|
450
|
+
if not ResourceLoader.exists(path):
|
|
451
|
+
return [false, "No resource at %s for %s" % [path, property]]
|
|
452
|
+
return [true, load(path)]
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
## The entry [param target] declares for [param property], or {} when it declares none. Headings
|
|
456
|
+
## are left out: a category or group shares the list with the properties, and Object.set ignores
|
|
457
|
+
## its name, so without this a typo or a heading saved a file that had not changed and reported it
|
|
458
|
+
## as one that had.
|
|
459
|
+
func declaration(target: Object, property: String) -> Dictionary:
|
|
460
|
+
for declared: Dictionary in target.get_property_list():
|
|
461
|
+
if str(declared.get("name", "")) != property:
|
|
462
|
+
continue
|
|
463
|
+
if Read.as_int(declared.get("usage", 0)) & HEADINGS:
|
|
464
|
+
continue
|
|
465
|
+
return declared
|
|
466
|
+
if target is Theme:
|
|
467
|
+
return _theme_item(property)
|
|
468
|
+
return {}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
## The entry a Theme would declare for [param property] once it held that item, or {} when the name
|
|
472
|
+
## is not an item's. A Theme lists only the items it holds and takes any other through set, so an
|
|
473
|
+
## item a read of one theme answered with was refused when written into another that lacked it.
|
|
474
|
+
static func _theme_item(property: String) -> Dictionary:
|
|
475
|
+
var parts: PackedStringArray = property.split("/")
|
|
476
|
+
if parts.size() != 3 or parts[0].is_empty() or parts[2].is_empty() or not THEME_ITEMS.has(parts[1]):
|
|
477
|
+
return {}
|
|
478
|
+
var item: Array = THEME_ITEMS[parts[1]]
|
|
479
|
+
var kind: int = item[0]
|
|
480
|
+
var made: Dictionary = {"name": property, "type": kind, "usage": PROPERTY_USAGE_DEFAULT}
|
|
481
|
+
if kind == TYPE_OBJECT:
|
|
482
|
+
made["hint"] = PROPERTY_HINT_RESOURCE_TYPE
|
|
483
|
+
made["hint_string"] = item[1]
|
|
484
|
+
return made
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
## Whether [param held] is [param given], as a property read back after a set answers it.
|
|
488
|
+
##
|
|
489
|
+
## Close enough rather than equal for anything made of floats: a Vector2 property stores 32-bit
|
|
490
|
+
## components, so 0.1 comes back as 0.100000001. A packed array read back against the list it was
|
|
491
|
+
## given compares element by element, since `==` between the two is an error rather than false.
|
|
492
|
+
static func same(held: Variant, given: Variant) -> bool:
|
|
493
|
+
var held_type: int = typeof(held)
|
|
494
|
+
var given_type: int = typeof(given)
|
|
495
|
+
# An empty object property reads back as an Object that is null, not as nil, and the two are
|
|
496
|
+
# the same nothing.
|
|
497
|
+
var held_none: bool = held_type == TYPE_NIL or (held_type == TYPE_OBJECT and held == null)
|
|
498
|
+
var given_none: bool = given_type == TYPE_NIL or (given_type == TYPE_OBJECT and given == null)
|
|
499
|
+
if held_none or given_none:
|
|
500
|
+
return held_none == given_none
|
|
501
|
+
if held_type == TYPE_OBJECT or given_type == TYPE_OBJECT:
|
|
502
|
+
return _same_object(held, given)
|
|
503
|
+
if Serialisation.NUMBERS.has(held_type) and Serialisation.NUMBERS.has(given_type):
|
|
504
|
+
if held_type == TYPE_BOOL or given_type == TYPE_BOOL:
|
|
505
|
+
return held_type == given_type and held == given
|
|
506
|
+
var held_number: float = Read.as_float(held)
|
|
507
|
+
var given_number: float = Read.as_float(given)
|
|
508
|
+
return (is_nan(held_number) and is_nan(given_number)) or is_equal_approx(held_number, given_number)
|
|
509
|
+
if Serialisation.TEXTS.has(held_type) and Serialisation.TEXTS.has(given_type):
|
|
510
|
+
return str(held) == str(given)
|
|
511
|
+
if _listed(held_type) and _listed(given_type):
|
|
512
|
+
var held_items: Array = type_convert(held, TYPE_ARRAY)
|
|
513
|
+
var given_items: Array = type_convert(given, TYPE_ARRAY)
|
|
514
|
+
if held_items.size() != given_items.size():
|
|
515
|
+
return false
|
|
516
|
+
for index: int in held_items.size():
|
|
517
|
+
if not same(held_items[index], given_items[index]):
|
|
518
|
+
return false
|
|
519
|
+
return true
|
|
520
|
+
if held_type == TYPE_DICTIONARY and given_type == TYPE_DICTIONARY:
|
|
521
|
+
var held_entries: Dictionary = held
|
|
522
|
+
var given_entries: Dictionary = given
|
|
523
|
+
return _same_entries(held_entries, given_entries)
|
|
524
|
+
if held_type != given_type:
|
|
525
|
+
return false
|
|
526
|
+
return _same_shape(held, given)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
static func _same_object(held: Variant, given: Variant) -> bool:
|
|
530
|
+
if typeof(held) != TYPE_OBJECT or typeof(given) != TYPE_OBJECT:
|
|
531
|
+
return false
|
|
532
|
+
if is_same(held, given):
|
|
533
|
+
return true
|
|
534
|
+
if not held is Resource or not given is Resource:
|
|
535
|
+
return false
|
|
536
|
+
var held_resource: Resource = held
|
|
537
|
+
var given_resource: Resource = given
|
|
538
|
+
if not held_resource.resource_path.is_empty():
|
|
539
|
+
return held_resource.resource_path == given_resource.resource_path
|
|
540
|
+
# A resource local to the scene is duplicated for every instance, so the copy read back is
|
|
541
|
+
# another object of the same class.
|
|
542
|
+
return held_resource.resource_local_to_scene and held_resource.get_class() == given_resource.get_class()
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
static func _same_entries(held: Dictionary, given: Dictionary) -> bool:
|
|
546
|
+
if held.size() != given.size():
|
|
547
|
+
return false
|
|
548
|
+
for key: Variant in given:
|
|
549
|
+
var found: bool = false
|
|
550
|
+
for held_key: Variant in held:
|
|
551
|
+
if same(held_key, key):
|
|
552
|
+
found = same(held[held_key], given[key])
|
|
553
|
+
break
|
|
554
|
+
if not found:
|
|
555
|
+
return false
|
|
556
|
+
return true
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
## Two values of one type made of floats, compared component by component through the type's own
|
|
560
|
+
## text form, which lists every component: each type has its own is_equal_approx, and reaching it
|
|
561
|
+
## through a Variant is a call the typed gate refuses.
|
|
562
|
+
static func _same_shape(held: Variant, given: Variant) -> bool:
|
|
563
|
+
if typeof(held) not in FLOAT_SHAPES:
|
|
564
|
+
return held == given
|
|
565
|
+
var held_parts: Array[float] = _components(held)
|
|
566
|
+
var given_parts: Array[float] = _components(given)
|
|
567
|
+
if held_parts.size() != given_parts.size():
|
|
568
|
+
return false
|
|
569
|
+
for index: int in held_parts.size():
|
|
570
|
+
if not same(held_parts[index], given_parts[index]):
|
|
571
|
+
return false
|
|
572
|
+
return true
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
static func _components(value: Variant) -> Array[float]:
|
|
576
|
+
var parts: Array[float] = []
|
|
577
|
+
var text: String = var_to_str(value)
|
|
578
|
+
var inner: String = text.substr(text.find("(") + 1).trim_suffix(")")
|
|
579
|
+
for part: String in inner.split(",", false):
|
|
580
|
+
parts.append(part.strip_edges().to_float())
|
|
581
|
+
return parts
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
static func _listed(type: int) -> bool:
|
|
585
|
+
return type == TYPE_ARRAY or PACKED_ELEMENTS.has(type)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
## [param value] as a caller would write it, for a refusal to quote.
|
|
589
|
+
func shown(value: Variant) -> String:
|
|
590
|
+
return JSON.stringify(_values.serialize_value(value))
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
## A dictionary carrying its own type name, as the serialiser writes it.
|
|
594
|
+
##
|
|
595
|
+
## Answers [handled, value] rather than just the value, because a handled tag may legitimately
|
|
596
|
+
## produce null: a Resource tag with neither a path nor a class is "this is nothing", not "this is
|
|
597
|
+
## not mine". A tag that names a type but lacks the keys to build it is left unhandled on purpose,
|
|
598
|
+
## so the caller can still read it against the type the property declares.
|
|
599
|
+
func _parse_tagged_dictionary(value: Dictionary) -> Array:
|
|
600
|
+
var type_tag: String = str(value.get("_type", value.get("type", "")))
|
|
601
|
+
if type_tag == "Resource":
|
|
602
|
+
return _parse_resource_tag(value)
|
|
603
|
+
|
|
604
|
+
# The shared builder is the same one the answer was written by, so every shape it writes comes
|
|
605
|
+
# back as itself. `type` as the tag is read as `_type`: it is what these tools answered with
|
|
606
|
+
# before the serialisers were made one, and a caller who kept an answer can still send it.
|
|
607
|
+
var tagged: Dictionary = value
|
|
608
|
+
if not value.has("_type") and not type_tag.is_empty():
|
|
609
|
+
tagged = value.duplicate()
|
|
610
|
+
tagged["_type"] = type_tag
|
|
611
|
+
var rebuilt: Variant = _values.deserialize_value(tagged)
|
|
612
|
+
if not rebuilt is Dictionary:
|
|
613
|
+
return [true, rebuilt]
|
|
614
|
+
|
|
615
|
+
return _parse_new_resource(type_tag, value)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
## A resource named by its path, which goes through the same checks as a path given bare: the
|
|
619
|
+
## project boundary, and a file being there. A tag skipping them loaded an absolute path into the
|
|
620
|
+
## scene, and cleared the property when nothing was at the path.
|
|
621
|
+
func _parse_resource_tag(value: Dictionary) -> Array:
|
|
622
|
+
var resource_path: String = str(value.get("path", ""))
|
|
623
|
+
if not resource_path.is_empty():
|
|
624
|
+
var found: Array = _resource_at(resource_path, "the value")
|
|
625
|
+
if not found[0]:
|
|
626
|
+
_refused = str(found[1])
|
|
627
|
+
return [true, null]
|
|
628
|
+
return [true, found[1]]
|
|
629
|
+
# What a read answers for a resource built into the scene it was read from: a class and no
|
|
630
|
+
# path. Taken as nothing, it cleared the property it was written back to.
|
|
631
|
+
if value.has("class"):
|
|
632
|
+
_refused = (
|
|
633
|
+
(
|
|
634
|
+
"The %s given has no path: it was read from a resource built into a scene, which cannot "
|
|
635
|
+
+ 'be referred to from elsewhere. Build a new one with "_type" set to its class and its '
|
|
636
|
+
+ "properties beside it."
|
|
637
|
+
)
|
|
638
|
+
% str(value["class"])
|
|
639
|
+
)
|
|
640
|
+
return [true, null]
|
|
641
|
+
return [true, null]
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
## A tag naming a Resource class builds a fresh one, its other keys set as properties, so a
|
|
645
|
+
## NavigationRegion2D can arrive with its NavigationPolygon and an AnimationTree with its root
|
|
646
|
+
## state machine in the same add as any other property.
|
|
647
|
+
func _parse_new_resource(type_tag: String, value: Dictionary) -> Array:
|
|
648
|
+
if (
|
|
649
|
+
type_tag.is_empty()
|
|
650
|
+
or not ClassDB.class_exists(type_tag)
|
|
651
|
+
or not ClassDB.is_parent_class(type_tag, "Resource")
|
|
652
|
+
or not ClassDB.can_instantiate(type_tag)
|
|
653
|
+
):
|
|
654
|
+
return [false, null]
|
|
655
|
+
|
|
656
|
+
var built: Resource = ClassDB.instantiate(type_tag)
|
|
657
|
+
var wanted: Dictionary = {}
|
|
658
|
+
for key: Variant in value:
|
|
659
|
+
var property: String = str(key)
|
|
660
|
+
if property != "_type" and property != "type":
|
|
661
|
+
wanted[property] = value[key]
|
|
662
|
+
var refused: String = write_all(built, wanted)
|
|
663
|
+
if not refused.is_empty():
|
|
664
|
+
_refused = refused
|
|
665
|
+
return [true, null]
|
|
666
|
+
return [true, built]
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
## A dictionary with no tag, read against the type the property declares, by the fields the tagged
|
|
670
|
+
## form of that type carries: the shape a read answers with, untagged, is a value too. A rectangle
|
|
671
|
+
## written as the position and size a read gave was refused while the four numbers were taken, and
|
|
672
|
+
## a Rect2i, an AABB or a transform had no untagged form at all. Anything else is a dictionary the
|
|
673
|
+
## property genuinely wants, and its values are read as values in their own right: kept as the tags
|
|
674
|
+
## they arrived as, a Vector2 read off one node and written to another was saved as a dictionary
|
|
675
|
+
## holding the word "Vector2".
|
|
676
|
+
func _parse_shaped_dictionary(value: Dictionary, expected_type: int, elements: Array) -> Variant:
|
|
677
|
+
if expected_type == TYPE_NIL or expected_type == TYPE_DICTIONARY:
|
|
678
|
+
return _parse_entries(value, elements)
|
|
679
|
+
var tag: String = type_string(expected_type)
|
|
680
|
+
if Serialisation.REQUIRED.has(tag):
|
|
681
|
+
var tagged: Dictionary = value.duplicate()
|
|
682
|
+
tagged["_type"] = tag
|
|
683
|
+
var built: Variant = _values.deserialize_value(tagged)
|
|
684
|
+
if typeof(built) == expected_type:
|
|
685
|
+
return built
|
|
686
|
+
return value
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
## The entries of a dictionary a property holds, each value read as a value in its own right, and
|
|
690
|
+
## typed as the property declares when every entry fits.
|
|
691
|
+
func _parse_entries(value: Dictionary, elements: Array) -> Dictionary:
|
|
692
|
+
var key_type: int = TYPE_NIL
|
|
693
|
+
var value_type: int = TYPE_NIL
|
|
694
|
+
if elements.size() == 2:
|
|
695
|
+
key_type = Read.as_int(elements[0], TYPE_NIL)
|
|
696
|
+
value_type = Read.as_int(elements[1], TYPE_NIL)
|
|
697
|
+
var entries: Dictionary = {}
|
|
698
|
+
for key: Variant in value:
|
|
699
|
+
entries[_values.fitted(key, key_type)] = parse(value[key], value_type)
|
|
700
|
+
return _typed_dictionary(entries, key_type, value_type)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
## [param entries] as the typed dictionary a property declares, so the file holds the type the
|
|
704
|
+
## game's script will load it into. Left untyped when an entry does not fit, for the check after
|
|
705
|
+
## the parse to refuse by name.
|
|
706
|
+
func _typed_dictionary(entries: Dictionary, key_type: int, value_type: int) -> Dictionary:
|
|
707
|
+
if key_type == TYPE_NIL and value_type == TYPE_NIL:
|
|
708
|
+
return entries
|
|
709
|
+
if key_type == TYPE_OBJECT or value_type == TYPE_OBJECT:
|
|
710
|
+
return entries
|
|
711
|
+
for key: Variant in entries:
|
|
712
|
+
if key_type != TYPE_NIL and typeof(key) != key_type:
|
|
713
|
+
return entries
|
|
714
|
+
if not Serialisation.acceptable(entries[key], value_type):
|
|
715
|
+
return entries
|
|
716
|
+
var fitted: Dictionary = {}
|
|
717
|
+
for key: Variant in entries:
|
|
718
|
+
fitted[key] = type_convert(entries[key], value_type) if value_type != TYPE_NIL else entries[key]
|
|
719
|
+
return Dictionary(fitted, key_type, &"", null, value_type, &"", null)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
## An array, either positional for a vector the property declares, or a list to parse per item.
|
|
723
|
+
func _parse_array(value: Array, expected_type: int, elements: Array) -> Variant:
|
|
724
|
+
if PACKED_ELEMENTS.has(expected_type):
|
|
725
|
+
var element: int = Read.as_int(PACKED_ELEMENTS[expected_type], TYPE_NIL)
|
|
726
|
+
return value.map(func(item: Variant) -> Variant: return parse(item, element))
|
|
727
|
+
|
|
728
|
+
match expected_type:
|
|
729
|
+
TYPE_VECTOR2:
|
|
730
|
+
if value.size() == 2:
|
|
731
|
+
return Vector2(Read.as_float(value[0]), Read.as_float(value[1]))
|
|
732
|
+
TYPE_VECTOR2I:
|
|
733
|
+
if value.size() == 2:
|
|
734
|
+
return Vector2i(Read.as_int(value[0]), Read.as_int(value[1]))
|
|
735
|
+
TYPE_VECTOR3:
|
|
736
|
+
if value.size() == 3:
|
|
737
|
+
return Vector3(Read.as_float(value[0]), Read.as_float(value[1]), Read.as_float(value[2]))
|
|
738
|
+
TYPE_VECTOR3I:
|
|
739
|
+
if value.size() == 3:
|
|
740
|
+
return Vector3i(Read.as_int(value[0]), Read.as_int(value[1]), Read.as_int(value[2]))
|
|
741
|
+
TYPE_VECTOR4:
|
|
742
|
+
if value.size() == 4:
|
|
743
|
+
return Vector4(
|
|
744
|
+
Read.as_float(value[0]),
|
|
745
|
+
Read.as_float(value[1]),
|
|
746
|
+
Read.as_float(value[2]),
|
|
747
|
+
Read.as_float(value[3])
|
|
748
|
+
)
|
|
749
|
+
TYPE_COLOR:
|
|
750
|
+
if value.size() == 3 or value.size() == 4:
|
|
751
|
+
return Color(
|
|
752
|
+
Read.as_float(value[0]),
|
|
753
|
+
Read.as_float(value[1]),
|
|
754
|
+
Read.as_float(value[2]),
|
|
755
|
+
Read.as_float(value[3]) if value.size() == 4 else 1.0
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
var element_type: int = TYPE_NIL
|
|
759
|
+
if expected_type == TYPE_ARRAY and not elements.is_empty():
|
|
760
|
+
element_type = Read.as_int(elements[0], TYPE_NIL)
|
|
761
|
+
var items: Array = value.map(func(item: Variant) -> Variant: return parse(item, element_type))
|
|
762
|
+
if element_type == TYPE_NIL or element_type == TYPE_OBJECT:
|
|
763
|
+
return items
|
|
764
|
+
if not items.all(func(item: Variant) -> bool: return Serialisation.acceptable(item, element_type)):
|
|
765
|
+
return items
|
|
766
|
+
var fitted: Array = items.map(func(item: Variant) -> Variant: return type_convert(item, element_type))
|
|
767
|
+
return Array(fitted, element_type, &"", null)
|