gdharness 1.3.2 → 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.
Files changed (46) hide show
  1. package/build/cli.js +163 -85
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +14 -11
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +157 -4
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
  5. package/build/godot/addons/gdharness_editor/file_walk.gd +134 -0
  6. package/build/godot/addons/gdharness_editor/file_walk.gd.uid +1 -0
  7. package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
  8. package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
  9. package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
  10. package/build/godot/addons/gdharness_editor/property_values.gd +249 -227
  11. package/build/godot/addons/gdharness_editor/reading.gd +15 -0
  12. package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
  13. package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
  14. package/build/godot/addons/gdharness_editor/scene_file.gd +100 -15
  15. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
  16. package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -3
  17. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +402 -55
  18. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
  19. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
  20. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +88 -5
  22. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +102 -337
  23. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
  24. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
  25. package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
  26. package/build/godot/addons/gdharness_runtime/runtime_input.gd +10 -26
  27. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
  28. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
  29. package/build/godot/addons/gdharness_runtime/runtime_values.gd +30 -0
  30. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
  31. package/build/godot/operations/animation_reads.gd +14 -11
  32. package/build/godot/operations/declared_values.gd +157 -4
  33. package/build/godot/operations/dependencies.gd +14 -39
  34. package/build/godot/operations/editor_tools.gd +7 -3
  35. package/build/godot/operations/file_reads.gd +15 -3
  36. package/build/godot/operations/file_walk.gd +1 -0
  37. package/build/godot/operations/gdscript_analysis.gd +229 -75
  38. package/build/godot/operations/gdscript_authoring.gd +142 -34
  39. package/build/godot/operations/import_pipeline.gd +17 -6
  40. package/build/godot/operations/logger.gd +7 -0
  41. package/build/godot/operations/property_paths.gd +225 -0
  42. package/build/godot/operations/property_values.gd +249 -227
  43. package/build/godot/operations/reading.gd +15 -0
  44. package/build/godot/operations/same_values.gd +177 -0
  45. package/build/godot/operations/serialisation.gd +75 -3
  46. package/package.json +1 -1
@@ -81,15 +81,7 @@ static func animations(player: AnimationPlayer, animation_name: String) -> Dicti
81
81
  listed.append(summary)
82
82
  return {"ok": true, "animations": listed}
83
83
  if not player.has_animation(StringName(animation_name)):
84
- var names: PackedStringArray = player.get_animation_list()
85
- return {
86
- "ok": false,
87
- "error":
88
- (
89
- "%s has no animation %s. It has: %s."
90
- % [player.name, animation_name, ", ".join(names) if not names.is_empty() else "none"]
91
- )
92
- }
84
+ return {"ok": false, "error": no_animation(player, str(player.name), animation_name)}
93
85
  var anim: Animation = player.get_animation(StringName(animation_name))
94
86
  var answer: Dictionary = _summary(anim)
95
87
  answer["ok"] = true
@@ -102,6 +94,16 @@ static func animations(player: AnimationPlayer, animation_name: String) -> Dicti
102
94
  return answer
103
95
 
104
96
 
97
+ ## Why [param player], called [param called], cannot be asked for [param animation_name], with the
98
+ ## animations it has.
99
+ static func no_animation(player: AnimationPlayer, called: String, animation_name: String) -> String:
100
+ var names: PackedStringArray = player.get_animation_list()
101
+ return (
102
+ "%s has no animation %s. It has: %s."
103
+ % [called, animation_name, ", ".join(names) if not names.is_empty() else "none"]
104
+ )
105
+
106
+
105
107
  static func _summary(anim: Animation) -> Dictionary:
106
108
  return {
107
109
  "length": anim.length,
@@ -162,8 +164,9 @@ static func state_machine(tree: AnimationTree, state_machine_path: String) -> An
162
164
  static func no_machine(tree: AnimationTree, state_machine_path: String) -> String:
163
165
  if tree == null or tree.tree_root == null:
164
166
  return (
165
- "The AnimationTree has no tree_root. Make one with resource_edit create of an "
166
- + "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."
167
170
  )
168
171
  var current: AnimationNodeStateMachine = tree.tree_root as AnimationNodeStateMachine
169
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
- 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
+ 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 by name or by number"
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 close: PackedStringArray = GdhRead.nearest(word, PackedStringArray(choices.keys()))
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
@@ -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 = {
@@ -0,0 +1,134 @@
1
+ @tool
2
+ extends RefCounted
3
+
4
+ const GdhRead = preload("reading.gd")
5
+
6
+ # Two walks rather than one because they disagree about hidden entries on purpose: a suffix
7
+ # walk descends into every visible directory and matches whole names, while the extension walk
8
+ # skips anything beginning with a dot, including .import sidecars that would otherwise answer
9
+ # for the resource they belong to.
10
+ #
11
+ # Both stop at a `.gdignore`, because the engine does: nothing under one is imported, so what is
12
+ # in there is not a resource, not a dependency and not a global class. Walking in anyway hands
13
+ # every caller files the engine will never answer for.
14
+
15
+
16
+ # Why [param path], a res:// or uid:// path a setting names, is not a file the engine can load, or
17
+ # empty when it is. A uid:// path resolves through the project's uid cache, which the import writes,
18
+ # so one that does not resolve is a file that has gone or a project not imported yet, and saying
19
+ # only that the file does not exist sent a caller looking for a scene that was there.
20
+ static func missing_reason(path: String) -> String:
21
+ if path.begins_with("uid://"):
22
+ var id: int = ResourceUID.text_to_id(path)
23
+ if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id):
24
+ return (
25
+ path
26
+ + " does not resolve: no file in the project's uid cache has it, so the file has gone or the"
27
+ + " project has not been imported yet (project_import refresh_uids imports it)"
28
+ )
29
+ var resolved: String = ResourceUID.get_id_path(id)
30
+ return (
31
+ "" if FileAccess.file_exists(resolved) else path + " names " + resolved + ", which does not exist"
32
+ )
33
+ return "" if FileAccess.file_exists(path) else path + " does not exist"
34
+
35
+
36
+ ## [param said], the refusal for [param path], a res:// path nothing is at, with the files of its
37
+ ## extension whose names are nearest its own, or every one when there are few and none is near, so
38
+ ## the caller has a path to send instead.
39
+ func with_nearest_files(said: String, path: String) -> String:
40
+ var extension: String = path.get_extension()
41
+ if extension.is_empty():
42
+ return said
43
+ var files: Array[String] = find_files_with_extensions("res://", [extension])
44
+ # The walk lists a directory in the order the filesystem keeps it, which differs between
45
+ # platforms, so the same project was answered with its files in another order on Linux.
46
+ files.sort()
47
+ if files.is_empty():
48
+ return "%s. The project holds no .%s file." % [said, extension]
49
+ var names: PackedStringArray = []
50
+ for file: String in files:
51
+ var _named: bool = names.append(file.get_file().get_basename())
52
+ var close: PackedStringArray = GdhRead.nearest(path.get_file().get_basename(), names)
53
+ var offered: PackedStringArray = []
54
+ for file: String in files:
55
+ if file.get_file().get_basename() in close:
56
+ var _offered: bool = offered.append(file)
57
+ if not offered.is_empty():
58
+ var one: bool = offered.size() == 1
59
+ return (
60
+ "%s. The nearest .%s %s %s."
61
+ % [said, extension, "file is" if one else "files are", ", ".join(offered)]
62
+ )
63
+ if files.size() <= GdhRead.NAMED_CHILDREN:
64
+ return "%s. The project's .%s files are %s." % [said, extension, ", ".join(files)]
65
+ return "%s. None of the project's %d .%s files is named like it." % [said, files.size(), extension]
66
+
67
+
68
+ # A directory the engine steps over, marker file and all.
69
+ func is_stepped_over(path: String) -> bool:
70
+ return FileAccess.file_exists(path + ".gdignore")
71
+
72
+
73
+ # An addon gdharness installed, told by the marker setup writes into it. The engine loads it, but
74
+ # nobody on the project wrote it: the health check counted its thirty-odd scripts as the project's,
75
+ # 39 beside the 5 project_info's own count gave, which the server's walks already step over.
76
+ func is_installed_by_gdharness(path: String) -> bool:
77
+ return FileAccess.file_exists(path + ".gdharness-version")
78
+
79
+
80
+ # Files under `path` whose name ends with `extension`, which is passed with its dot.
81
+ func find_files(path: String, extension: String) -> Array[String]:
82
+ var files: Array[String] = []
83
+ if is_stepped_over(path) or is_installed_by_gdharness(path):
84
+ return files
85
+
86
+ var dir: DirAccess = DirAccess.open(path)
87
+
88
+ if dir:
89
+ if dir.list_dir_begin() != OK:
90
+ return files
91
+ var file_name: String = dir.get_next()
92
+
93
+ while file_name != "":
94
+ if dir.current_is_dir() and not file_name.begins_with("."):
95
+ files.append_array(find_files(path + file_name + "/", extension))
96
+ elif file_name.ends_with(extension):
97
+ files.append(path + file_name)
98
+
99
+ file_name = dir.get_next()
100
+
101
+ return files
102
+
103
+
104
+ # Files under `path` whose extension is in `extensions`, which are passed without their dot.
105
+ func find_files_with_extensions(path: String, extensions: Array) -> Array[String]:
106
+ var files: Array[String] = []
107
+ if is_stepped_over(path) or is_installed_by_gdharness(path):
108
+ return files
109
+
110
+ var dir: DirAccess = DirAccess.open(path)
111
+
112
+ if dir:
113
+ if dir.list_dir_begin() != OK:
114
+ return files
115
+ var file_name: String = dir.get_next()
116
+
117
+ while file_name != "":
118
+ if file_name.begins_with("."):
119
+ file_name = dir.get_next()
120
+ continue
121
+
122
+ var full_path: String = path + file_name
123
+ if dir.current_is_dir():
124
+ files.append_array(find_files_with_extensions(full_path + "/", extensions))
125
+ else:
126
+ var ext: String = file_name.get_extension().to_lower()
127
+ if ext in extensions:
128
+ files.append(full_path)
129
+
130
+ file_name = dir.get_next()
131
+
132
+ dir.list_dir_end()
133
+
134
+ return files
@@ -0,0 +1 @@
1
+ uid://c16785cs9xozr
@@ -11,8 +11,9 @@ extends RefCounted
11
11
  ## both, so a property a built-in setter derives (a Control's offsets from its size) is left alone
12
12
  ## unless a script that is not a tool is involved.
13
13
 
14
- const GdhPropertyValues = preload("property_values.gd")
14
+ const GdhPropertyPaths = preload("property_paths.gd")
15
15
  const GdhRead = preload("reading.gd")
16
+ const GdhSame = preload("same_values.gd")
16
17
 
17
18
  const REMEDY: String = (
18
19
  "Open the project's editor (editor_launch with hidden: true) and ask again: the write then goes "
@@ -78,7 +79,7 @@ static func differing(object: Object, before: Dictionary, names: Dictionary) ->
78
79
  if names.has(name):
79
80
  continue
80
81
  var was: Variant = before[name] if before.has(name) else default_of(object, name)
81
- if not GdhPropertyValues.same(now[name], was):
82
+ if not GdhSame.same(now[name], was):
82
83
  changed.append(name)
83
84
  return changed
84
85
 
@@ -122,28 +123,46 @@ static func _initialised(script: Script, name: String, type: int) -> Variant:
122
123
  ## nothing a script did: the saver writes a property whose default the engine cannot report whatever
123
124
  ## it holds, the editor's save as well.
124
125
  static func at_default(object: Object, name: String) -> bool:
125
- return GdhPropertyValues.same(object.get(name), default_of(object, name))
126
+ return GdhSame.same(object.get(name), default_of(object, name))
126
127
 
127
128
 
128
- ## The ids, in the file, of the embedded resources [param property] reaches into on [param object],
129
- ## such as the material of [code]material:albedo_color[/code], as a set.
129
+ ## What [param property] writes in the embedded resource it reaches into on [param object], by its
130
+ ## id in the file, as the set of names written in it: the hp of the resource built in as inner for
131
+ ## [code]inner/hp[/code], or position for [code]inner/position:x[/code]. Only those names are spared
132
+ ## from the check for what a script changed: the resource's _init still ran when it was loaded, and
133
+ ## sparing the whole resource saved what that set.
130
134
  static func reached(object: Object, property: String) -> Dictionary:
131
135
  var ids: Dictionary = {}
132
- var parts: PackedStringArray = property.split(":")
133
- var current: Variant = object
134
- for index: int in parts.size() - 1:
135
- if not current is Object:
136
- break
137
- var holder: Object = current
138
- current = holder.get(parts[index])
139
- if current is Resource:
140
- var resource: Resource = current
141
- var id: String = resource.get_scene_unique_id()
142
- if not id.is_empty():
143
- ids[id] = true
136
+ var place: Array = GdhPropertyPaths.holder_of(object, property)
137
+ var through: Array[Resource] = place[2]
138
+ if through.is_empty():
139
+ return ids
140
+ var holder: Resource = through.back()
141
+ var name: String = place[1]
142
+ if GdhPropertyPaths.declared_on(holder, name).is_empty():
143
+ name = name.get_slice(":", 0)
144
+ var id: String = holder.get_scene_unique_id()
145
+ if not id.is_empty():
146
+ ids[id] = {name: true}
144
147
  return ids
145
148
 
146
149
 
150
+ ## Adds what [param more] spares to [param into], both as [method reached] answers them; true for an
151
+ ## id spares the whole resource, as one a tool rebuilt.
152
+ static func spare(into: Dictionary, more: Dictionary) -> void:
153
+ for id: Variant in more:
154
+ var held: Variant = into.get(id)
155
+ var given: Variant = more[id]
156
+ if held is bool or given is bool:
157
+ into[id] = true
158
+ elif held is Dictionary:
159
+ var names: Dictionary = held
160
+ var added: Dictionary = given
161
+ names.merge(added)
162
+ else:
163
+ into[id] = given
164
+
165
+
147
166
  ## Whether [param object] holds, at any depth, an embedded resource with a script that is not a tool.
148
167
  static func holds_scripted_embedded(object: Object, seen: Dictionary) -> bool:
149
168
  for value: Variant in stored(object).values():
@@ -276,7 +295,7 @@ static func reads_the_same(was: Variant, now: Variant, default: Variant) -> bool
276
295
  return false
277
296
  else:
278
297
  values.append(str_to_var(str(text)))
279
- return GdhPropertyValues.same(values[0], values[1])
298
+ return GdhSame.same(values[0], values[1])
280
299
 
281
300
 
282
301
  static func _main_entries(text: String) -> Dictionary:
@@ -315,9 +334,9 @@ static func scripted_sections(text: String) -> Dictionary:
315
334
 
316
335
  ## Each property an embedded resource of [param after] sets differently from [param before], as
317
336
  ## [its id, name, whether before sets it, the text before gives it, the text after gives it or
318
- ## null]. Only those with a script that
319
- ## is not a tool are compared, since only a script runs differently here, and those in
320
- ## [param reached_ids], which the tool changed, are left out.
337
+ ## null]. Only those with a script that is not a tool are compared, since only a script runs
338
+ ## differently here, and what [param reached_ids] says the tool wrote is left out, as
339
+ ## [method reached] answers it.
321
340
  static func embedded_changes(before: String, after: String, reached_ids: Dictionary) -> Array[Array]:
322
341
  var changed: Array[Array] = []
323
342
  if before.is_empty():
@@ -327,8 +346,10 @@ static func embedded_changes(before: String, after: String, reached_ids: Diction
327
346
  for key: Variant in scripted_sections(before):
328
347
  var section: String = str(key)
329
348
  var id: String = section.trim_prefix("sub:")
330
- if not section.begins_with("sub:") or reached_ids.has(id) or not new.has(section):
349
+ var spared: Variant = reached_ids.get(id, {})
350
+ if not section.begins_with("sub:") or spared is bool or not new.has(section):
331
351
  continue
352
+ var written: Dictionary = spared
332
353
  var was_section: Dictionary = old[section]
333
354
  var now_section: Dictionary = new[section]
334
355
  var was: Dictionary = entries_of(was_section)
@@ -337,7 +358,7 @@ static func embedded_changes(before: String, after: String, reached_ids: Diction
337
358
  all.merge(now)
338
359
  for entry: Variant in all:
339
360
  var name: String = str(entry)
340
- if was.get(name) != now.get(name):
361
+ if was.get(name) != now.get(name) and not written.has(name):
341
362
  changed.append([id, name, was.has(name), str(was.get(name, "")), now.get(name)])
342
363
  return changed
343
364