gdharness 1.3.3 → 1.3.5

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 (52) hide show
  1. package/build/cli.js +532 -456
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +3 -2
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +181 -4
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
  7. package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
  8. package/build/godot/addons/gdharness_editor/property_values.gd +192 -231
  9. package/build/godot/addons/gdharness_editor/reading.gd +15 -0
  10. package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
  11. package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/scene_file.gd +97 -14
  13. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
  14. package/build/godot/addons/gdharness_editor/tool_executor.gd +9 -3
  15. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +38 -35
  16. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +20 -12
  17. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
  18. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
  19. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
  20. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +68 -2
  21. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +94 -333
  22. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
  23. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_runtime/declared_values.gd +293 -0
  25. package/build/godot/addons/gdharness_runtime/declared_values.gd.uid +1 -0
  26. package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
  27. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +4 -0
  28. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +29 -1
  29. package/build/godot/addons/gdharness_runtime/runtime_input.gd +26 -39
  30. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +18 -0
  31. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
  32. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
  33. package/build/godot/addons/gdharness_runtime/runtime_values.gd +62 -5
  34. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +48 -15
  35. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
  36. package/build/godot/operations/animation_reads.gd +3 -2
  37. package/build/godot/operations/classdb_queries.gd +112 -46
  38. package/build/godot/operations/declared_values.gd +181 -4
  39. package/build/godot/operations/dependencies.gd +14 -39
  40. package/build/godot/operations/editor_tools.gd +4 -3
  41. package/build/godot/operations/file_reads.gd +15 -3
  42. package/build/godot/operations/gdscript_analysis.gd +229 -75
  43. package/build/godot/operations/gdscript_authoring.gd +157 -36
  44. package/build/godot/operations/import_pipeline.gd +17 -6
  45. package/build/godot/operations/logger.gd +7 -0
  46. package/build/godot/operations/property_paths.gd +225 -0
  47. package/build/godot/operations/property_values.gd +192 -231
  48. package/build/godot/operations/reading.gd +15 -0
  49. package/build/godot/operations/same_values.gd +177 -0
  50. package/build/godot/operations/serialisation.gd +75 -3
  51. package/build/keeper.js +83 -69
  52. 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. Make one with resource_edit create of an "
168
- + "AnimationNodeStateMachine, then give its res:// path to tree_root with scene_node set."
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:
@@ -4,6 +4,9 @@ const GdhRead = preload("reading.gd")
4
4
  const GdhLog = preload("logger.gd")
5
5
  const GdhSerialisation = preload("serialisation.gd")
6
6
 
7
+ # How far below the closest name another may score and still be offered beside it.
8
+ const CLOSE_TO_THE_BEST: float = 0.2
9
+
7
10
  # The base classes each category name stands for. Kept to the list the tool's schema offers, which
8
11
  # a case holds: the schema offered physics3d and ui, which this refused, and physics answered the 3D
9
12
  # bodies alone beside physics2d and physics3d.
@@ -114,20 +117,20 @@ func query_class_info(params: Dictionary) -> Dictionary:
114
117
  )
115
118
 
116
119
  if not ClassDB.class_exists(class_name_str):
117
- return _not_found(class_name_str)
120
+ return _not_found(class_name_str, _declared_classes(params))
118
121
 
119
122
  var member: String = str(params.get("member", ""))
120
123
  if not member.is_empty():
121
124
  return _member_of(class_name_str, member)
122
125
 
123
- var methods: Array[Dictionary] = _methods_of(class_name_str, include_inherited)
124
- var properties: Array[Dictionary] = _properties_of(class_name_str, include_inherited)
125
- var signals: Array[Dictionary] = _signals_of(class_name_str, include_inherited)
126
+ var methods: Array[Dictionary] = _methods_of(class_name_str)
127
+ var properties: Array[Dictionary] = _properties_of(class_name_str)
128
+ var signals: Array[Dictionary] = _signals_of(class_name_str)
126
129
 
127
130
  var enums: Dictionary = {}
128
- for e: String in ClassDB.class_get_enum_list(class_name_str, not include_inherited):
131
+ for e: String in ClassDB.class_get_enum_list(class_name_str, true):
129
132
  var enum_values: Dictionary = {}
130
- for c: String in ClassDB.class_get_enum_constants(class_name_str, e, not include_inherited):
133
+ for c: String in ClassDB.class_get_enum_constants(class_name_str, e, true):
131
134
  enum_values[c] = ClassDB.class_get_integer_constant(class_name_str, c)
132
135
  enums[e] = enum_values
133
136
 
@@ -143,7 +146,7 @@ func query_class_info(params: Dictionary) -> Dictionary:
143
146
  )
144
147
  )
145
148
 
146
- return {
149
+ var answer: Dictionary = {
147
150
  "class_name": class_name_str,
148
151
  "parent_class": ClassDB.get_parent_class(class_name_str),
149
152
  "can_instantiate": ClassDB.can_instantiate(class_name_str),
@@ -156,43 +159,99 @@ func query_class_info(params: Dictionary) -> Dictionary:
156
159
  "signals": signals,
157
160
  "enums": enums
158
161
  }
162
+ if include_inherited:
163
+ answer["inherited"] = _inherited_names(class_name_str)
164
+ return answer
165
+
166
+
167
+ # What [param class_name_str] inherits, by name under the ancestor declaring it, nearest first. In
168
+ # full, CharacterBody2D's inherited members ran to two hundred thousand characters, more than a
169
+ # client takes in one answer, and any one of them is read in full by asking for it as member.
170
+ func _inherited_names(class_name_str: String) -> Array[Dictionary]:
171
+ var inherited: Array[Dictionary] = []
172
+ var current: String = ClassDB.get_parent_class(class_name_str)
173
+ while not current.is_empty():
174
+ (
175
+ inherited
176
+ . append(
177
+ {
178
+ "class": current,
179
+ "methods": _names(_methods_of(current)),
180
+ "properties": _names(_properties_of(current)),
181
+ "signals": _names(_signals_of(current)),
182
+ "enums": Array(ClassDB.class_get_enum_list(current, true)),
183
+ }
184
+ )
185
+ )
186
+ current = ClassDB.get_parent_class(current)
187
+ return inherited
188
+
189
+
190
+ static func _names(members: Array[Dictionary]) -> Array[String]:
191
+ var names: Array[String] = []
192
+ for each: Dictionary in members:
193
+ names.append(str(each["name"]))
194
+ return names
195
+
196
+
197
+ static func _declared_classes(params: Dictionary) -> Dictionary:
198
+ var given: Variant = params.get("project_classes", {})
199
+ if given is Dictionary:
200
+ var declared: Dictionary = given
201
+ return declared
202
+ return {}
159
203
 
160
204
 
161
- # One member of a class by name, wherever in its ancestry it is declared.
162
- #
163
- # The question a member is asked by name for is "does this engine have it, and what does it take":
164
- # the whole class in answer to that ran to seventy thousand characters for Control, and the member
165
- # is as often an ancestor's as the class's own, so the ancestry is walked without being asked. Each
166
- # match says which class declares it. Nothing found names the members whose names contain the one
167
- # asked for, since the usual miss is a spelling.
168
205
  ## Why [param wanted] has no answer here. A class_name a project script declares is not in ClassDB,
169
- ## and "Class not found" for one read as though the project had no such class.
170
- func _not_found(wanted: String) -> Dictionary:
171
- for entry: Dictionary in ProjectSettings.get_global_class_list():
172
- if str(entry.get("class", "")) == wanted:
173
- return (
174
- _log
175
- . failure(
176
- (
177
- (
178
- "%s is not an engine class: it is the class_name %s declares, and editor_classes reads "
179
- + "the engine's ClassDB only. script_info structure on that script, with "
180
- + "includeInherited, answers what it declares and what it inherits."
181
- )
182
- % [wanted, str(entry.get("path", ""))]
183
- )
206
+ ## and "Class not found" for one read as though the project had no such class. [param declared] is
207
+ ## what the project's scripts declare on disk, class to path, rather than the engine's list, which is
208
+ ## the class cache: that lags the scripts, and a class written a moment ago was refused as declared
209
+ ## nowhere.
210
+ func _not_found(wanted: String, declared: Dictionary) -> Dictionary:
211
+ if declared.has(wanted):
212
+ return _log.failure(
213
+ (
214
+ (
215
+ "%s is not an engine class: it is the class_name %s declares, and editor_classes reads "
216
+ + "the engine's ClassDB only. script_info structure on that script, with "
217
+ + "includeInherited, answers what it declares and what it inherits."
184
218
  )
219
+ % [wanted, str(declared[wanted])]
185
220
  )
186
- # The usual miss is a spelling, one letter off a class the engine has.
187
- var close: PackedStringArray = GdhRead.nearest(wanted, ClassDB.get_class_list())
221
+ )
222
+ # The usual miss is a spelling, one letter off a class the engine has or one the project declares.
223
+ # Ranked as one pool and kept near the best, since a project class a letter off came behind three
224
+ # engine classes that shared only "Texture" with it.
225
+ var pool: PackedStringArray = ClassDB.get_class_list()
226
+ pool.append_array(PackedStringArray(declared.keys()))
227
+ var close: PackedStringArray = GdhRead.nearest(wanted, pool)
228
+ var best: float = 0.0 if close.is_empty() else close[0].to_lower().similarity(wanted.to_lower())
229
+ var engine: Array[String] = []
230
+ var own: Array[String] = []
231
+ for name: String in close:
232
+ if name.to_lower().similarity(wanted.to_lower()) < best - CLOSE_TO_THE_BEST:
233
+ continue
234
+ if declared.has(name):
235
+ own.append(name)
236
+ else:
237
+ engine.append(name)
188
238
  var offered: String = (
189
239
  ""
190
- if close.is_empty()
240
+ if engine.is_empty()
191
241
  else (
192
242
  " The nearest engine %s %s."
193
- % ["class is" if close.size() == 1 else "classes are", ", ".join(close)]
243
+ % ["class is" if engine.size() == 1 else "classes are", ", ".join(engine)]
194
244
  )
195
245
  )
246
+ if not own.is_empty():
247
+ offered += (
248
+ " The nearest %s a project script declares %s %s, which script_info structure reads."
249
+ % [
250
+ "class" if own.size() == 1 else "classes",
251
+ "is" if own.size() == 1 else "are",
252
+ ", ".join(own),
253
+ ]
254
+ )
196
255
  return _log.failure(
197
256
  (
198
257
  (
@@ -204,17 +263,24 @@ func _not_found(wanted: String) -> Dictionary:
204
263
  )
205
264
 
206
265
 
266
+ # One member of a class by name, wherever in its ancestry it is declared.
267
+ #
268
+ # The question a member is asked by name for is "does this engine have it, and what does it take":
269
+ # the whole class in answer to that ran to seventy thousand characters for Control, and the member
270
+ # is as often an ancestor's as the class's own, so the ancestry is walked without being asked. Each
271
+ # match says which class declares it. Nothing found names the members whose names contain the one
272
+ # asked for, since the usual miss is a spelling.
207
273
  func _member_of(class_name_str: String, member: String) -> Dictionary:
208
274
  var found: Array[Dictionary] = []
209
275
  var current: String = class_name_str
210
276
  while not current.is_empty():
211
- for m: Dictionary in _methods_of(current, false):
277
+ for m: Dictionary in _methods_of(current):
212
278
  if m["name"] == member:
213
279
  found.append({"kind": "method", "declared_in": current, "member": m})
214
- for p: Dictionary in _properties_of(current, false):
280
+ for p: Dictionary in _properties_of(current):
215
281
  if p["name"] == member:
216
282
  found.append({"kind": "property", "declared_in": current, "member": p})
217
- for s: Dictionary in _signals_of(current, false):
283
+ for s: Dictionary in _signals_of(current):
218
284
  if s["name"] == member:
219
285
  found.append({"kind": "signal", "declared_in": current, "member": s})
220
286
  for e: String in ClassDB.class_get_enum_list(current, true):
@@ -251,15 +317,15 @@ func _members_alike(class_name_str: String, member: String) -> Array[String]:
251
317
  var wanted: String = member.to_lower()
252
318
  var current: String = class_name_str
253
319
  while not current.is_empty():
254
- for m: Dictionary in _methods_of(current, false):
320
+ for m: Dictionary in _methods_of(current):
255
321
  var name: String = m["name"]
256
322
  if name.to_lower().contains(wanted) and not alike.has(name):
257
323
  alike.append(name)
258
- for p: Dictionary in _properties_of(current, false):
324
+ for p: Dictionary in _properties_of(current):
259
325
  var name: String = p["name"]
260
326
  if name.to_lower().contains(wanted) and not alike.has(name):
261
327
  alike.append(name)
262
- for s: Dictionary in _signals_of(current, false):
328
+ for s: Dictionary in _signals_of(current):
263
329
  var name: String = s["name"]
264
330
  if name.to_lower().contains(wanted) and not alike.has(name):
265
331
  alike.append(name)
@@ -275,7 +341,7 @@ func inspect_inheritance(params: Dictionary) -> Dictionary:
275
341
  _log.info("Inspecting inheritance for: " + class_name_str)
276
342
 
277
343
  if not ClassDB.class_exists(class_name_str):
278
- return _not_found(class_name_str)
344
+ return _not_found(class_name_str, _declared_classes(params))
279
345
 
280
346
  var ancestors: Array[String] = []
281
347
  var current: String = class_name_str
@@ -324,10 +390,10 @@ func inspect_inheritance(params: Dictionary) -> Dictionary:
324
390
  }
325
391
 
326
392
 
327
- func _methods_of(class_name_str: String, include_inherited: bool) -> Array[Dictionary]:
393
+ func _methods_of(class_name_str: String) -> Array[Dictionary]:
328
394
  var methods: Array[Dictionary] = []
329
395
 
330
- for m: Dictionary in ClassDB.class_get_method_list(class_name_str, not include_inherited):
396
+ for m: Dictionary in ClassDB.class_get_method_list(class_name_str, true):
331
397
  var args: Array[Dictionary] = []
332
398
  var declared_args: Array = m.get("args", [])
333
399
  for a: Dictionary in declared_args:
@@ -389,10 +455,10 @@ static func _type_name(info: Dictionary, nil_name: String) -> String:
389
455
  return type_string(type)
390
456
 
391
457
 
392
- func _properties_of(class_name_str: String, include_inherited: bool) -> Array[Dictionary]:
458
+ func _properties_of(class_name_str: String) -> Array[Dictionary]:
393
459
  var properties: Array[Dictionary] = []
394
460
 
395
- for p: Dictionary in ClassDB.class_get_property_list(class_name_str, not include_inherited):
461
+ for p: Dictionary in ClassDB.class_get_property_list(class_name_str, true):
396
462
  # A category, group or subgroup is an editor heading rather than a property.
397
463
  var usage: int = GdhRead.as_int(p.get("usage", 0))
398
464
  if usage & PROPERTY_USAGE_CATEGORY or usage & PROPERTY_USAGE_GROUP or usage & PROPERTY_USAGE_SUBGROUP:
@@ -412,10 +478,10 @@ func _properties_of(class_name_str: String, include_inherited: bool) -> Array[Di
412
478
  return properties
413
479
 
414
480
 
415
- func _signals_of(class_name_str: String, include_inherited: bool) -> Array[Dictionary]:
481
+ func _signals_of(class_name_str: String) -> Array[Dictionary]:
416
482
  var signals: Array[Dictionary] = []
417
483
 
418
- for s: Dictionary in ClassDB.class_get_signal_list(class_name_str, not include_inherited):
484
+ for s: Dictionary in ClassDB.class_get_signal_list(class_name_str, true):
419
485
  var sig_args: Array[Dictionary] = []
420
486
  var declared_args: Array = s.get("args", [])
421
487
  for a: Dictionary in declared_args:
@@ -58,12 +58,70 @@ static func enum_named(choices: Dictionary, word: String) -> Variant:
58
58
  return null
59
59
 
60
60
 
61
- static func enum_refusal(holder: String, property: String, word: String, choices: Dictionary) -> String:
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
+ ## [param word] as the number of the value the enum property [param declared] of [param target]
89
+ ## names by it, as [true, number], or [false, why] naming the values, with [param holder] naming the
90
+ ## target in the refusal. [false, ""] for a word that is a number, which converts as one.
91
+ ##
92
+ ## An enum is written by the name of a value as well as its number, since the name is what the script
93
+ ## and the inspector show and the number is only where the value happens to sit. Tried as the script
94
+ ## spells it, then as the hint does, then as the engine's constant.
95
+ static func enum_by_name(target: Object, declared: Dictionary, word: String, holder: String) -> Array:
96
+ var property: String = str(declared.get("name", ""))
97
+ var choices: Dictionary = enum_of(target, declared)
98
+ var chosen: Variant = enum_named(choices, word)
99
+ if chosen == null:
100
+ chosen = enum_named(enum_values(str(declared.get("hint_string", ""))), word)
101
+ var constants: Dictionary = {}
102
+ if chosen == null:
103
+ constants = engine_constants(target, property)
104
+ chosen = enum_named(constants, word)
105
+ if chosen != null:
106
+ return [true, chosen]
107
+ if word.strip_edges().is_valid_int():
108
+ return [false, ""]
109
+ return [false, enum_refusal(holder, property, word, choices, constants)]
110
+
111
+
112
+ static func enum_refusal(
113
+ holder: String, property: String, word: String, choices: Dictionary, constants: Dictionary = {}
114
+ ) -> String:
115
+ var ways: String = "by name or by number"
116
+ if not constants.is_empty():
117
+ ways = "by name, by number or by the engine's constant, such as %s" % str(constants.keys()[0])
62
118
  var said: String = (
63
- "%s.%s is an enum, and %s is none of its values: send %s by name or by number"
64
- % [holder, property, word, _listed(choices)]
119
+ "%s.%s is an enum, and %s is none of its values: send %s %s"
120
+ % [holder, property, word, _listed(choices), ways]
65
121
  )
66
- var close: PackedStringArray = GdhRead.nearest(word, PackedStringArray(choices.keys()))
122
+ var named: PackedStringArray = PackedStringArray(choices.keys())
123
+ named.append_array(PackedStringArray(constants.keys()))
124
+ var close: PackedStringArray = GdhRead.nearest(word, named)
67
125
  if close.is_empty():
68
126
  return said + "."
69
127
  return said + ". The nearest %s %s." % ["is" if close.size() == 1 else "are", ", ".join(close)]
@@ -76,6 +134,17 @@ static func outside(target: Object, declared: Dictionary, value: Variant, shown:
76
134
  var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
77
135
  var hint_string: String = str(declared.get("hint_string", ""))
78
136
  var property: String = str(declared.get("name", ""))
137
+ if hint == PROPERTY_HINT_ENUM and (typeof(value) == TYPE_STRING or typeof(value) == TYPE_STRING_NAME):
138
+ var names: PackedStringArray = PackedStringArray(enum_values(hint_string).keys())
139
+ if names.is_empty() or names.has(str(value)):
140
+ return ""
141
+ return (
142
+ (
143
+ "%s is none of the names %s lists: %s, which the inspector holds it to; the engine "
144
+ + "stores it as given."
145
+ )
146
+ % [shown, property, ", ".join(names)]
147
+ )
79
148
  if typeof(value) != TYPE_INT and typeof(value) != TYPE_FLOAT:
80
149
  return ""
81
150
  var number: float = GdhRead.as_float(value)
@@ -114,3 +183,111 @@ static func _listed(choices: Dictionary) -> String:
114
183
  ## 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
184
  static func _bound(end: float) -> String:
116
185
  return str(int(end)) if end == floorf(end) and absf(end) < 1e15 else str(end)
186
+
187
+
188
+ ## Puts back on [param built] what its script set as it was attached and was not among
189
+ ## [param wanted], to the defaults it declares, outside the editor. Attaching runs the script's _init
190
+ ## there, which the editor never runs for a script that is not a tool, so a resource_name _init chose
191
+ ## was saved as if asked for. A property whose default the engine cannot report is left as made.
192
+ static func unmade(built: Resource, wanted: Dictionary) -> void:
193
+ var attached: Variant = built.get_script()
194
+ if Engine.is_editor_hint() or not attached is Script:
195
+ return
196
+ var script: Script = attached
197
+ var asked: Dictionary = {}
198
+ for key: Variant in wanted:
199
+ asked[str(key).get_slice(":", 0)] = true
200
+ for entry: Dictionary in built.get_property_list():
201
+ var name: String = str(entry["name"])
202
+ var usage: int = GdhRead.as_int(entry["usage"])
203
+ if usage & PROPERTY_USAGE_STORAGE == 0 or name == "script" or asked.has(name):
204
+ continue
205
+ var declared: Variant = ClassDB.class_get_property_default_value(built.get_class(), name)
206
+ if usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
207
+ declared = script.get_property_default_value(name)
208
+ var held: Variant = built.get(name)
209
+ if declared != null and not (typeof(held) == typeof(declared) and held == declared):
210
+ built.set(name, declared)
211
+
212
+
213
+ ## The element types a typed Array or Dictionary property declares: [element] for an array,
214
+ ## [key, value] for a dictionary, [] when nothing is declared.
215
+ ##
216
+ ## Read from the declaration because the value cannot say: a script's exported `Array[int]` sits
217
+ ## behind a placeholder in the editor, and measured on 4.7.2 that placeholder stores [1.5, 2.0]
218
+ ## as given, which the game then fails to load into the typed array.
219
+ static func elements_of(declared: Dictionary) -> Array:
220
+ var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
221
+ var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
222
+ var wanted: String = str(declared.get("hint_string", ""))
223
+ if wanted.is_empty():
224
+ return []
225
+ if hint == PROPERTY_HINT_TYPE_STRING:
226
+ var parts: PackedStringArray = wanted.split(";")
227
+ if type == TYPE_ARRAY and parts.size() == 1:
228
+ return [_type_in_hint(parts[0])]
229
+ if type == TYPE_DICTIONARY and parts.size() == 2:
230
+ return [_type_in_hint(parts[0]), _type_in_hint(parts[1])]
231
+ return []
232
+ if hint == PROPERTY_HINT_ARRAY_TYPE and type == TYPE_ARRAY:
233
+ return [_type_named(wanted)]
234
+ if hint == PROPERTY_HINT_DICTIONARY_TYPE and type == TYPE_DICTIONARY:
235
+ var parts: PackedStringArray = wanted.split(";")
236
+ if parts.size() == 2:
237
+ return [_type_named(parts[0]), _type_named(parts[1])]
238
+ return []
239
+
240
+
241
+ ## The type at the front of one entry of a type-string hint, "2:" or "24/17:Texture2D".
242
+ static func _type_in_hint(entry: String) -> int:
243
+ var number: String = entry.get_slice(":", 0).get_slice("/", 0)
244
+ return number.to_int() if number.is_valid_int() else TYPE_NIL
245
+
246
+
247
+ ## The Variant type [param name] names, or TYPE_OBJECT for a class.
248
+ static func _type_named(name: String) -> int:
249
+ var named: String = name.get_slice(":", 0).strip_edges()
250
+ for type: int in TYPE_MAX:
251
+ if type_string(type) == named:
252
+ return type
253
+ return TYPE_OBJECT if not named.is_empty() else TYPE_NIL
254
+
255
+
256
+ ## The class the objects of a typed list or map hold, and whether they are nodes, as
257
+ ## [class, nodes], read from entry [param at] of [param declared]'s hint: "24/17:Item" in a type
258
+ ## string, "Item" in an array or dictionary type.
259
+ static func object_kind(declared: Dictionary, at: int) -> Array:
260
+ var entry: String = str(declared.get("hint_string", "")).get_slice(";", at)
261
+ if GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE)) != PROPERTY_HINT_TYPE_STRING:
262
+ return [entry.strip_edges(), false]
263
+ var nodes: bool = entry.get_slice(":", 0).get_slice("/", 1) == str(PROPERTY_HINT_NODE_TYPE)
264
+ return [entry.get_slice(":", 1), nodes]
265
+
266
+
267
+ ## The base class and script that type a list or map holding [param wanted], as [class, script],
268
+ ## or [] when nothing in the project or the engine is called that.
269
+ static func object_typing(wanted: String) -> Array:
270
+ if ClassDB.class_exists(wanted):
271
+ return [StringName(wanted), null]
272
+ var script: Script = script_class(wanted)
273
+ return [] if script == null else [script.get_instance_base_type(), script]
274
+
275
+
276
+ ## The script declaring the class_name [param name], or null.
277
+ static func script_class(name: String) -> Script:
278
+ for entry: Dictionary in ProjectSettings.get_global_class_list():
279
+ if str(entry.get("class", "")) == name:
280
+ return load(str(entry.get("path", ""))) as Script
281
+ return null
282
+
283
+
284
+ ## Whether [param value] is the class [param name] names, an engine class or a script's class_name.
285
+ static func is_a(value: Object, name: String) -> bool:
286
+ if value.is_class(name):
287
+ return true
288
+ var script: Script = value.get_script()
289
+ while script != null:
290
+ if str(script.get_global_name()) == name:
291
+ return true
292
+ script = script.get_base_script()
293
+ 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
- _log.info(
62
- (
63
- "Getting dependencies"
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 total_resources: int = 0
71
-
72
- if not resource_path.is_empty():
73
- var full_path: String = resource_path
74
- if not full_path.begins_with("res://"):
75
- full_path = "res://" + full_path
76
-
77
- if not FileAccess.file_exists(full_path):
78
- return _log.failure(
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
- "total_resources": total_resources,
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
- return {"name": "", "type": type, "properties": {}, "children": children}
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 GdhPropertyValues.same(value, default):
434
+ if GdhSame.same(value, default):
423
435
  continue
424
436
  properties[property] = serialiser.serialize_value(value)
425
437
  var answer: Dictionary = {