gdharness 1.3.0 → 1.3.2
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 +322 -96
- package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -19
- package/build/godot/addons/gdharness_editor/bridge_client.gd +8 -8
- package/build/godot/addons/gdharness_editor/declared_values.gd +116 -0
- package/build/godot/addons/gdharness_editor/declared_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +65 -25
- package/build/godot/addons/gdharness_editor/headless_check.gd +7 -7
- package/build/godot/addons/gdharness_editor/plugin.gd +6 -6
- package/build/godot/addons/gdharness_editor/property_values.gd +222 -59
- package/build/godot/addons/gdharness_editor/reading.gd +65 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +28 -23
- package/build/godot/addons/gdharness_editor/serialisation.gd +49 -94
- package/build/godot/addons/gdharness_editor/tool_executor.gd +24 -24
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +52 -41
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +4 -4
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +2 -2
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +14 -14
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +175 -74
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +17 -17
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +14 -12
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +7 -7
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +88 -84
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +4 -4
- package/build/godot/addons/gdharness_editor/value_shapes.gd +118 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +65 -0
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +18 -18
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +29 -29
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +21 -18
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +86 -80
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +65 -64
- package/build/godot/addons/gdharness_runtime/runtime_items.gd +6 -6
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd +8 -8
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +6 -6
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +10 -10
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +7 -7
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +41 -41
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +3 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +33 -33
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +20 -20
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +52 -25
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +56 -50
- package/build/godot/addons/gdharness_runtime/serialisation.gd +49 -94
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +118 -0
- package/build/godot/addons/gdharness_runtime/value_shapes.gd.uid +1 -0
- package/build/godot/operations/animation_reads.gd +7 -19
- package/build/godot/operations/audio_buses.gd +10 -10
- package/build/godot/operations/class_cache.gd +16 -16
- package/build/godot/operations/classdb_queries.gd +25 -15
- package/build/godot/operations/declared_values.gd +116 -0
- package/build/godot/operations/dependencies.gd +141 -33
- package/build/godot/operations/editor_tools.gd +12 -12
- package/build/godot/operations/export_presets.gd +14 -14
- package/build/godot/operations/file_reads.gd +65 -25
- package/build/godot/operations/file_walk.gd +34 -0
- package/build/godot/operations/gdscript_analysis.gd +27 -22
- package/build/godot/operations/gdscript_authoring.gd +122 -17
- package/build/godot/operations/godot_operations.gd +66 -63
- package/build/godot/operations/import_pipeline.gd +59 -20
- package/build/godot/operations/input_actions.gd +90 -18
- package/build/godot/operations/logger.gd +1 -1
- package/build/godot/operations/plugins.gd +8 -8
- package/build/godot/operations/project_config.gd +83 -20
- package/build/godot/operations/project_diagnostics.gd +36 -11
- package/build/godot/operations/property_values.gd +222 -59
- package/build/godot/operations/reading.gd +65 -0
- package/build/godot/operations/rename_checks.gd +3 -3
- package/build/godot/operations/resource_files.gd +9 -6
- package/build/godot/operations/script_check.gd +23 -7
- package/build/godot/operations/serialisation.gd +49 -94
- package/build/godot/operations/value_shapes.gd +118 -0
- package/package.json +1 -1
|
@@ -14,8 +14,10 @@ extends RefCounted
|
|
|
14
14
|
## write accepts. What is here on top of it is the part only a property knows: the type wanted, so a
|
|
15
15
|
## dictionary shaped like a vector or a bare pair of numbers can be read as one.
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const GdhDeclared = preload("declared_values.gd")
|
|
18
|
+
const GdhRead = preload("reading.gd")
|
|
19
|
+
const GdhSerialisation = preload("serialisation.gd")
|
|
20
|
+
const GdhShapes = preload("value_shapes.gd")
|
|
19
21
|
|
|
20
22
|
# The data types a Theme names its items by, as in "Button/colors/font_color", and what each holds.
|
|
21
23
|
const THEME_ITEMS: Dictionary[String, Array] = {
|
|
@@ -65,12 +67,23 @@ const FLOAT_SHAPES: Array[int] = [
|
|
|
65
67
|
# names, so a write to "Visibility" or "Transform" changed nothing and was answered as made.
|
|
66
68
|
const HEADINGS: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
|
|
67
69
|
|
|
68
|
-
var _values:
|
|
70
|
+
var _values: GdhSerialisation = GdhSerialisation.new()
|
|
69
71
|
|
|
70
72
|
# Why the value being parsed cannot be written, when the parse is what found out. A resource is
|
|
71
73
|
# built inside the parse and there is nowhere in a parsed value to put the reason it is wrong.
|
|
72
74
|
var _refused: String = ""
|
|
73
75
|
|
|
76
|
+
# The stored properties the last write_all changed without being given them, by name, as [what it
|
|
77
|
+
# held before, what it holds now]. A setter keeps other properties in step with the one it is given:
|
|
78
|
+
# a Gradient given a third colour gives that point an offset of 0, a Control given a size moves its
|
|
79
|
+
# offsets. An answer naming only what was given leaves the caller to guess the rest.
|
|
80
|
+
var _also_changed: Dictionary = {}
|
|
81
|
+
|
|
82
|
+
# The values the last write_all wrote outside what their property declares, by name, as a sentence
|
|
83
|
+
# saying so: a number past an export range, or one no enum value has. The engine stores either, and
|
|
84
|
+
# the inspector would not have let it be entered, so a write answered as made said nothing of it.
|
|
85
|
+
var _outside_declared: Dictionary = {}
|
|
86
|
+
|
|
74
87
|
|
|
75
88
|
## Turns what arrived over the wire into the Godot value a property wants.
|
|
76
89
|
##
|
|
@@ -85,7 +98,7 @@ func parse(value: Variant, expected_type: int = TYPE_NIL, elements: Array = [])
|
|
|
85
98
|
var fields: Dictionary = value
|
|
86
99
|
# A dictionary with keys that are not text arrives as its entries, and is read against the
|
|
87
100
|
# declared element types like any other.
|
|
88
|
-
if str(fields.get("_type", "")) == "Dictionary" and
|
|
101
|
+
if str(fields.get("_type", "")) == "Dictionary" and GdhSerialisation.pairs(fields.get("entries")):
|
|
89
102
|
var built: Dictionary = _values.deserialize_value(fields)
|
|
90
103
|
return _parse_entries(built, elements)
|
|
91
104
|
var tagged: Array = _parse_tagged_dictionary(fields)
|
|
@@ -113,38 +126,39 @@ func cannot_hold(
|
|
|
113
126
|
) -> String:
|
|
114
127
|
var held: String = type_string(expected_type)
|
|
115
128
|
var cost: String = " Setting it would save a different value than the one asked for."
|
|
116
|
-
if not
|
|
129
|
+
if not GdhSerialisation.acceptable(value, expected_type):
|
|
117
130
|
var shape: String = "%s.%s is %s and the value given is %s, which cannot become one."
|
|
118
|
-
|
|
131
|
+
var remedy: String = GdhShapes.remedy(value, expected_type)
|
|
132
|
+
return shape % [holder, property, held, type_string(typeof(value))] + remedy + cost
|
|
119
133
|
|
|
120
134
|
# And the same question of each element, because a list is accepted for a packed or typed array
|
|
121
135
|
# whatever is in it: a polygon written as [{"x": 1}] passes the check above and lands as one zero
|
|
122
136
|
# vector, and [1.5, "x"] for a PackedInt32Array as [1, 0].
|
|
123
|
-
var element: int =
|
|
137
|
+
var element: int = GdhRead.as_int(PACKED_ELEMENTS.get(expected_type, TYPE_NIL), TYPE_NIL)
|
|
124
138
|
if expected_type == TYPE_ARRAY and not elements.is_empty():
|
|
125
|
-
element =
|
|
139
|
+
element = GdhRead.as_int(elements[0], TYPE_NIL)
|
|
126
140
|
if element != TYPE_NIL and value is Array:
|
|
127
141
|
var items: Array = value
|
|
128
142
|
for index: int in items.size():
|
|
129
|
-
if not
|
|
143
|
+
if not GdhSerialisation.acceptable(items[index], element):
|
|
130
144
|
var shape: String = "%s.%s is %s and item %d of the list given is %s, not %s."
|
|
131
145
|
var named: Array = [
|
|
132
146
|
holder, property, held, index, type_string(typeof(items[index])), type_string(element)
|
|
133
147
|
]
|
|
134
|
-
return shape % named + cost
|
|
148
|
+
return shape % named + GdhShapes.remedy(items[index], element) + cost
|
|
135
149
|
if expected_type == TYPE_DICTIONARY and elements.size() == 2 and value is Dictionary:
|
|
136
150
|
var entries: Dictionary = value
|
|
137
|
-
var key_type: int =
|
|
138
|
-
var value_type: int =
|
|
151
|
+
var key_type: int = GdhRead.as_int(elements[0], TYPE_NIL)
|
|
152
|
+
var value_type: int = GdhRead.as_int(elements[1], TYPE_NIL)
|
|
139
153
|
for key: Variant in entries:
|
|
140
|
-
if not
|
|
154
|
+
if not GdhSerialisation.acceptable(key, key_type):
|
|
141
155
|
var shape: String = "%s.%s has %s keys, and %s is not one."
|
|
142
156
|
return shape % [holder, property, type_string(key_type), shown(key)] + cost
|
|
143
157
|
var entry: Variant = entries[key]
|
|
144
|
-
if not
|
|
158
|
+
if not GdhSerialisation.acceptable(entry, value_type):
|
|
145
159
|
var shape: String = "%s.%s holds %s values, and the one given for %s is %s."
|
|
146
160
|
var named: Array = [holder, property, type_string(value_type), shown(key), shown(entry)]
|
|
147
|
-
return shape % named + cost
|
|
161
|
+
return shape % named + GdhShapes.remedy(entry, value_type) + cost
|
|
148
162
|
return ""
|
|
149
163
|
|
|
150
164
|
|
|
@@ -168,19 +182,35 @@ func cannot_hold(
|
|
|
168
182
|
## Nothing here saves, so a refusal partway leaves the change in memory and the file as it was. The
|
|
169
183
|
## caller returns the refusal instead of saving.
|
|
170
184
|
func write_all(target: Object, properties: Dictionary) -> String:
|
|
185
|
+
_also_changed = {}
|
|
186
|
+
_outside_declared = {}
|
|
187
|
+
var named: Dictionary = {}
|
|
188
|
+
for key: Variant in properties:
|
|
189
|
+
named[str(key).get_slice(":", 0)] = true
|
|
190
|
+
var before: Dictionary = _stored(target, named)
|
|
171
191
|
var given: Dictionary = {}
|
|
192
|
+
# A refusal of one of several leaves the others unwritten too, which a refusal naming only the
|
|
193
|
+
# one it refused left a caller to guess.
|
|
194
|
+
var unwritten: String = (
|
|
195
|
+
"" if properties.size() < 2 else " None of the %d properties given was written." % properties.size()
|
|
196
|
+
)
|
|
172
197
|
for key: Variant in properties:
|
|
173
198
|
var property: String = str(key)
|
|
174
199
|
var declared: Dictionary = declaration(target, property)
|
|
175
200
|
if declared.is_empty() and ":" in property:
|
|
176
201
|
declared = _component_slot(target, property)
|
|
177
202
|
if declared.has("refused"):
|
|
178
|
-
return str(declared["refused"])
|
|
203
|
+
return str(declared["refused"]) + unwritten
|
|
204
|
+
if declared.is_empty() and target is ShaderMaterial and property.begins_with("shader_parameter/"):
|
|
205
|
+
return _no_uniform(target as ShaderMaterial, property) + unwritten
|
|
179
206
|
if declared.is_empty():
|
|
180
|
-
return "%s has no property %s" % [target
|
|
207
|
+
return "%s has no property %s." % [class_called(target), property] + unwritten
|
|
181
208
|
var value: Array = value_for(target, declared, properties[key])
|
|
182
209
|
if not value[0]:
|
|
183
|
-
return str(value[1])
|
|
210
|
+
return str(value[1]) + unwritten
|
|
211
|
+
var outside: String = GdhDeclared.outside(target, declared, value[1], shown(value[1]))
|
|
212
|
+
if not outside.is_empty():
|
|
213
|
+
_outside_declared[property] = outside
|
|
184
214
|
_write(target, property, value[1])
|
|
185
215
|
given[property] = value[1]
|
|
186
216
|
|
|
@@ -197,9 +227,83 @@ func write_all(target: Object, properties: Dictionary) -> String:
|
|
|
197
227
|
)
|
|
198
228
|
% [target.get_class(), property, shown(given[property]), shown(held)]
|
|
199
229
|
)
|
|
230
|
+
for property: String in before:
|
|
231
|
+
var now: Variant = target.get(property)
|
|
232
|
+
if not same(now, before[property]):
|
|
233
|
+
_also_changed[property] = [before[property], now]
|
|
200
234
|
return ""
|
|
201
235
|
|
|
202
236
|
|
|
237
|
+
## Why [param material] has no [param property]: a ShaderMaterial's parameters are its shader's
|
|
238
|
+
## uniforms, so the shader is what lacks one, and "ShaderMaterial has no property" named the
|
|
239
|
+
## material.
|
|
240
|
+
static func _no_uniform(material: ShaderMaterial, property: String) -> String:
|
|
241
|
+
var uniform: String = property.trim_prefix("shader_parameter/")
|
|
242
|
+
if material.shader == null:
|
|
243
|
+
return "The ShaderMaterial has no shader, so it has no parameter %s yet; set shader first." % uniform
|
|
244
|
+
var shader: String = material.shader.resource_path
|
|
245
|
+
var named: String = shader if not shader.is_empty() else "Its shader"
|
|
246
|
+
var uniforms: Array = []
|
|
247
|
+
for entry: Dictionary in material.shader.get_shader_uniform_list():
|
|
248
|
+
uniforms.append(str(entry["name"]))
|
|
249
|
+
var said: String = "%s declares no uniform %s, which %s would set" % [named, uniform, property]
|
|
250
|
+
if uniforms.is_empty():
|
|
251
|
+
return said + "; it declares no uniforms."
|
|
252
|
+
return GdhRead.offered(said, uniform, uniforms) + "."
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
## Puts under alsoChanged in [param answer] each property the last [method write_all] changed
|
|
256
|
+
## without being given it, as {was, now}, and under outsideDeclared each value it wrote outside what
|
|
257
|
+
## its property declares, when there are any. A node is named by its path from [param relative_to],
|
|
258
|
+
## as a scene answer names one. Called before the scene is written, since the write frees the nodes
|
|
259
|
+
## these values can hold.
|
|
260
|
+
func note_write(answer: Dictionary, relative_to: Node = null) -> void:
|
|
261
|
+
if not _outside_declared.is_empty():
|
|
262
|
+
answer["outsideDeclared"] = _outside_declared.duplicate()
|
|
263
|
+
if _also_changed.is_empty():
|
|
264
|
+
return
|
|
265
|
+
var noted: Dictionary = {}
|
|
266
|
+
for property: Variant in _also_changed:
|
|
267
|
+
var pair: Array = _also_changed[property]
|
|
268
|
+
noted[property] = {"was": _readable(pair[0], relative_to), "now": _readable(pair[1], relative_to)}
|
|
269
|
+
answer["alsoChanged"] = noted
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
func _readable(value: Variant, relative_to: Node) -> Variant:
|
|
273
|
+
if value is Node and relative_to != null:
|
|
274
|
+
var target: Node = value
|
|
275
|
+
return _values.serialize_value(relative_to.get_path_to(target))
|
|
276
|
+
if value is Array:
|
|
277
|
+
var items: Array = value
|
|
278
|
+
return items.map(func(item: Variant) -> Variant: return _readable(item, relative_to))
|
|
279
|
+
return _values.serialize_value(value)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
## What [param target] stores, by name, but its script and the properties in [param leave]. A list
|
|
283
|
+
## or a map is copied, since a setter can change the one it holds in place.
|
|
284
|
+
func _stored(target: Object, leave: Dictionary) -> Dictionary:
|
|
285
|
+
var held: Dictionary = {}
|
|
286
|
+
for entry: Dictionary in target.get_property_list():
|
|
287
|
+
var name: String = str(entry["name"])
|
|
288
|
+
var usage: int = GdhRead.as_int(entry["usage"])
|
|
289
|
+
if (
|
|
290
|
+
usage & PROPERTY_USAGE_STORAGE == 0
|
|
291
|
+
or usage & HEADINGS != 0
|
|
292
|
+
or name == "script"
|
|
293
|
+
or leave.has(name)
|
|
294
|
+
):
|
|
295
|
+
continue
|
|
296
|
+
var value: Variant = target.get(name)
|
|
297
|
+
if value is Array:
|
|
298
|
+
var listed: Array = value
|
|
299
|
+
value = listed.duplicate(true)
|
|
300
|
+
elif value is Dictionary:
|
|
301
|
+
var mapped: Dictionary = value
|
|
302
|
+
value = mapped.duplicate(true)
|
|
303
|
+
held[name] = value
|
|
304
|
+
return held
|
|
305
|
+
|
|
306
|
+
|
|
203
307
|
## What [param target] holds under [param key], a property or a colon path into one's components.
|
|
204
308
|
func read_at(target: Object, key: String) -> Variant:
|
|
205
309
|
if ":" in key and declaration(target, key).is_empty():
|
|
@@ -228,9 +332,8 @@ func _component_slot(target: Object, path: String) -> Dictionary:
|
|
|
228
332
|
var walked: String = steps[0]
|
|
229
333
|
for index: int in range(1, steps.size()):
|
|
230
334
|
var step: String = steps[index]
|
|
231
|
-
var has: PackedStringArray =
|
|
232
|
-
var
|
|
233
|
-
var sort: String = ("an " if kind.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + kind
|
|
335
|
+
var has: PackedStringArray = GdhSerialisation.components_of(value)
|
|
336
|
+
var sort: String = GdhRead.with_article(type_string(typeof(value)))
|
|
234
337
|
if has.is_empty():
|
|
235
338
|
return {
|
|
236
339
|
"refused":
|
|
@@ -255,10 +358,22 @@ func _component_slot(target: Object, path: String) -> Dictionary:
|
|
|
255
358
|
## [param raw] as the value [param declared] takes, as [ok, value] or [false, why not].
|
|
256
359
|
func value_for(target: Object, declared: Dictionary, raw: Variant) -> Array:
|
|
257
360
|
var property: String = str(declared.get("name", ""))
|
|
258
|
-
var expected_type: int =
|
|
259
|
-
var hint: int =
|
|
361
|
+
var expected_type: int = GdhRead.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
|
|
362
|
+
var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
260
363
|
if expected_type == TYPE_OBJECT and hint == PROPERTY_HINT_NODE_TYPE:
|
|
261
364
|
return _node_reference(target, property, raw)
|
|
365
|
+
# An enum is written by the name of a value as well as its number, since the name is what the
|
|
366
|
+
# script and the inspector show and the number is only where the value happens to sit.
|
|
367
|
+
if expected_type == TYPE_INT and hint == PROPERTY_HINT_ENUM and typeof(raw) == TYPE_STRING:
|
|
368
|
+
var choices: Dictionary = GdhDeclared.enum_of(target, declared)
|
|
369
|
+
var chosen: Variant = GdhDeclared.enum_named(choices, str(raw))
|
|
370
|
+
if chosen == null:
|
|
371
|
+
var hinted: Dictionary = GdhDeclared.enum_values(str(declared.get("hint_string", "")))
|
|
372
|
+
chosen = GdhDeclared.enum_named(hinted, str(raw))
|
|
373
|
+
if chosen != null:
|
|
374
|
+
return [true, chosen]
|
|
375
|
+
if not str(raw).strip_edges().is_valid_int():
|
|
376
|
+
return [false, GdhDeclared.enum_refusal(class_called(target), property, str(raw), choices)]
|
|
262
377
|
|
|
263
378
|
# A resource-valued property takes the path of one, which is how a caller names a TileSet,
|
|
264
379
|
# a material or a theme: there is no other way to hand a tool a Resource.
|
|
@@ -272,14 +387,14 @@ func value_for(target: Object, declared: Dictionary, raw: Variant) -> Array:
|
|
|
272
387
|
var elements: Array = elements_of(declared)
|
|
273
388
|
if _fraction(raw, expected_type, elements):
|
|
274
389
|
return [false, _fraction_refusal(target, property, raw)]
|
|
275
|
-
var rounded: String =
|
|
390
|
+
var rounded: String = GdhSerialisation.inexact(raw, expected_type)
|
|
276
391
|
if not rounded.is_empty():
|
|
277
|
-
return [false, "%s.%s holds int: %s." % [target
|
|
392
|
+
return [false, "%s.%s holds int: %s." % [class_called(target), property, rounded]]
|
|
278
393
|
_refused = ""
|
|
279
394
|
value = parse(raw, expected_type, elements)
|
|
280
395
|
if not _refused.is_empty():
|
|
281
396
|
return [false, _refused]
|
|
282
|
-
var refusal: String = cannot_hold(target
|
|
397
|
+
var refusal: String = cannot_hold(class_called(target), property, value, expected_type, elements)
|
|
283
398
|
if not refusal.is_empty():
|
|
284
399
|
return [false, refusal]
|
|
285
400
|
if value is Object:
|
|
@@ -328,7 +443,7 @@ func _node_reference(target: Object, property: String, raw: Variant) -> Array:
|
|
|
328
443
|
## an object of the wrong class: measured on 4.7.2, a Sprite2D given a Material for its texture
|
|
329
444
|
## held nothing afterwards, and the scene was saved without the texture it had.
|
|
330
445
|
func _wrong_class(value: Object, declared: Dictionary) -> String:
|
|
331
|
-
var hint: int =
|
|
446
|
+
var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
332
447
|
var wanted: String = str(declared.get("hint_string", ""))
|
|
333
448
|
if hint != PROPERTY_HINT_RESOURCE_TYPE or wanted.is_empty():
|
|
334
449
|
return ""
|
|
@@ -354,8 +469,8 @@ func _wrong_class(value: Object, declared: Dictionary) -> String:
|
|
|
354
469
|
## behind a placeholder in the editor, and measured on 4.7.2 that placeholder stores [1.5, 2.0]
|
|
355
470
|
## as given, which the game then fails to load into the typed array.
|
|
356
471
|
static func elements_of(declared: Dictionary) -> Array:
|
|
357
|
-
var type: int =
|
|
358
|
-
var hint: int =
|
|
472
|
+
var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
|
|
473
|
+
var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
359
474
|
var wanted: String = str(declared.get("hint_string", ""))
|
|
360
475
|
if wanted.is_empty():
|
|
361
476
|
return []
|
|
@@ -397,21 +512,21 @@ func _fraction(value: Variant, type: int, elements: Array = []) -> bool:
|
|
|
397
512
|
if type == TYPE_INT:
|
|
398
513
|
var number: Variant = value
|
|
399
514
|
if typeof(value) == TYPE_STRING:
|
|
400
|
-
number =
|
|
515
|
+
number = GdhRead.json_or_null(str(value))
|
|
401
516
|
if typeof(number) != TYPE_FLOAT:
|
|
402
517
|
return false
|
|
403
518
|
var whole: float = number
|
|
404
519
|
return is_finite(whole) and whole != floorf(whole)
|
|
405
|
-
var element: int =
|
|
520
|
+
var element: int = GdhRead.as_int(PACKED_ELEMENTS.get(type, TYPE_NIL), TYPE_NIL)
|
|
406
521
|
if type == TYPE_ARRAY and not elements.is_empty():
|
|
407
|
-
element =
|
|
522
|
+
element = GdhRead.as_int(elements[0], TYPE_NIL)
|
|
408
523
|
if value is Array and element != TYPE_NIL:
|
|
409
524
|
var items: Array = value
|
|
410
525
|
return items.any(func(item: Variant) -> bool: return _fraction(item, element))
|
|
411
526
|
if type == TYPE_DICTIONARY and elements.size() == 2 and value is Dictionary:
|
|
412
527
|
var entries: Dictionary = value
|
|
413
|
-
var key_type: int =
|
|
414
|
-
var value_type: int =
|
|
528
|
+
var key_type: int = GdhRead.as_int(elements[0], TYPE_NIL)
|
|
529
|
+
var value_type: int = GdhRead.as_int(elements[1], TYPE_NIL)
|
|
415
530
|
for key: Variant in entries:
|
|
416
531
|
if _fraction(key, key_type) or _fraction(entries[key], value_type):
|
|
417
532
|
return true
|
|
@@ -424,10 +539,43 @@ func _fraction_refusal(target: Object, property: String, raw: Variant) -> String
|
|
|
424
539
|
"%s.%s holds whole numbers, and %s would be cut to fit. Setting it would save a different "
|
|
425
540
|
+ "value than the one asked for."
|
|
426
541
|
)
|
|
427
|
-
% [target
|
|
542
|
+
% [class_called(target), property, shown(raw)]
|
|
428
543
|
)
|
|
429
544
|
|
|
430
545
|
|
|
546
|
+
## [param raw] as a colour, read as a property write reads one, as [true, Color] or [false, why]:
|
|
547
|
+
## fields from 0 to 1 with a optional, or an html code. [param named] is what the refusal calls it.
|
|
548
|
+
func colour_from(raw: Variant, named: String) -> Array:
|
|
549
|
+
_refused = ""
|
|
550
|
+
var value: Variant = parse(raw, TYPE_COLOR)
|
|
551
|
+
if not _refused.is_empty():
|
|
552
|
+
return [false, _refused]
|
|
553
|
+
if value is Color:
|
|
554
|
+
return [true, value]
|
|
555
|
+
return [
|
|
556
|
+
false,
|
|
557
|
+
(
|
|
558
|
+
(
|
|
559
|
+
'%s is {"r": .., "g": .., "b": .., "a": ..} from 0 to 1, a optional, or an html code such as '
|
|
560
|
+
+ '"#ff8000" or "#ff800080"; %s is neither.'
|
|
561
|
+
)
|
|
562
|
+
% [named, JSON.stringify(raw)]
|
|
563
|
+
)
|
|
564
|
+
]
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
## What [param target] is called in a refusal: the class_name its script declares, or else its
|
|
568
|
+
## engine class. A resource made as a project class is a Resource to the engine, and "Resource.kind"
|
|
569
|
+
## named a property no Resource has.
|
|
570
|
+
static func class_called(target: Object) -> String:
|
|
571
|
+
var attached: Variant = target.get_script()
|
|
572
|
+
if attached is Script:
|
|
573
|
+
var script: Script = attached
|
|
574
|
+
if not str(script.get_global_name()).is_empty():
|
|
575
|
+
return str(script.get_global_name())
|
|
576
|
+
return target.get_class()
|
|
577
|
+
|
|
578
|
+
|
|
431
579
|
## The resource [param path] names, as [found, resource or why not].
|
|
432
580
|
##
|
|
433
581
|
## The project boundary is enforced here as well as on the server, because only the engine knows
|
|
@@ -451,6 +599,19 @@ func _resource_at(path: String, property: String) -> Array:
|
|
|
451
599
|
)
|
|
452
600
|
]
|
|
453
601
|
if not ResourceLoader.exists(path):
|
|
602
|
+
# An image or a sound nobody has imported is a file the engine has no loader for, and "No
|
|
603
|
+
# resource at" sent a caller looking for a file that was there.
|
|
604
|
+
if path.begins_with("res://") and FileAccess.file_exists(path):
|
|
605
|
+
return [
|
|
606
|
+
false,
|
|
607
|
+
(
|
|
608
|
+
(
|
|
609
|
+
"%s is there, but it has never been imported, so the engine has no resource to load"
|
|
610
|
+
+ " from it for %s. project_import reimport with resourcePath %s imports it."
|
|
611
|
+
)
|
|
612
|
+
% [path, property, path]
|
|
613
|
+
)
|
|
614
|
+
]
|
|
454
615
|
return [false, "No resource at %s for %s" % [path, property]]
|
|
455
616
|
return [true, load(path)]
|
|
456
617
|
|
|
@@ -463,7 +624,7 @@ func declaration(target: Object, property: String) -> Dictionary:
|
|
|
463
624
|
for declared: Dictionary in target.get_property_list():
|
|
464
625
|
if str(declared.get("name", "")) != property:
|
|
465
626
|
continue
|
|
466
|
-
if
|
|
627
|
+
if GdhRead.as_int(declared.get("usage", 0)) & HEADINGS:
|
|
467
628
|
continue
|
|
468
629
|
return declared
|
|
469
630
|
if target is Theme:
|
|
@@ -503,13 +664,13 @@ static func same(held: Variant, given: Variant) -> bool:
|
|
|
503
664
|
return held_none == given_none
|
|
504
665
|
if held_type == TYPE_OBJECT or given_type == TYPE_OBJECT:
|
|
505
666
|
return _same_object(held, given)
|
|
506
|
-
if
|
|
667
|
+
if GdhSerialisation.NUMBERS.has(held_type) and GdhSerialisation.NUMBERS.has(given_type):
|
|
507
668
|
if held_type == TYPE_BOOL or given_type == TYPE_BOOL:
|
|
508
669
|
return held_type == given_type and held == given
|
|
509
|
-
var held_number: float =
|
|
510
|
-
var given_number: float =
|
|
670
|
+
var held_number: float = GdhRead.as_float(held)
|
|
671
|
+
var given_number: float = GdhRead.as_float(given)
|
|
511
672
|
return (is_nan(held_number) and is_nan(given_number)) or is_equal_approx(held_number, given_number)
|
|
512
|
-
if
|
|
673
|
+
if GdhSerialisation.TEXTS.has(held_type) and GdhSerialisation.TEXTS.has(given_type):
|
|
513
674
|
return str(held) == str(given)
|
|
514
675
|
if _listed(held_type) and _listed(given_type):
|
|
515
676
|
var held_items: Array = type_convert(held, TYPE_ARRAY)
|
|
@@ -675,7 +836,7 @@ func _parse_shaped_dictionary(value: Dictionary, expected_type: int, elements: A
|
|
|
675
836
|
if expected_type == TYPE_NIL or expected_type == TYPE_DICTIONARY:
|
|
676
837
|
return _parse_entries(value, elements)
|
|
677
838
|
var tag: String = type_string(expected_type)
|
|
678
|
-
if
|
|
839
|
+
if GdhShapes.REQUIRED.has(tag):
|
|
679
840
|
var tagged: Dictionary = value.duplicate()
|
|
680
841
|
tagged["_type"] = tag
|
|
681
842
|
var built: Variant = _values.deserialize_value(tagged)
|
|
@@ -690,8 +851,8 @@ func _parse_entries(value: Dictionary, elements: Array) -> Dictionary:
|
|
|
690
851
|
var key_type: int = TYPE_NIL
|
|
691
852
|
var value_type: int = TYPE_NIL
|
|
692
853
|
if elements.size() == 2:
|
|
693
|
-
key_type =
|
|
694
|
-
value_type =
|
|
854
|
+
key_type = GdhRead.as_int(elements[0], TYPE_NIL)
|
|
855
|
+
value_type = GdhRead.as_int(elements[1], TYPE_NIL)
|
|
695
856
|
var entries: Dictionary = {}
|
|
696
857
|
for key: Variant in value:
|
|
697
858
|
entries[_values.fitted(key, key_type)] = parse(value[key], value_type)
|
|
@@ -709,7 +870,7 @@ func _typed_dictionary(entries: Dictionary, key_type: int, value_type: int) -> D
|
|
|
709
870
|
for key: Variant in entries:
|
|
710
871
|
if key_type != TYPE_NIL and typeof(key) != key_type:
|
|
711
872
|
return entries
|
|
712
|
-
if not
|
|
873
|
+
if not GdhSerialisation.acceptable(entries[key], value_type):
|
|
713
874
|
return entries
|
|
714
875
|
var fitted: Dictionary = {}
|
|
715
876
|
for key: Variant in entries:
|
|
@@ -720,46 +881,48 @@ func _typed_dictionary(entries: Dictionary, key_type: int, value_type: int) -> D
|
|
|
720
881
|
## An array, either positional for a vector the property declares, or a list to parse per item.
|
|
721
882
|
func _parse_array(value: Array, expected_type: int, elements: Array) -> Variant:
|
|
722
883
|
if PACKED_ELEMENTS.has(expected_type):
|
|
723
|
-
var element: int =
|
|
884
|
+
var element: int = GdhRead.as_int(PACKED_ELEMENTS[expected_type], TYPE_NIL)
|
|
724
885
|
return value.map(func(item: Variant) -> Variant: return parse(item, element))
|
|
725
886
|
|
|
726
887
|
match expected_type:
|
|
727
888
|
TYPE_VECTOR2:
|
|
728
889
|
if value.size() == 2:
|
|
729
|
-
return Vector2(
|
|
890
|
+
return Vector2(GdhRead.as_float(value[0]), GdhRead.as_float(value[1]))
|
|
730
891
|
TYPE_VECTOR2I:
|
|
731
892
|
if value.size() == 2:
|
|
732
|
-
return Vector2i(
|
|
893
|
+
return Vector2i(GdhRead.as_int(value[0]), GdhRead.as_int(value[1]))
|
|
733
894
|
TYPE_VECTOR3:
|
|
734
895
|
if value.size() == 3:
|
|
735
|
-
return Vector3(
|
|
896
|
+
return Vector3(
|
|
897
|
+
GdhRead.as_float(value[0]), GdhRead.as_float(value[1]), GdhRead.as_float(value[2])
|
|
898
|
+
)
|
|
736
899
|
TYPE_VECTOR3I:
|
|
737
900
|
if value.size() == 3:
|
|
738
|
-
return Vector3i(
|
|
901
|
+
return Vector3i(GdhRead.as_int(value[0]), GdhRead.as_int(value[1]), GdhRead.as_int(value[2]))
|
|
739
902
|
TYPE_VECTOR4:
|
|
740
903
|
if value.size() == 4:
|
|
741
904
|
return Vector4(
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
905
|
+
GdhRead.as_float(value[0]),
|
|
906
|
+
GdhRead.as_float(value[1]),
|
|
907
|
+
GdhRead.as_float(value[2]),
|
|
908
|
+
GdhRead.as_float(value[3])
|
|
746
909
|
)
|
|
747
910
|
TYPE_COLOR:
|
|
748
911
|
if value.size() == 3 or value.size() == 4:
|
|
749
912
|
return Color(
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
913
|
+
GdhRead.as_float(value[0]),
|
|
914
|
+
GdhRead.as_float(value[1]),
|
|
915
|
+
GdhRead.as_float(value[2]),
|
|
916
|
+
GdhRead.as_float(value[3]) if value.size() == 4 else 1.0
|
|
754
917
|
)
|
|
755
918
|
|
|
756
919
|
var element_type: int = TYPE_NIL
|
|
757
920
|
if expected_type == TYPE_ARRAY and not elements.is_empty():
|
|
758
|
-
element_type =
|
|
921
|
+
element_type = GdhRead.as_int(elements[0], TYPE_NIL)
|
|
759
922
|
var items: Array = value.map(func(item: Variant) -> Variant: return parse(item, element_type))
|
|
760
923
|
if element_type == TYPE_NIL or element_type == TYPE_OBJECT:
|
|
761
924
|
return items
|
|
762
|
-
if not items.all(func(item: Variant) -> bool: return
|
|
925
|
+
if not items.all(func(item: Variant) -> bool: return GdhSerialisation.acceptable(item, element_type)):
|
|
763
926
|
return items
|
|
764
927
|
var fitted: Array = items.map(func(item: Variant) -> Variant: return type_convert(item, element_type))
|
|
765
928
|
return Array(fitted, element_type, &"", null)
|
|
@@ -139,3 +139,68 @@ static func missing_below(
|
|
|
139
139
|
"its children are " + shown if not children.is_empty() else "it has no children",
|
|
140
140
|
]
|
|
141
141
|
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
## [param said], the refusal for a [param kind] the scene does not have at [param node_path], with
|
|
145
|
+
## what is there when the scene has a node there, [param found] its class, and [param holders], the
|
|
146
|
+
## paths of the nodes in the scene that are one, so the caller has the path to pass. Names up to
|
|
147
|
+
## [constant NAMED_CHILDREN] of them.
|
|
148
|
+
static func not_of_kind(
|
|
149
|
+
said: String, node_path: String, found: String, kind: String, holders: PackedStringArray
|
|
150
|
+
) -> String:
|
|
151
|
+
var told: String = said + ("" if said.ends_with(".") else ".")
|
|
152
|
+
if not found.is_empty():
|
|
153
|
+
told += ' The node at "%s" is %s, not %s.' % [node_path, with_article(found), with_article(kind)]
|
|
154
|
+
if holders.is_empty():
|
|
155
|
+
return told + " The scene holds no %s." % kind
|
|
156
|
+
var quoted: PackedStringArray = []
|
|
157
|
+
for holder: String in holders.slice(0, NAMED_CHILDREN):
|
|
158
|
+
var _quoted: bool = quoted.append('"%s"' % holder)
|
|
159
|
+
var shown: String = ", ".join(quoted)
|
|
160
|
+
if holders.size() > NAMED_CHILDREN:
|
|
161
|
+
shown += " and %d more" % (holders.size() - NAMED_CHILDREN)
|
|
162
|
+
if holders.size() == 1:
|
|
163
|
+
return told + " The scene's %s is at %s." % [kind, shown]
|
|
164
|
+
return told + " The scene's %ss are at %s." % [kind, shown]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
## Up to three of [param names] closest to [param word], closest first, ignoring case: the names a
|
|
168
|
+
## refusal offers for one that is not there, as a typo usually is one of them.
|
|
169
|
+
static func nearest(word: String, names: PackedStringArray) -> PackedStringArray:
|
|
170
|
+
var wanted: String = word.to_lower()
|
|
171
|
+
var close: Array[String] = []
|
|
172
|
+
for name: String in names:
|
|
173
|
+
if name.to_lower().similarity(wanted) >= 0.5:
|
|
174
|
+
close.append(name)
|
|
175
|
+
close.sort_custom(
|
|
176
|
+
func(a: String, b: String) -> bool:
|
|
177
|
+
return a.to_lower().similarity(wanted) > b.to_lower().similarity(wanted)
|
|
178
|
+
)
|
|
179
|
+
return PackedStringArray(close.slice(0, 3))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
## [param said], the refusal for [param word], which is not among [param names], with those nearest
|
|
183
|
+
## it when any are, or else every one of them, so the caller has a name to send instead.
|
|
184
|
+
static func offered(said: String, word: String, names: Array) -> String:
|
|
185
|
+
var every: PackedStringArray = PackedStringArray(names)
|
|
186
|
+
var close: PackedStringArray = nearest(word, every)
|
|
187
|
+
if close.is_empty():
|
|
188
|
+
return "%s; it has %s" % [said, ", ".join(every)]
|
|
189
|
+
return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
## The signals [param object] has that a caller can connect, its own class's first: the engine
|
|
193
|
+
## lists them from Object down, which puts a Button's pressed after thirty a Control has, and names
|
|
194
|
+
## starting with an underscore are the engine's own.
|
|
195
|
+
static func signals_of(object: Object) -> Array:
|
|
196
|
+
var names: Array = []
|
|
197
|
+
for entry: Dictionary in object.get_signal_list():
|
|
198
|
+
var name: String = str(entry["name"])
|
|
199
|
+
if not name.begins_with("_"):
|
|
200
|
+
names.push_front(name)
|
|
201
|
+
return names
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
|
|
205
|
+
static func with_article(word: String) -> String:
|
|
206
|
+
return ("an " if word.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + word
|