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