gdharness 1.3.41 → 1.3.43

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 (107) hide show
  1. package/build/cli.js +131 -47
  2. package/build/godot/addons/auto_reload/auto_reload.gd +18 -19
  3. package/build/godot/addons/gdharness_editor/animation_reads.gd +84 -35
  4. package/build/godot/addons/gdharness_editor/bridge_client.gd +53 -58
  5. package/build/godot/addons/gdharness_editor/declared_values.gd +68 -32
  6. package/build/godot/addons/gdharness_editor/file_reads.gd +114 -39
  7. package/build/godot/addons/gdharness_editor/file_walk.gd +12 -5
  8. package/build/godot/addons/gdharness_editor/headless_check.gd +94 -41
  9. package/build/godot/addons/gdharness_editor/plugin.gd +2 -3
  10. package/build/godot/addons/gdharness_editor/property_paths.gd +22 -12
  11. package/build/godot/addons/gdharness_editor/property_values.gd +143 -78
  12. package/build/godot/addons/gdharness_editor/reading.gd +37 -12
  13. package/build/godot/addons/gdharness_editor/same_values.gd +4 -4
  14. package/build/godot/addons/gdharness_editor/scene_file.gd +136 -79
  15. package/build/godot/addons/gdharness_editor/scene_uids.gd +12 -6
  16. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -51
  17. package/build/godot/addons/gdharness_editor/setting_reads.gd +7 -6
  18. package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -6
  19. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +41 -28
  20. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +84 -50
  21. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +70 -40
  22. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +34 -30
  23. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +24 -22
  24. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +49 -29
  25. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +26 -21
  26. package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +40 -40
  27. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +181 -107
  28. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -9
  29. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +75 -43
  30. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +35 -20
  31. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +28 -13
  32. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +66 -0
  33. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +171 -62
  35. package/build/godot/addons/gdharness_editor/tools/script_paths.gd +20 -8
  36. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +11 -8
  37. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +47 -34
  38. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +77 -50
  39. package/build/godot/addons/gdharness_editor/value_shapes.gd +5 -6
  40. package/build/godot/addons/gdharness_runtime/call_errors.gd +1 -2
  41. package/build/godot/addons/gdharness_runtime/declared_values.gd +68 -32
  42. package/build/godot/addons/gdharness_runtime/error_report.gd +1 -2
  43. package/build/godot/addons/gdharness_runtime/reading.gd +37 -12
  44. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +84 -63
  45. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +153 -148
  46. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +64 -42
  47. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +184 -122
  48. package/build/godot/addons/gdharness_runtime/runtime_frames.gd +2 -2
  49. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +24 -20
  50. package/build/godot/addons/gdharness_runtime/runtime_input.gd +134 -92
  51. package/build/godot/addons/gdharness_runtime/runtime_items.gd +29 -13
  52. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +0 -1
  53. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +38 -20
  54. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +17 -13
  55. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +84 -46
  56. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +117 -79
  57. package/build/godot/addons/gdharness_runtime/runtime_says.gd +14 -17
  58. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +23 -16
  59. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +117 -66
  60. package/build/godot/addons/gdharness_runtime/runtime_touch.gd +211 -0
  61. package/build/godot/addons/gdharness_runtime/runtime_touch.gd.uid +1 -0
  62. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +44 -27
  63. package/build/godot/addons/gdharness_runtime/runtime_values.gd +108 -73
  64. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +111 -84
  65. package/build/godot/addons/gdharness_runtime/runtime_words.gd +10 -7
  66. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -51
  67. package/build/godot/addons/gdharness_runtime/value_shapes.gd +5 -6
  68. package/build/godot/addons/gdharness_tests/bench.gd +4 -5
  69. package/build/godot/addons/gdharness_tests/event.gd +10 -5
  70. package/build/godot/addons/gdharness_tests/share.gd +38 -18
  71. package/build/godot/addons/gdharness_tests/suites.gd +3 -3
  72. package/build/godot/addons/gdharness_tests/wait.gd +3 -2
  73. package/build/godot/loader/loader.gd +9 -9
  74. package/build/godot/operations/animation_reads.gd +84 -35
  75. package/build/godot/operations/audio_buses.gd +48 -19
  76. package/build/godot/operations/class_cache.gd +16 -11
  77. package/build/godot/operations/classdb_queries.gd +89 -44
  78. package/build/godot/operations/declared_values.gd +68 -32
  79. package/build/godot/operations/dependencies.gd +139 -93
  80. package/build/godot/operations/editor_tools.gd +9 -10
  81. package/build/godot/operations/export_presets.gd +100 -54
  82. package/build/godot/operations/file_reads.gd +114 -39
  83. package/build/godot/operations/file_walk.gd +12 -5
  84. package/build/godot/operations/gdscript_analysis.gd +105 -66
  85. package/build/godot/operations/gdscript_authoring.gd +138 -39
  86. package/build/godot/operations/godot_operations.gd +17 -11
  87. package/build/godot/operations/import_pipeline.gd +110 -59
  88. package/build/godot/operations/input_actions.gd +63 -39
  89. package/build/godot/operations/patterns.gd +3 -1
  90. package/build/godot/operations/plugins.gd +36 -16
  91. package/build/godot/operations/project_config.gd +144 -27
  92. package/build/godot/operations/project_diagnostics.gd +24 -11
  93. package/build/godot/operations/project_file.gd +16 -16
  94. package/build/godot/operations/project_validation.gd +71 -44
  95. package/build/godot/operations/property_paths.gd +22 -12
  96. package/build/godot/operations/property_values.gd +143 -78
  97. package/build/godot/operations/reading.gd +37 -12
  98. package/build/godot/operations/rename_checks.gd +2 -2
  99. package/build/godot/operations/resource_files.gd +11 -9
  100. package/build/godot/operations/same_values.gd +4 -4
  101. package/build/godot/operations/scene_connections.gd +27 -13
  102. package/build/godot/operations/script_check.gd +52 -49
  103. package/build/godot/operations/serialisation.gd +75 -51
  104. package/build/godot/operations/setting_reads.gd +7 -6
  105. package/build/godot/operations/value_shapes.gd +5 -6
  106. package/build/godot/tests/listed.gd +321 -268
  107. package/package.json +1 -1
@@ -22,7 +22,7 @@ func list_plugins(_params: Dictionary) -> Dictionary:
22
22
  "addons_directory_exists": false,
23
23
  "enabled_count": 0,
24
24
  "disabled_count": 0,
25
- "message": "No addons directory found in the project"
25
+ "message": "No addons directory found in the project",
26
26
  }
27
27
 
28
28
  var enabled_paths: Array[String] = _enabled_paths()
@@ -34,7 +34,7 @@ func list_plugins(_params: Dictionary) -> Dictionary:
34
34
  var plugin_cfg_path: String = folder.path_join("plugin.cfg")
35
35
  var name: String = folder.trim_prefix(addons_path)
36
36
  var enabled: bool = plugin_cfg_path in enabled_paths
37
- var plugin_info: Dictionary = {"name": name, "path": folder, "enabled": enabled}
37
+ var plugin_info: Dictionary = { "name": name, "path": folder, "enabled": enabled }
38
38
 
39
39
  var plugin_config: ConfigFile = ConfigFile.new()
40
40
  if plugin_config.load(plugin_cfg_path) == OK:
@@ -52,7 +52,7 @@ func list_plugins(_params: Dictionary) -> Dictionary:
52
52
  "plugins": plugins,
53
53
  "addons_directory_exists": true,
54
54
  "enabled_count": enabled_count,
55
- "disabled_count": plugins.size() - enabled_count
55
+ "disabled_count": plugins.size() - enabled_count,
56
56
  }
57
57
  # Enabled in the project with no plugin.cfg where the entry points: the editor reports each one
58
58
  # as an error at every start and loads nothing for it.
@@ -99,7 +99,9 @@ func enable_plugin(params: Dictionary) -> Dictionary:
99
99
 
100
100
  if plugin_cfg_path in enabled_paths:
101
101
  return {
102
- "plugin_name": plugin_name, "action": "already_enabled", "message": "Plugin is already enabled"
102
+ "plugin_name": plugin_name,
103
+ "action": "already_enabled",
104
+ "message": "Plugin is already enabled",
103
105
  }
104
106
 
105
107
  enabled_paths.append(plugin_cfg_path)
@@ -108,7 +110,9 @@ func enable_plugin(params: Dictionary) -> Dictionary:
108
110
  return _log.failure("Failed to save project.godot: " + error_string(err))
109
111
 
110
112
  var answer: Dictionary = {
111
- "plugin_name": plugin_name, "action": "enabled", "enabled_plugins": _names_of(enabled_paths)
113
+ "plugin_name": plugin_name,
114
+ "action": "enabled",
115
+ "enabled_plugins": _names_of(enabled_paths),
112
116
  }
113
117
  var unloadable: String = _why_not_loaded(plugin_cfg_path)
114
118
  if not unloadable.is_empty():
@@ -116,9 +120,9 @@ func enable_plugin(params: Dictionary) -> Dictionary:
116
120
  return answer
117
121
 
118
122
 
119
- # What the editor finds wrong with the plugin at [param plugin_cfg_path] when it loads it, the checks
120
- # EditorNode::set_addon_plugin_enabled makes, or "" when it would load. Written all the same, since
121
- # the script can be put right before the editor next starts.
123
+ # What the editor finds wrong with the plugin at [param plugin_cfg_path] when it loads it, the
124
+ # checks EditorNode::set_addon_plugin_enabled makes, or "" when it would load. Written all the same,
125
+ # since the script can be put right before the editor next starts.
122
126
  static func _why_not_loaded(plugin_cfg_path: String) -> String:
123
127
  var config: ConfigFile = ConfigFile.new()
124
128
  if config.load(plugin_cfg_path) != OK:
@@ -134,11 +138,16 @@ static func _why_not_loaded(plugin_cfg_path: String) -> String:
134
138
  # Loaded only to learn whether the editor could; what the engine says of a script that does not
135
139
  # parse is about a file the answer does not describe.
136
140
  printerr(GdhLog.ASIDE)
137
- var script: Script = ResourceLoader.load(script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
141
+ var script: Script = ResourceLoader.load(
142
+ script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE
143
+ )
138
144
  printerr(GdhLog.RESUMED)
139
145
  if script == null or script.get_instance_base_type() == &"":
140
146
  return (
141
- "%s does not compile, so the editor disables the plugin again at its next start." % script_path
147
+ (
148
+ "%s does not compile, so the editor disables the plugin again at its next start."
149
+ % script_path
150
+ )
142
151
  + " script_diagnostics names the error."
143
152
  )
144
153
  if not ClassDB.is_parent_class(script.get_instance_base_type(), "EditorPlugin"):
@@ -161,7 +170,7 @@ func disable_plugin(params: Dictionary) -> Dictionary:
161
170
  return {
162
171
  "plugin_name": plugin_name,
163
172
  "action": "already_disabled",
164
- "message": "Plugin is not currently enabled"
173
+ "message": "Plugin is not currently enabled",
165
174
  }
166
175
 
167
176
  enabled_paths.erase(plugin_cfg_path)
@@ -169,7 +178,11 @@ func disable_plugin(params: Dictionary) -> Dictionary:
169
178
  if err != OK:
170
179
  return _log.failure("Failed to save project.godot: " + error_string(err))
171
180
 
172
- return {"plugin_name": plugin_name, "action": "disabled", "enabled_plugins": _names_of(enabled_paths)}
181
+ return {
182
+ "plugin_name": plugin_name,
183
+ "action": "disabled",
184
+ "enabled_plugins": _names_of(enabled_paths),
185
+ }
173
186
 
174
187
 
175
188
  # Every entry in [editor_plugins], as the plugin.cfg path the editor stores. Paths rather than
@@ -180,13 +193,17 @@ func disable_plugin(params: Dictionary) -> Dictionary:
180
193
  # hand-edited file can carry.
181
194
  func _enabled_paths() -> Array[String]:
182
195
  var paths: Array[String] = []
183
- var enabled_value: Variant = ProjectSettings.get_setting("editor_plugins/enabled", PackedStringArray())
196
+ var enabled_value: Variant = ProjectSettings.get_setting(
197
+ "editor_plugins/enabled", PackedStringArray()
198
+ )
184
199
  if enabled_value is PackedStringArray:
185
200
  var packed: PackedStringArray = enabled_value
186
201
  paths.assign(packed)
187
202
  elif enabled_value is String:
188
203
  var listed: String = enabled_value
189
- for m: RegExMatch in GdhPatterns.compiled("res://[^\"',)]+?plugin\\.cfg").search_all(listed):
204
+ for m: RegExMatch in GdhPatterns.compiled("res://[^\"',)]+?plugin\\.cfg").search_all(
205
+ listed
206
+ ):
190
207
  paths.append(m.get_string())
191
208
  return paths
192
209
 
@@ -197,7 +214,9 @@ static func _names_of(paths: Array[String]) -> Array[String]:
197
214
  var names: Array[String] = []
198
215
  for path: String in paths:
199
216
  var inside: bool = path.begins_with("res://addons/") and path.ends_with("/plugin.cfg")
200
- names.append(path.trim_prefix("res://addons/").trim_suffix("/plugin.cfg") if inside else path)
217
+ names.append(
218
+ path.trim_prefix("res://addons/").trim_suffix("/plugin.cfg") if inside else path
219
+ )
201
220
  return names
202
221
 
203
222
 
@@ -215,6 +234,7 @@ func _save_enabled_paths(paths: Array[String]) -> Error:
215
234
  # Kept in an Array[String] and converted here, because appending to a packed array answers with
216
235
  # whether it worked and a project holding return_value_discarded at error level will not compile
217
236
  # a script that drops that answer. The conversion is what matters: an Array[String] handed to
218
- # set_setting is written as Array[String]([...]), which the editor does not read as a plugin list.
237
+ # set_setting is written as Array[String]([...]), which the editor does not read as a plugin
238
+ # list.
219
239
  ProjectSettings.set_setting("editor_plugins/enabled", PackedStringArray(paths))
220
240
  return GdhProjectFile.save(["editor_plugins/enabled"])
@@ -7,6 +7,52 @@ const GdhProjectFile = preload("project_file.gd")
7
7
  const GdhSerialisation = preload("serialisation.gd")
8
8
  const GdhSettingReads = preload("setting_reads.gd")
9
9
  const GdhShapes = preload("value_shapes.gd")
10
+ ## The words GDScript reserves, which no script could reach an autoload by.
11
+ const GDSCRIPT_KEYWORDS: Array[String] = [
12
+ "and",
13
+ "as",
14
+ "assert",
15
+ "await",
16
+ "break",
17
+ "breakpoint",
18
+ "class",
19
+ "class_name",
20
+ "const",
21
+ "continue",
22
+ "elif",
23
+ "else",
24
+ "enum",
25
+ "extends",
26
+ "false",
27
+ "for",
28
+ "func",
29
+ "if",
30
+ "in",
31
+ "INF",
32
+ "is",
33
+ "match",
34
+ "NAN",
35
+ "namespace",
36
+ "not",
37
+ "null",
38
+ "or",
39
+ "pass",
40
+ "PI",
41
+ "preload",
42
+ "return",
43
+ "self",
44
+ "signal",
45
+ "static",
46
+ "super",
47
+ "TAU",
48
+ "trait",
49
+ "true",
50
+ "var",
51
+ "void",
52
+ "when",
53
+ "while",
54
+ "yield",
55
+ ]
10
56
 
11
57
  var _log: GdhLog
12
58
  var _values: GdhSerialisation = GdhSerialisation.new()
@@ -54,9 +100,9 @@ func set_project_setting(params: Dictionary) -> Dictionary:
54
100
  var in_file: bool = GdhProjectFile.sets(setting_path)
55
101
 
56
102
  var wanted: int = _wanted_type(setting_path, old_value if had_value else null)
57
- # A value of fields or a colour's html code is fitted as a property write fits it, so a value one
58
- # of them takes the other takes too. A number is left to the check below, which refuses one the
59
- # setting would cut where fitting it would cut it.
103
+ # A value of fields or a colour's html code is fitted as a property write fits it, so a value
104
+ # one of them takes the other takes too. A number is left to the check below, which refuses one
105
+ # the setting would cut where fitting it would cut it.
60
106
  var final_value: Variant = _values.deserialize_value(value)
61
107
  if final_value is Dictionary or (wanted == TYPE_COLOR and final_value is String):
62
108
  final_value = _values.fitted(value, wanted)
@@ -64,8 +110,8 @@ func set_project_setting(params: Dictionary) -> Dictionary:
64
110
  if not _converts_faithfully(final_value, wanted):
65
111
  return _log.failure(_cannot_hold(setting_path, wanted, final_value))
66
112
  final_value = type_convert(final_value, wanted)
67
- # JSON carries every number as a float and a new setting has no declared type to fit one to, so a
68
- # whole number was saved as 800.0 beside an answer saying 800.
113
+ # JSON carries every number as a float and a new setting has no declared type to fit one to, so
114
+ # a whole number was saved as 800.0 beside an answer saying 800.
69
115
  if not had_value and wanted == TYPE_NIL and final_value is float:
70
116
  var number: float = final_value
71
117
  if number == floorf(number) and absf(number) < 9.0e15:
@@ -86,7 +132,8 @@ func set_project_setting(params: Dictionary) -> Dictionary:
86
132
  "saved": true,
87
133
  }
88
134
  # The same value in the same type is no change, and the file is left alone: a save rewrites it
89
- # whole. The type is compared as well, so an int setting the file holds as 50.0 is still put right.
135
+ # whole. The type is compared as well, so an int setting the file holds as 50.0 is still put
136
+ # right.
90
137
  if had_value and typeof(old_value) == typeof(final_value) and old_value == final_value:
91
138
  answer["saved"] = false
92
139
  answer["unchanged"] = true
@@ -127,12 +174,34 @@ func set_project_setting(params: Dictionary) -> Dictionary:
127
174
  )
128
175
  var made: Array[String] = _translations_made_by(missing)
129
176
  if not made.is_empty():
130
- said += ("; project_import reimport makes the translation files from " + ", ".join(made))
177
+ said += (
178
+ "; project_import reimport makes the translation files from " + ", ".join(made)
179
+ )
131
180
  answer["note"] = (str(answer["note"]) + " " if answer.has("note") else "") + said + "."
132
181
  ProjectSettings.set_setting(setting_path, final_value)
133
182
  var err: Error = GdhProjectFile.save([setting_path])
134
183
  if err != OK:
135
184
  return _log.failure("Failed to save project.godot: " + error_string(err))
185
+ # An override still replaces what was written wherever its feature is, and a write of the base
186
+ # value read as if it reached every build.
187
+ var overrides: Dictionary = GdhSettingReads.overrides_of(setting_path, _values)
188
+ if not overrides.is_empty():
189
+ answer["overrides"] = overrides
190
+ var parts: Array[String] = []
191
+ for feature: Variant in overrides:
192
+ parts.append(
193
+ (
194
+ "one with the feature %s reads %s from %s.%s"
195
+ % [feature, JSON.stringify(overrides[feature]), setting_path, feature]
196
+ )
197
+ )
198
+ answer["overridesNote"] = (
199
+ (
200
+ "Not every build reads what was written: %s. Set or unset an override to change "
201
+ + "what its builds read."
202
+ )
203
+ % "; ".join(parts)
204
+ )
136
205
  return answer
137
206
 
138
207
 
@@ -175,8 +244,9 @@ static func _translations_made_by(missing: Array[String]) -> Array[String]:
175
244
  return sources
176
245
 
177
246
 
178
- # Takes a setting's line out of project.godot, so the engine's default holds again, or nothing at all
179
- # for one the engine does not declare: the way back from a set, which writes a misspelt name as given.
247
+ # Takes a setting's line out of project.godot, so the engine's default holds again, or nothing at
248
+ # all for one the engine does not declare: the way back from a set, which writes a misspelt name as
249
+ # given.
180
250
  func unset_project_setting(params: Dictionary) -> Dictionary:
181
251
  var setting_path: String = str(params.get("setting", ""))
182
252
  if setting_path.is_empty():
@@ -193,7 +263,9 @@ func unset_project_setting(params: Dictionary) -> Dictionary:
193
263
  if not held or not GdhProjectFile.sets(setting_path):
194
264
  answer["removed"] = false
195
265
  answer["saved"] = false
196
- answer["note"] = "project.godot does not set %s, so there was nothing to take out." % setting_path
266
+ answer["note"] = (
267
+ "project.godot does not set %s, so there was nothing to take out." % setting_path
268
+ )
197
269
  var near: PackedStringArray = PackedStringArray() if held else _settings_near(setting_path)
198
270
  if not near.is_empty():
199
271
  answer["note"] += (
@@ -202,8 +274,8 @@ func unset_project_setting(params: Dictionary) -> Dictionary:
202
274
  )
203
275
  return _with_what_holds(answer, setting_path)
204
276
  answer["old_value"] = _values.serialize_value(value)
205
- # The default rather than null for a setting the engine declares: null takes the declaration away
206
- # as well, and a value at its default is left out of the file all the same.
277
+ # The default rather than null for a setting the engine declares: null takes the declaration
278
+ # away as well, and a value at its default is left out of the file all the same.
207
279
  ProjectSettings.set_setting(setting_path, default_value)
208
280
  var err: Error = GdhProjectFile.save([setting_path])
209
281
  if err != OK:
@@ -277,7 +349,10 @@ static func _cannot_hold(setting_path: String, wanted: int, value: Variant) -> S
277
349
  var declared: String = "%s is declared as %s" % [setting_path, type_string(wanted)]
278
350
  var cost: String = " Setting it would save a different value than the one asked for."
279
351
  if wanted == TYPE_INT and typeof(value) == TYPE_FLOAT:
280
- return "%s, which holds whole numbers, and %s would be cut to fit.%s" % [declared, str(value), cost]
352
+ return (
353
+ "%s, which holds whole numbers, and %s would be cut to fit.%s"
354
+ % [declared, str(value), cost]
355
+ )
281
356
  if wanted in numbers and typeof(value) in numbers:
282
357
  return "%s, and %s is not the same number as one.%s" % [declared, str(value), cost]
283
358
  var given: String = type_string(typeof(value))
@@ -285,8 +360,8 @@ static func _cannot_hold(setting_path: String, wanted: int, value: Variant) -> S
285
360
  return "%s, and %s (%s) cannot become one.%s" % [declared, JSON.stringify(value), given, remedy]
286
361
 
287
362
 
288
- ## Why [param tagged], a value whose tag could not be built, was not written to [param setting_path],
289
- ## and how a value of that tag is written.
363
+ ## Why [param tagged], a value whose tag could not be built, was not written to
364
+ ## [param setting_path], and how a value of that tag is written.
290
365
  static func _unbuilt(setting_path: String, tagged: Dictionary) -> String:
291
366
  var tag: String = str(tagged["_type"])
292
367
  var refused: String = (
@@ -299,7 +374,9 @@ static func _unbuilt(setting_path: String, tagged: Dictionary) -> String:
299
374
  var keys: Array = shapes[0]
300
375
  var parts: Array[String] = ['"_type": "%s"' % tag]
301
376
  for key: Variant in keys:
302
- parts.append('"%s": %s' % [key, "[...]" if tag.begins_with("Packed") and key == "value" else "..."])
377
+ parts.append(
378
+ '"%s": %s' % [key, "[...]" if tag.begins_with("Packed") and key == "value" else "..."]
379
+ )
303
380
  return refused + " One is written as {%s}." % ", ".join(parts)
304
381
 
305
382
 
@@ -313,14 +390,21 @@ func add_autoload(params: Dictionary) -> Dictionary:
313
390
  # every start. `enabled: false` used to write the entry without its asterisk and answer that it
314
391
  # was disabled, and the autoload ran all the same.
315
392
  if not GdhRead.as_bool(params.get("enabled", true), true):
316
- return _log.failure(
317
- (
318
- "An autoload cannot be added disabled: Godot loads every autoload in the list at every"
319
- + " start. global: false keeps its name out of the global scope and still loads it;"
320
- + " remove_autoload is what stops it loading"
393
+ return (
394
+ _log
395
+ . failure(
396
+ (
397
+ "An autoload cannot be added disabled: Godot loads every autoload in the list "
398
+ + "at every start. global: false keeps its name out of the global scope and "
399
+ + "still loads it; remove_autoload is what stops it loading"
400
+ )
321
401
  )
322
402
  )
323
403
 
404
+ var clash: String = _autoload_name_refusal(name)
405
+ if not clash.is_empty():
406
+ return _log.failure(clash)
407
+
324
408
  if not path.begins_with("res://"):
325
409
  path = "res://" + path
326
410
 
@@ -347,7 +431,7 @@ func add_autoload(params: Dictionary) -> Dictionary:
347
431
  "enabled": true,
348
432
  "global": global,
349
433
  "action": "unchanged",
350
- "saved": false
434
+ "saved": false,
351
435
  }
352
436
  ProjectSettings.set_setting(setting, entry)
353
437
  var err: Error = GdhProjectFile.save([setting])
@@ -364,6 +448,35 @@ func add_autoload(params: Dictionary) -> Dictionary:
364
448
  }
365
449
 
366
450
 
451
+ ## Why [param name] cannot name an autoload, or empty when it can. The editor's Autoload tab refuses
452
+ ## the same names: an autoload named after an engine class or a built-in type takes that name in
453
+ ## every script, so an autoload called Input made every Input.is_action_pressed call the autoload.
454
+ static func _autoload_name_refusal(name: String) -> String:
455
+ var reason: String = ""
456
+ if not name.is_valid_ascii_identifier():
457
+ reason = "it is not an identifier, which a script could name"
458
+ elif name in GDSCRIPT_KEYWORDS:
459
+ reason = "it is a GDScript keyword"
460
+ elif ClassDB.class_exists(name):
461
+ reason = (
462
+ "it is the engine class %s, and every script naming %s would reach the autoload instead"
463
+ % [name, name]
464
+ )
465
+ else:
466
+ for type: int in TYPE_MAX:
467
+ if type_string(type) == name:
468
+ reason = "it is the built-in type %s" % name
469
+ if reason.is_empty():
470
+ return ""
471
+ return (
472
+ (
473
+ "%s cannot name an autoload: %s. Nothing was written; the editor's Autoload tab "
474
+ + "refuses it too."
475
+ )
476
+ % [JSON.stringify(name), reason]
477
+ )
478
+
479
+
367
480
  # Remove an autoload singleton
368
481
  func remove_autoload(params: Dictionary) -> Dictionary:
369
482
  var name: String = str(params.get("name", ""))
@@ -380,7 +493,7 @@ func remove_autoload(params: Dictionary) -> Dictionary:
380
493
  if err != OK:
381
494
  return _log.failure("Failed to save project.godot: " + error_string(err))
382
495
 
383
- return {"name": name, "removed": true, "old_path": old_value.trim_prefix("*")}
496
+ return { "name": name, "removed": true, "old_path": old_value.trim_prefix("*") }
384
497
 
385
498
 
386
499
  # List all autoload singletons
@@ -407,7 +520,7 @@ func list_autoloads(_params: Dictionary) -> Dictionary:
407
520
  }
408
521
  autoloads.append(autoload_info)
409
522
 
410
- return {"autoloads": autoloads, "count": autoloads.size()}
523
+ return { "autoloads": autoloads, "count": autoloads.size() }
411
524
 
412
525
 
413
526
  # Set the main scene
@@ -421,7 +534,9 @@ func set_main_scene(params: Dictionary) -> Dictionary:
421
534
 
422
535
  if not FileAccess.file_exists(scene_path):
423
536
  return _log.failure(
424
- GdhFileWalk.new().with_nearest_files("Scene file does not exist: " + scene_path, scene_path)
537
+ GdhFileWalk.new().with_nearest_files(
538
+ "Scene file does not exist: " + scene_path, scene_path
539
+ )
425
540
  )
426
541
 
427
542
  # The engine starts the game by instancing the main scene, so a script or any other resource set
@@ -433,7 +548,9 @@ func set_main_scene(params: Dictionary) -> Dictionary:
433
548
  "%s is %s, not a scene, and the main scene has to be one"
434
549
  % [scene_path, "a " + kind if not kind.is_empty() else "no resource the engine loads"]
435
550
  )
436
- return _log.failure(GdhFileWalk.new().with_nearest_files(said, scene_path.get_basename() + ".tscn"))
551
+ return _log.failure(
552
+ GdhFileWalk.new().with_nearest_files(said, scene_path.get_basename() + ".tscn")
553
+ )
437
554
 
438
555
  var old_main_scene: String = str(ProjectSettings.get_setting("application/run/main_scene", ""))
439
556
 
@@ -443,4 +560,4 @@ func set_main_scene(params: Dictionary) -> Dictionary:
443
560
  if err != OK:
444
561
  return _log.failure("Failed to save project settings: " + str(err))
445
562
 
446
- return {"old_main_scene": old_main_scene, "new_main_scene": scene_path, "saved": true}
563
+ return { "old_main_scene": old_main_scene, "new_main_scene": scene_path, "saved": true }
@@ -40,14 +40,17 @@ func get_project_health(params: Dictionary) -> Dictionary:
40
40
  # failed this check and lost points, while Godot's own guidance is to keep a scene's files
41
41
  # together by feature, so the check failed projects for following the engine's advice.
42
42
 
43
- checks["structure"] = {"name": "Project Structure", "passed": details.is_empty(), "details": details}
43
+ checks["structure"] = {
44
+ "name": "Project Structure", "passed": details.is_empty(), "details": details,
45
+ }
44
46
 
45
47
  # 2. Resource Check
46
48
  if "resources" in check_categories:
47
49
  var details: Array[String] = []
48
50
 
49
- # Every format the engine imports, the list the import status reads: five of them were checked
50
- # here, so a project of glTF models, fonts and WebP textures had nothing to reimport.
51
+ # Every format the engine imports, the list the import status reads: five of them were
52
+ # checked here, so a project of glTF models, fonts and WebP textures had nothing to
53
+ # reimport.
51
54
  var resources: Array[String] = _files.find_files_with_extensions(
52
55
  "res://", GdhImportPipeline.IMPORTABLE_EXTENSIONS
53
56
  )
@@ -68,7 +71,7 @@ func get_project_health(params: Dictionary) -> Dictionary:
68
71
  "details": details,
69
72
  # What the check read: files the engine imports. A .tres is a resource and was counted
70
73
  # under this name as none, beside a project holding one.
71
- "importable_files": resources.size()
74
+ "importable_files": resources.size(),
72
75
  }
73
76
 
74
77
  # 3. Scripts Check
@@ -106,7 +109,7 @@ func get_project_health(params: Dictionary) -> Dictionary:
106
109
  "name": "Scripts",
107
110
  "passed": details.is_empty(),
108
111
  "details": details,
109
- "total_scripts": script_files.size()
112
+ "total_scripts": script_files.size(),
110
113
  }
111
114
  if compiled.has("checked"):
112
115
  scripts["compiled"] = GdhRead.as_int(compiled["checked"])
@@ -123,14 +126,18 @@ func get_project_health(params: Dictionary) -> Dictionary:
123
126
  for entry: Dictionary in GdhInputActions.new(_log).undefined_in_scripts():
124
127
  details.append(GdhInputActions.undefined_line(entry))
125
128
  deductions += 5
126
- checks["references"] = {"name": "References", "passed": details.is_empty(), "details": details}
129
+ checks["references"] = {
130
+ "name": "References", "passed": details.is_empty(), "details": details,
131
+ }
127
132
 
128
133
  # 4. Scenes Check
129
134
  if "scenes" in check_categories:
130
135
  var details: Array[String] = []
131
136
 
132
137
  # Binary scenes too, which a project may save instead and which counted as none.
133
- var scene_files: Array[String] = _files.find_files_with_extensions("res://", ["tscn", "scn"])
138
+ var scene_files: Array[String] = _files.find_files_with_extensions(
139
+ "res://", ["tscn", "scn"]
140
+ )
134
141
  if scene_files.size() == 0:
135
142
  details.append("No scene files found in project")
136
143
  deductions += 5
@@ -142,7 +149,7 @@ func get_project_health(params: Dictionary) -> Dictionary:
142
149
  "name": "Scenes",
143
150
  "passed": details.is_empty(),
144
151
  "details": details,
145
- "total_scenes": scene_files.size()
152
+ "total_scenes": scene_files.size(),
146
153
  }
147
154
 
148
155
  # 5. Configuration Check
@@ -162,7 +169,9 @@ func get_project_health(params: Dictionary) -> Dictionary:
162
169
  details.append("No project name set")
163
170
  deductions += 2
164
171
 
165
- checks["config"] = {"name": "Configuration", "passed": details.is_empty(), "details": details}
172
+ checks["config"] = {
173
+ "name": "Configuration", "passed": details.is_empty(), "details": details,
174
+ }
166
175
 
167
176
  var score: int = maxi(0, 100 - deductions)
168
177
 
@@ -193,7 +202,11 @@ func get_project_health(params: Dictionary) -> Dictionary:
193
202
  var check: Dictionary = checks[category]
194
203
  var found: Array[String] = check["details"]
195
204
  for detail: String in found:
196
- issues.append({"check": category, "detail": detail})
205
+ issues.append({ "check": category, "detail": detail })
197
206
  return {
198
- "score": score, "grade": grade, "checks": checks, "issues": issues, "recommendations": recommendations
207
+ "score": score,
208
+ "grade": grade,
209
+ "checks": checks,
210
+ "issues": issues,
211
+ "recommendations": recommendations,
199
212
  }
@@ -1,16 +1,15 @@
1
1
  extends RefCounted
2
-
3
2
  ## project.godot written through the engine's own save, in the order the file already held.
4
3
  ##
5
4
  ## The engine writes each section's keys in the order it registered them, and an engine running a
6
5
  ## script registers them in another order than the editor does, so a one-setting change moved lines
7
- ## nobody touched across several sections and a commit of it read as a rewrite. The save is still the
8
- ## engine's, for what only it gets right: the header and the text of each value the call changed.
9
- ## The order is taken from the file as it was, and so is every line of a key the call did not change,
10
- ## because the engine writes its settings as it holds them rather than as the file had them. It
11
- ## leaves out each key at its default, so a project holding `unsafe_call_argument=0` on purpose lost
12
- ## the line to a change of another key. It writes a dictionary with its keys sorted, so gdUnit4's
13
- ## session hooks, which run in the order listed, changed order. And it writes what a
6
+ ## nobody touched across several sections and a commit of it read as a rewrite. The save is still
7
+ ## the engine's, for what only it gets right: the header and the text of each value the call
8
+ ## changed. The order is taken from the file as it was, and so is every line of a key the call did
9
+ ## not change, because the engine writes its settings as it holds them rather than as the file had
10
+ ## them. It leaves out each key at its default, so a project holding `unsafe_call_argument=0` on
11
+ ## purpose lost the line to a change of another key. It writes a dictionary with its keys sorted, so
12
+ ## gdUnit4's session hooks, which run in the order listed, changed order. And it writes what a
14
13
  ## `project_settings_override` file laid over the project, so a player's window mode in the user
15
14
  ## directory became the game's default.
16
15
 
@@ -42,8 +41,8 @@ static func save(changed: PackedStringArray) -> Error:
42
41
  ## [param before] with the lines of the settings in [param changed] as [param written] has them: one
43
42
  ## the engine wrote goes in its place, or where the engine put it relative to its neighbours when it
44
43
  ## is new, and one the engine left out leaves the file. Every other line is [param before]'s, in its
45
- ## place, and a key the engine wrote that the file did not hold and the call did not change stays out.
46
- ## The header is [param written]'s.
44
+ ## place, and a key the engine wrote that the file did not hold and the call did not change stays
45
+ ## out. The header is [param written]'s.
47
46
  static func in_order_of(before: String, written: String, changed: PackedStringArray) -> String:
48
47
  var old: Dictionary = _parsed(before)
49
48
  var fresh: Dictionary = _parsed(written)
@@ -113,8 +112,8 @@ static func in_order_of(before: String, written: String, changed: PackedStringAr
113
112
  fresh_keys[name].append(str(entry["key"]))
114
113
 
115
114
  # The blank lines closing a section belong to its place in the file rather than to the section,
116
- # so the last one keeps the ending the engine gave the file wherever it moved from. A section put
117
- # back sits between two others, which the engine parts with a blank line.
115
+ # so the last one keeps the ending the engine gave the file wherever it moved from. A section
116
+ # put back sits between two others, which the engine parts with a blank line.
118
117
  var names: Array[String] = _merged(old_names, fresh_names)
119
118
  var tails: Array[String] = []
120
119
  for section: Dictionary in fresh_sections:
@@ -181,7 +180,8 @@ static func _merged(held: Array[String], wanted: Array[String]) -> Array[String]
181
180
 
182
181
 
183
182
  ## [param text] as a preamble (the header and anything before the first section) and its sections,
184
- ## each with its header line and the blank lines after it, its entries, and the blank lines closing it.
183
+ ## each with its header line and the blank lines after it, its entries, and the blank lines closing
184
+ ## it.
185
185
  static func _parsed(text: String) -> Dictionary:
186
186
  var preamble: String = ""
187
187
  var sections: Array[Dictionary] = []
@@ -193,7 +193,7 @@ static func _parsed(text: String) -> Dictionary:
193
193
  if not current.is_empty():
194
194
  _close(current, body)
195
195
  sections.append(current)
196
- current = {"name": stripped.substr(1, stripped.length() - 2), "head": line}
196
+ current = { "name": stripped.substr(1, stripped.length() - 2), "head": line }
197
197
  body = []
198
198
  elif current.is_empty():
199
199
  preamble += line
@@ -204,7 +204,7 @@ static func _parsed(text: String) -> Dictionary:
204
204
  if not current.is_empty():
205
205
  _close(current, body)
206
206
  sections.append(current)
207
- return {"preamble": preamble, "sections": sections}
207
+ return { "preamble": preamble, "sections": sections }
208
208
 
209
209
 
210
210
  ## Fills in [param section]'s entries and closing blank lines from the lines after its head. A blank
@@ -218,7 +218,7 @@ static func _close(section: Dictionary, body: Array[String]) -> void:
218
218
  for line: String in body:
219
219
  var stripped: String = line.strip_edges()
220
220
  if entries.is_empty() or not (stripped.is_empty() or stripped.begins_with(";")):
221
- entries.append({"key": _key_of(line), "text": line})
221
+ entries.append({ "key": _key_of(line), "text": line })
222
222
  else:
223
223
  var last: Dictionary = entries[entries.size() - 1]
224
224
  last["text"] = str(last["text"]) + line