gdharness 1.3.3 → 1.3.4
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 +105 -74
- package/build/godot/addons/gdharness_editor/animation_reads.gd +3 -2
- package/build/godot/addons/gdharness_editor/declared_values.gd +157 -4
- package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
- package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
- package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
- package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +195 -221
- package/build/godot/addons/gdharness_editor/reading.gd +15 -0
- package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
- package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +97 -14
- package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
- package/build/godot/addons/gdharness_editor/tool_executor.gd +9 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +38 -35
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +68 -2
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +94 -333
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +10 -26
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +30 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
- package/build/godot/operations/animation_reads.gd +3 -2
- package/build/godot/operations/declared_values.gd +157 -4
- package/build/godot/operations/dependencies.gd +14 -39
- package/build/godot/operations/editor_tools.gd +4 -3
- package/build/godot/operations/file_reads.gd +15 -3
- package/build/godot/operations/gdscript_analysis.gd +229 -75
- package/build/godot/operations/gdscript_authoring.gd +142 -34
- package/build/godot/operations/import_pipeline.gd +17 -6
- package/build/godot/operations/logger.gd +7 -0
- package/build/godot/operations/property_paths.gd +225 -0
- package/build/godot/operations/property_values.gd +195 -221
- package/build/godot/operations/reading.gd +15 -0
- package/build/godot/operations/same_values.gd +177 -0
- package/build/godot/operations/serialisation.gd +75 -3
- package/package.json +1 -1
|
@@ -164,8 +164,9 @@ static func state_machine(tree: AnimationTree, state_machine_path: String) -> An
|
|
|
164
164
|
static func no_machine(tree: AnimationTree, state_machine_path: String) -> String:
|
|
165
165
|
if tree == null or tree.tree_root == null:
|
|
166
166
|
return (
|
|
167
|
-
"The AnimationTree has no tree_root.
|
|
168
|
-
+ "AnimationNodeStateMachine
|
|
167
|
+
"The AnimationTree has no tree_root. Give it one with scene_node set: tree_root "
|
|
168
|
+
+ '{"_type": "AnimationNodeStateMachine"} builds one into the scene, and the res:// path '
|
|
169
|
+
+ "of one made with resource_edit create keeps it in a file of its own."
|
|
169
170
|
)
|
|
170
171
|
var current: AnimationNodeStateMachine = tree.tree_root as AnimationNodeStateMachine
|
|
171
172
|
if current == null:
|
|
@@ -58,12 +58,46 @@ static func enum_named(choices: Dictionary, word: String) -> Variant:
|
|
|
58
58
|
return null
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
## The values of the engine enum [param property] of [param target] holds, by the constants its
|
|
62
|
+
## class declares them as: blend_mode's Add is BLEND_MODE_ADD in a script, and the hint gives only
|
|
63
|
+
## the inspector's word. Empty for an enum a script declares.
|
|
64
|
+
##
|
|
65
|
+
## The enum is named by the setter's argument, as Node.ProcessMode, since the property list gives
|
|
66
|
+
## an engine enum property no class of its own.
|
|
67
|
+
static func engine_constants(target: Object, property: String) -> Dictionary:
|
|
68
|
+
var values: Dictionary = {}
|
|
69
|
+
var engine_class: String = target.get_class()
|
|
70
|
+
var setter: String = str(ClassDB.class_get_property_setter(engine_class, property))
|
|
71
|
+
if setter.is_empty():
|
|
72
|
+
return values
|
|
73
|
+
var named: String = ""
|
|
74
|
+
for method: Dictionary in ClassDB.class_get_method_list(engine_class):
|
|
75
|
+
var arguments: Array = method.get("args", [])
|
|
76
|
+
if str(method.get("name", "")) == setter and not arguments.is_empty():
|
|
77
|
+
var value_argument: Dictionary = arguments[-1]
|
|
78
|
+
named = str(value_argument.get("class_name", ""))
|
|
79
|
+
break
|
|
80
|
+
var holder: String = named.get_slice(".", 0)
|
|
81
|
+
if named.count(".") != 1 or not ClassDB.class_exists(holder):
|
|
82
|
+
return values
|
|
83
|
+
for constant: String in ClassDB.class_get_enum_constants(holder, named.get_slice(".", 1)):
|
|
84
|
+
values[constant] = ClassDB.class_get_integer_constant(holder, constant)
|
|
85
|
+
return values
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
static func enum_refusal(
|
|
89
|
+
holder: String, property: String, word: String, choices: Dictionary, constants: Dictionary = {}
|
|
90
|
+
) -> String:
|
|
91
|
+
var ways: String = "by name or by number"
|
|
92
|
+
if not constants.is_empty():
|
|
93
|
+
ways = "by name, by number or by the engine's constant, such as %s" % str(constants.keys()[0])
|
|
62
94
|
var said: String = (
|
|
63
|
-
"%s.%s is an enum, and %s is none of its values: send %s
|
|
64
|
-
% [holder, property, word, _listed(choices)]
|
|
95
|
+
"%s.%s is an enum, and %s is none of its values: send %s %s"
|
|
96
|
+
% [holder, property, word, _listed(choices), ways]
|
|
65
97
|
)
|
|
66
|
-
var
|
|
98
|
+
var named: PackedStringArray = PackedStringArray(choices.keys())
|
|
99
|
+
named.append_array(PackedStringArray(constants.keys()))
|
|
100
|
+
var close: PackedStringArray = GdhRead.nearest(word, named)
|
|
67
101
|
if close.is_empty():
|
|
68
102
|
return said + "."
|
|
69
103
|
return said + ". The nearest %s %s." % ["is" if close.size() == 1 else "are", ", ".join(close)]
|
|
@@ -76,6 +110,17 @@ static func outside(target: Object, declared: Dictionary, value: Variant, shown:
|
|
|
76
110
|
var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
77
111
|
var hint_string: String = str(declared.get("hint_string", ""))
|
|
78
112
|
var property: String = str(declared.get("name", ""))
|
|
113
|
+
if hint == PROPERTY_HINT_ENUM and (typeof(value) == TYPE_STRING or typeof(value) == TYPE_STRING_NAME):
|
|
114
|
+
var names: PackedStringArray = PackedStringArray(enum_values(hint_string).keys())
|
|
115
|
+
if names.is_empty() or names.has(str(value)):
|
|
116
|
+
return ""
|
|
117
|
+
return (
|
|
118
|
+
(
|
|
119
|
+
"%s is none of the names %s lists: %s, which the inspector holds it to; the engine "
|
|
120
|
+
+ "stores it as given."
|
|
121
|
+
)
|
|
122
|
+
% [shown, property, ", ".join(names)]
|
|
123
|
+
)
|
|
79
124
|
if typeof(value) != TYPE_INT and typeof(value) != TYPE_FLOAT:
|
|
80
125
|
return ""
|
|
81
126
|
var number: float = GdhRead.as_float(value)
|
|
@@ -114,3 +159,111 @@ static func _listed(choices: Dictionary) -> String:
|
|
|
114
159
|
## A range's end as the script wrote it: the engine's hint gives @export_range(0, 10) as 0.0 and 10.0.
|
|
115
160
|
static func _bound(end: float) -> String:
|
|
116
161
|
return str(int(end)) if end == floorf(end) and absf(end) < 1e15 else str(end)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
## Puts back on [param built] what its script set as it was attached and was not among
|
|
165
|
+
## [param wanted], to the defaults it declares, outside the editor. Attaching runs the script's _init
|
|
166
|
+
## there, which the editor never runs for a script that is not a tool, so a resource_name _init chose
|
|
167
|
+
## was saved as if asked for. A property whose default the engine cannot report is left as made.
|
|
168
|
+
static func unmade(built: Resource, wanted: Dictionary) -> void:
|
|
169
|
+
var attached: Variant = built.get_script()
|
|
170
|
+
if Engine.is_editor_hint() or not attached is Script:
|
|
171
|
+
return
|
|
172
|
+
var script: Script = attached
|
|
173
|
+
var asked: Dictionary = {}
|
|
174
|
+
for key: Variant in wanted:
|
|
175
|
+
asked[str(key).get_slice(":", 0)] = true
|
|
176
|
+
for entry: Dictionary in built.get_property_list():
|
|
177
|
+
var name: String = str(entry["name"])
|
|
178
|
+
var usage: int = GdhRead.as_int(entry["usage"])
|
|
179
|
+
if usage & PROPERTY_USAGE_STORAGE == 0 or name == "script" or asked.has(name):
|
|
180
|
+
continue
|
|
181
|
+
var declared: Variant = ClassDB.class_get_property_default_value(built.get_class(), name)
|
|
182
|
+
if usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
|
|
183
|
+
declared = script.get_property_default_value(name)
|
|
184
|
+
var held: Variant = built.get(name)
|
|
185
|
+
if declared != null and not (typeof(held) == typeof(declared) and held == declared):
|
|
186
|
+
built.set(name, declared)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
## The element types a typed Array or Dictionary property declares: [element] for an array,
|
|
190
|
+
## [key, value] for a dictionary, [] when nothing is declared.
|
|
191
|
+
##
|
|
192
|
+
## Read from the declaration because the value cannot say: a script's exported `Array[int]` sits
|
|
193
|
+
## behind a placeholder in the editor, and measured on 4.7.2 that placeholder stores [1.5, 2.0]
|
|
194
|
+
## as given, which the game then fails to load into the typed array.
|
|
195
|
+
static func elements_of(declared: Dictionary) -> Array:
|
|
196
|
+
var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
|
|
197
|
+
var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
198
|
+
var wanted: String = str(declared.get("hint_string", ""))
|
|
199
|
+
if wanted.is_empty():
|
|
200
|
+
return []
|
|
201
|
+
if hint == PROPERTY_HINT_TYPE_STRING:
|
|
202
|
+
var parts: PackedStringArray = wanted.split(";")
|
|
203
|
+
if type == TYPE_ARRAY and parts.size() == 1:
|
|
204
|
+
return [_type_in_hint(parts[0])]
|
|
205
|
+
if type == TYPE_DICTIONARY and parts.size() == 2:
|
|
206
|
+
return [_type_in_hint(parts[0]), _type_in_hint(parts[1])]
|
|
207
|
+
return []
|
|
208
|
+
if hint == PROPERTY_HINT_ARRAY_TYPE and type == TYPE_ARRAY:
|
|
209
|
+
return [_type_named(wanted)]
|
|
210
|
+
if hint == PROPERTY_HINT_DICTIONARY_TYPE and type == TYPE_DICTIONARY:
|
|
211
|
+
var parts: PackedStringArray = wanted.split(";")
|
|
212
|
+
if parts.size() == 2:
|
|
213
|
+
return [_type_named(parts[0]), _type_named(parts[1])]
|
|
214
|
+
return []
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
## The type at the front of one entry of a type-string hint, "2:" or "24/17:Texture2D".
|
|
218
|
+
static func _type_in_hint(entry: String) -> int:
|
|
219
|
+
var number: String = entry.get_slice(":", 0).get_slice("/", 0)
|
|
220
|
+
return number.to_int() if number.is_valid_int() else TYPE_NIL
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
## The Variant type [param name] names, or TYPE_OBJECT for a class.
|
|
224
|
+
static func _type_named(name: String) -> int:
|
|
225
|
+
var named: String = name.get_slice(":", 0).strip_edges()
|
|
226
|
+
for type: int in TYPE_MAX:
|
|
227
|
+
if type_string(type) == named:
|
|
228
|
+
return type
|
|
229
|
+
return TYPE_OBJECT if not named.is_empty() else TYPE_NIL
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
## The class the objects of a typed list or map hold, and whether they are nodes, as
|
|
233
|
+
## [class, nodes], read from entry [param at] of [param declared]'s hint: "24/17:Item" in a type
|
|
234
|
+
## string, "Item" in an array or dictionary type.
|
|
235
|
+
static func object_kind(declared: Dictionary, at: int) -> Array:
|
|
236
|
+
var entry: String = str(declared.get("hint_string", "")).get_slice(";", at)
|
|
237
|
+
if GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE)) != PROPERTY_HINT_TYPE_STRING:
|
|
238
|
+
return [entry.strip_edges(), false]
|
|
239
|
+
var nodes: bool = entry.get_slice(":", 0).get_slice("/", 1) == str(PROPERTY_HINT_NODE_TYPE)
|
|
240
|
+
return [entry.get_slice(":", 1), nodes]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
## The base class and script that type a list or map holding [param wanted], as [class, script],
|
|
244
|
+
## or [] when nothing in the project or the engine is called that.
|
|
245
|
+
static func object_typing(wanted: String) -> Array:
|
|
246
|
+
if ClassDB.class_exists(wanted):
|
|
247
|
+
return [StringName(wanted), null]
|
|
248
|
+
var script: Script = script_class(wanted)
|
|
249
|
+
return [] if script == null else [script.get_instance_base_type(), script]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
## The script declaring the class_name [param name], or null.
|
|
253
|
+
static func script_class(name: String) -> Script:
|
|
254
|
+
for entry: Dictionary in ProjectSettings.get_global_class_list():
|
|
255
|
+
if str(entry.get("class", "")) == name:
|
|
256
|
+
return load(str(entry.get("path", ""))) as Script
|
|
257
|
+
return null
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
## Whether [param value] is the class [param name] names, an engine class or a script's class_name.
|
|
261
|
+
static func is_a(value: Object, name: String) -> bool:
|
|
262
|
+
if value.is_class(name):
|
|
263
|
+
return true
|
|
264
|
+
var script: Script = value.get_script()
|
|
265
|
+
while script != null:
|
|
266
|
+
if str(script.get_global_name()) == name:
|
|
267
|
+
return true
|
|
268
|
+
script = script.get_base_script()
|
|
269
|
+
return false
|
|
@@ -58,47 +58,21 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
58
58
|
var max_depth: int = depth if depth > 0 else 1000
|
|
59
59
|
var include_built_in: bool = GdhRead.as_bool(params.get("include_builtin", false))
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
(
|
|
63
|
-
|
|
64
|
-
+ (" for: " + resource_path if not resource_path.is_empty() else " for all resources")
|
|
65
|
-
)
|
|
66
|
-
)
|
|
61
|
+
if resource_path.is_empty():
|
|
62
|
+
return _log.failure("resource_path is required")
|
|
63
|
+
_log.info("Getting dependencies for: " + resource_path)
|
|
67
64
|
|
|
68
65
|
var dependencies: Dictionary = {}
|
|
69
66
|
var circular_references: Array = []
|
|
70
|
-
var
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
_files.with_nearest_files("Resource file does not exist: " + full_path, full_path)
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
var walk: GdhDependencyWalk = GdhDependencyWalk.new(max_depth, include_built_in, circular_references)
|
|
83
|
-
dependencies[full_path] = _analyze_resource(full_path, 0, walk)
|
|
84
|
-
total_resources = 1
|
|
85
|
-
else:
|
|
86
|
-
var resource_extensions: Array[String] = ["tscn", "tres", "gd", "gdshader", "shader"]
|
|
87
|
-
var all_resources: Array[String] = []
|
|
88
|
-
for ext: String in resource_extensions:
|
|
89
|
-
all_resources.append_array(_files.find_files("res://", "." + ext))
|
|
90
|
-
|
|
91
|
-
# Each root gets a fresh walk so a cycle is reported from every resource it passes
|
|
92
|
-
# through, rather than only from whichever one happened to be walked first.
|
|
93
|
-
for res_path: String in all_resources:
|
|
94
|
-
var walk: GdhDependencyWalk = GdhDependencyWalk.new(
|
|
95
|
-
max_depth, include_built_in, circular_references
|
|
96
|
-
)
|
|
97
|
-
var deps: Array[Dictionary] = _analyze_resource(res_path, 0, walk)
|
|
98
|
-
if deps.size() > 0:
|
|
99
|
-
dependencies[res_path] = deps
|
|
100
|
-
|
|
101
|
-
total_resources = all_resources.size()
|
|
67
|
+
var full_path: String = resource_path
|
|
68
|
+
if not full_path.begins_with("res://"):
|
|
69
|
+
full_path = "res://" + full_path
|
|
70
|
+
if not FileAccess.file_exists(full_path):
|
|
71
|
+
return _log.failure(
|
|
72
|
+
_files.with_nearest_files("Resource file does not exist: " + full_path, full_path)
|
|
73
|
+
)
|
|
74
|
+
var walk: GdhDependencyWalk = GdhDependencyWalk.new(max_depth, include_built_in, circular_references)
|
|
75
|
+
dependencies[full_path] = _analyze_resource(full_path, 0, walk)
|
|
102
76
|
|
|
103
77
|
var references: int = 0
|
|
104
78
|
var distinct: Dictionary = {}
|
|
@@ -121,7 +95,8 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
121
95
|
"circular_references": circular_references,
|
|
122
96
|
"summary":
|
|
123
97
|
{
|
|
124
|
-
|
|
98
|
+
# The file the walk starts from, which a call always names one of.
|
|
99
|
+
"total_resources": 1,
|
|
125
100
|
# The files depended on, each once. The tree names a file again under everything that
|
|
126
101
|
# loads it, and counting its entries answered 11 for a scene with six dependencies.
|
|
127
102
|
"total_dependencies": distinct.size(),
|
|
@@ -10,6 +10,7 @@ extends RefCounted
|
|
|
10
10
|
const GdhAnimationTools = preload("../addons/gdharness_editor/tools/animation_tools.gd")
|
|
11
11
|
const GdhResourceTools = preload("../addons/gdharness_editor/tools/resource_tools.gd")
|
|
12
12
|
const GdhSceneTools = preload("../addons/gdharness_editor/tools/scene_tools.gd")
|
|
13
|
+
const GdhSignalTools = preload("../addons/gdharness_editor/tools/signal_tools.gd")
|
|
13
14
|
const GdhSceneFile = preload("../addons/gdharness_editor/scene_file.gd")
|
|
14
15
|
|
|
15
16
|
const SCENE_COMMANDS: Array[String] = [
|
|
@@ -21,10 +22,8 @@ const SCENE_COMMANDS: Array[String] = [
|
|
|
21
22
|
"duplicate_node",
|
|
22
23
|
"reparent_node",
|
|
23
24
|
"delete_node",
|
|
24
|
-
"connect_signal",
|
|
25
|
-
"disconnect_signal",
|
|
26
|
-
"list_connections",
|
|
27
25
|
]
|
|
26
|
+
const SIGNAL_COMMANDS: Array[String] = ["connect_signal", "disconnect_signal", "list_connections"]
|
|
28
27
|
const RESOURCE_COMMANDS: Array[String] = [
|
|
29
28
|
"set_tilemap_cells",
|
|
30
29
|
"create_resource",
|
|
@@ -55,6 +54,8 @@ static func run(params: Dictionary) -> Dictionary:
|
|
|
55
54
|
var tools: Node = null
|
|
56
55
|
if command in SCENE_COMMANDS:
|
|
57
56
|
tools = GdhSceneTools.new()
|
|
57
|
+
elif command in SIGNAL_COMMANDS:
|
|
58
|
+
tools = GdhSignalTools.new()
|
|
58
59
|
elif command in RESOURCE_COMMANDS:
|
|
59
60
|
tools = GdhResourceTools.new()
|
|
60
61
|
elif command in ANIMATION_COMMANDS:
|
|
@@ -10,8 +10,8 @@ extends RefCounted
|
|
|
10
10
|
## without making a node. Loading the file still does what loading does in the game: static
|
|
11
11
|
## initialisers run, and a resource with a script is made with it.
|
|
12
12
|
|
|
13
|
-
const GdhPropertyValues = preload("property_values.gd")
|
|
14
13
|
const GdhRead = preload("reading.gd")
|
|
14
|
+
const GdhSame = preload("same_values.gd")
|
|
15
15
|
const GdhSerialisation = preload("serialisation.gd")
|
|
16
16
|
|
|
17
17
|
|
|
@@ -188,6 +188,12 @@ static func _lay(
|
|
|
188
188
|
path,
|
|
189
189
|
state.get_node_index(index)
|
|
190
190
|
)
|
|
191
|
+
# Every file on the way puts the node in its own groups: the scene making it, and each scene
|
|
192
|
+
# instancing or inheriting that one.
|
|
193
|
+
var grouped: Array[String] = nodes[path]["groups"]
|
|
194
|
+
for group: StringName in state.get_node_groups(index):
|
|
195
|
+
if not grouped.has(str(group)):
|
|
196
|
+
grouped.append(str(group))
|
|
191
197
|
var held: Dictionary = nodes[path]["properties"]
|
|
192
198
|
for property: int in state.get_node_property_count(index):
|
|
193
199
|
held[str(state.get_node_property_name(index, property))] = state.get_node_property_value(
|
|
@@ -281,7 +287,8 @@ static func _by_id(nodes: Dictionary, owner: String, id: int) -> String:
|
|
|
281
287
|
|
|
282
288
|
static func _made(type: String) -> Dictionary:
|
|
283
289
|
var children: Array[String] = []
|
|
284
|
-
|
|
290
|
+
var groups: Array[String] = []
|
|
291
|
+
return {"name": "", "type": type, "properties": {}, "children": children, "groups": groups}
|
|
285
292
|
|
|
286
293
|
|
|
287
294
|
## Puts [param path] among [param parent]'s children, at [param at] when the file says where (a node
|
|
@@ -317,6 +324,11 @@ static func _entry(
|
|
|
317
324
|
data["instanceOf"] = node["instanceOf"]
|
|
318
325
|
if node.has("editableChildren"):
|
|
319
326
|
data["editableChildren"] = node["editableChildren"]
|
|
327
|
+
var groups: Array[String] = node["groups"]
|
|
328
|
+
if not groups.is_empty():
|
|
329
|
+
var sorted: Array[String] = groups.duplicate()
|
|
330
|
+
sorted.sort()
|
|
331
|
+
data["groups"] = sorted
|
|
320
332
|
if include_properties:
|
|
321
333
|
var given: Dictionary = node["properties"]
|
|
322
334
|
data["properties"] = _stored(str(node["type"]), given)
|
|
@@ -419,7 +431,7 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
419
431
|
if scripted.has(property)
|
|
420
432
|
else ClassDB.class_get_property_default_value(loaded.get_class(), property)
|
|
421
433
|
)
|
|
422
|
-
if
|
|
434
|
+
if GdhSame.same(value, default):
|
|
423
435
|
continue
|
|
424
436
|
properties[property] = serialiser.serialize_value(value)
|
|
425
437
|
var answer: Dictionary = {
|