gdharness 1.2.30 → 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.
Files changed (37) hide show
  1. package/build/cli.js +7197 -6159
  2. package/build/godot/addons/gdharness_editor/bridge_client.gd +14 -0
  3. package/build/godot/addons/gdharness_editor/file_reads.gd +237 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd.uid +1 -0
  5. package/build/godot/addons/gdharness_editor/plugin.gd +27 -0
  6. package/build/godot/addons/gdharness_editor/property_values.gd +80 -54
  7. package/build/godot/addons/gdharness_editor/scene_file.gd +38 -1
  8. package/build/godot/addons/gdharness_editor/serialisation.gd +46 -0
  9. package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
  10. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +22 -0
  11. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +64 -1
  12. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +82 -44
  13. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +178 -0
  14. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +73 -124
  16. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +39 -0
  17. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +285 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_hover.gd.uid +1 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_input.gd +151 -106
  20. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +112 -0
  21. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd.uid +1 -0
  22. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +5 -4
  23. package/build/godot/addons/gdharness_runtime/runtime_values.gd +1 -47
  24. package/build/godot/addons/gdharness_runtime/serialisation.gd +46 -0
  25. package/build/godot/operations/audio_buses.gd +14 -1
  26. package/build/godot/operations/class_cache.gd +146 -35
  27. package/build/godot/operations/classdb_queries.gd +19 -12
  28. package/build/godot/operations/file_reads.gd +237 -0
  29. package/build/godot/operations/gdscript_authoring.gd +136 -19
  30. package/build/godot/operations/godot_operations.gd +24 -0
  31. package/build/godot/operations/project_config.gd +16 -6
  32. package/build/godot/operations/property_values.gd +767 -0
  33. package/build/godot/operations/rename_checks.gd +5 -2
  34. package/build/godot/operations/serialisation.gd +46 -0
  35. package/build/index.js +4896 -3872
  36. package/build/keeper.js +184 -71
  37. 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)