gdharness 0.4.1

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 (37) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +125 -0
  3. package/build/cli.js +30353 -0
  4. package/build/godot/addons/auto_reload/auto_reload.gd +89 -0
  5. package/build/godot/addons/auto_reload/plugin.cfg +6 -0
  6. package/build/godot/addons/gdharness_editor/bridge_client.gd +197 -0
  7. package/build/godot/addons/gdharness_editor/plugin.cfg +6 -0
  8. package/build/godot/addons/gdharness_editor/plugin.gd +88 -0
  9. package/build/godot/addons/gdharness_editor/tool_executor.gd +104 -0
  10. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +397 -0
  11. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +85 -0
  12. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +427 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +885 -0
  14. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +292 -0
  15. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +77 -0
  16. package/build/godot/addons/gdharness_runtime/runtime_input.gd +254 -0
  17. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +283 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_values.gd +169 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +119 -0
  20. package/build/godot/operations/audio_buses.gd +125 -0
  21. package/build/godot/operations/class_cache.gd +180 -0
  22. package/build/godot/operations/classdb_queries.gd +252 -0
  23. package/build/godot/operations/dependencies.gd +293 -0
  24. package/build/godot/operations/file_walk.gd +55 -0
  25. package/build/godot/operations/gdscript_analysis.gd +288 -0
  26. package/build/godot/operations/gdscript_authoring.gd +419 -0
  27. package/build/godot/operations/godot_operations.gd +186 -0
  28. package/build/godot/operations/import_pipeline.gd +390 -0
  29. package/build/godot/operations/input_actions.gd +237 -0
  30. package/build/godot/operations/logger.gd +33 -0
  31. package/build/godot/operations/plugins.gd +159 -0
  32. package/build/godot/operations/project_config.gd +153 -0
  33. package/build/godot/operations/project_diagnostics.gd +159 -0
  34. package/build/godot/operations/resource_files.gd +132 -0
  35. package/build/godot/operations/serialisation.gd +154 -0
  36. package/build/index.js +29252 -0
  37. package/package.json +57 -0
@@ -0,0 +1,159 @@
1
+ extends RefCounted
2
+
3
+ const Log = preload("logger.gd")
4
+
5
+ var _log: Log
6
+
7
+
8
+ func _init(p_log: Log) -> void:
9
+ _log = p_log
10
+
11
+
12
+ # List all plugins in the project with their status
13
+ func list_plugins(_params: Dictionary) -> Dictionary:
14
+ _log.info("Listing plugins")
15
+
16
+ var addons_path: String = "res://addons/"
17
+ if not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(addons_path)):
18
+ return {
19
+ "plugins": [],
20
+ "addons_directory_exists": false,
21
+ "enabled_count": 0,
22
+ "disabled_count": 0,
23
+ "message": "No addons directory found in the project"
24
+ }
25
+
26
+ var enabled_plugins: Array[String] = _enabled_plugin_names()
27
+ _log.debug("Enabled plugins: " + str(enabled_plugins))
28
+
29
+ var plugins: Array[Dictionary] = []
30
+ var enabled_count: int = 0
31
+
32
+ var dir: DirAccess = DirAccess.open(addons_path)
33
+ if dir:
34
+ dir.list_dir_begin()
35
+ var folder_name: String = dir.get_next()
36
+
37
+ while folder_name != "":
38
+ if dir.current_is_dir() and not folder_name.begins_with("."):
39
+ var plugin_cfg_path: String = addons_path + folder_name + "/plugin.cfg"
40
+
41
+ if FileAccess.file_exists(plugin_cfg_path):
42
+ var enabled: bool = folder_name in enabled_plugins
43
+ var plugin_info: Dictionary = {
44
+ "name": folder_name, "path": addons_path + folder_name, "enabled": enabled
45
+ }
46
+
47
+ var plugin_config: ConfigFile = ConfigFile.new()
48
+ if plugin_config.load(plugin_cfg_path) == OK:
49
+ plugin_info["display_name"] = plugin_config.get_value("plugin", "name", folder_name)
50
+ plugin_info["description"] = plugin_config.get_value("plugin", "description", "")
51
+ plugin_info["author"] = plugin_config.get_value("plugin", "author", "")
52
+ plugin_info["version"] = plugin_config.get_value("plugin", "version", "")
53
+ plugin_info["script"] = plugin_config.get_value("plugin", "script", "")
54
+
55
+ plugins.append(plugin_info)
56
+ if enabled:
57
+ enabled_count += 1
58
+
59
+ folder_name = dir.get_next()
60
+
61
+ dir.list_dir_end()
62
+
63
+ return {
64
+ "plugins": plugins,
65
+ "addons_directory_exists": true,
66
+ "enabled_count": enabled_count,
67
+ "disabled_count": plugins.size() - enabled_count
68
+ }
69
+
70
+
71
+ # Enable a plugin
72
+ func enable_plugin(params: Dictionary) -> Dictionary:
73
+ var plugin_name: String = str(params.get("plugin_name", ""))
74
+
75
+ _log.info("Enabling plugin: " + plugin_name)
76
+
77
+ var plugin_cfg_path: String = "res://addons/" + plugin_name + "/plugin.cfg"
78
+ if not FileAccess.file_exists(plugin_cfg_path):
79
+ _log.error("Plugin not found: " + plugin_name)
80
+ return _log.failure("Expected plugin.cfg at: " + plugin_cfg_path)
81
+
82
+ var enabled_plugins: Array[String] = _enabled_plugin_names()
83
+
84
+ if plugin_name in enabled_plugins:
85
+ return {
86
+ "plugin_name": plugin_name, "action": "already_enabled", "message": "Plugin is already enabled"
87
+ }
88
+
89
+ enabled_plugins.append(plugin_name)
90
+ var err: Error = _save_enabled_plugins(enabled_plugins)
91
+ if err != OK:
92
+ return _log.failure("Failed to save project.godot: " + error_string(err))
93
+
94
+ return {"plugin_name": plugin_name, "action": "enabled", "enabled_plugins": enabled_plugins}
95
+
96
+
97
+ # Disable a plugin
98
+ func disable_plugin(params: Dictionary) -> Dictionary:
99
+ var plugin_name: String = str(params.get("plugin_name", ""))
100
+
101
+ _log.info("Disabling plugin: " + plugin_name)
102
+
103
+ var enabled_plugins: Array[String] = _enabled_plugin_names()
104
+
105
+ if not plugin_name in enabled_plugins:
106
+ return {
107
+ "plugin_name": plugin_name,
108
+ "action": "already_disabled",
109
+ "message": "Plugin is not currently enabled"
110
+ }
111
+
112
+ enabled_plugins.erase(plugin_name)
113
+ var err: Error = _save_enabled_plugins(enabled_plugins)
114
+ if err != OK:
115
+ return _log.failure("Failed to save project.godot: " + error_string(err))
116
+
117
+ return {"plugin_name": plugin_name, "action": "disabled", "enabled_plugins": enabled_plugins}
118
+
119
+
120
+ # The addon directory names in [editor_plugins]. The editor stores them as the engine
121
+ # expression PackedStringArray("res://addons/<name>/plugin.cfg", ...), which loads as that
122
+ # type; the String form is what an older hand-edited file can carry.
123
+ func _enabled_plugin_names() -> Array[String]:
124
+ var names: Array[String] = []
125
+ var regex: RegEx = RegEx.new()
126
+ regex.compile("res://addons/([^/]+)/plugin.cfg")
127
+
128
+ var enabled_value: Variant = ProjectSettings.get_setting("editor_plugins/enabled", PackedStringArray())
129
+ if enabled_value is PackedStringArray:
130
+ var paths: PackedStringArray = enabled_value
131
+ for path: String in paths:
132
+ var m: RegExMatch = regex.search(path)
133
+ if m:
134
+ names.append(m.get_string(1))
135
+ elif enabled_value is String:
136
+ var listed: String = enabled_value
137
+ for m: RegExMatch in regex.search_all(listed):
138
+ names.append(m.get_string(1))
139
+
140
+ return names
141
+
142
+
143
+ # Written through ProjectSettings rather than a ConfigFile of project.godot: the engine saves
144
+ # the file the way the editor does, header comment and all, where ConfigFile drops every
145
+ # comment and reorders what it kept. A real PackedStringArray, because that is serialised as
146
+ # the unquoted expression the editor reads, whereas the same text handed over as a String is
147
+ # written quoted and loads as a String the editor does not recognise as a plugin list. An
148
+ # empty list is the setting removed, which is what the editor writes for no plugins.
149
+ func _save_enabled_plugins(names: Array[String]) -> Error:
150
+ if names.is_empty():
151
+ ProjectSettings.set_setting("editor_plugins/enabled", null)
152
+ return ProjectSettings.save()
153
+
154
+ var enabled_paths: PackedStringArray = PackedStringArray()
155
+ for name: String in names:
156
+ enabled_paths.append("res://addons/" + name + "/plugin.cfg")
157
+
158
+ ProjectSettings.set_setting("editor_plugins/enabled", enabled_paths)
159
+ return ProjectSettings.save()
@@ -0,0 +1,153 @@
1
+ extends RefCounted
2
+
3
+ const Log = preload("logger.gd")
4
+ const Serialisation = preload("serialisation.gd")
5
+
6
+ var _log: Log
7
+ var _values: Serialisation = Serialisation.new()
8
+
9
+
10
+ func _init(p_log: Log) -> void:
11
+ _log = p_log
12
+
13
+
14
+ # Get a project setting value
15
+ func get_project_setting(params: Dictionary) -> Dictionary:
16
+ var setting_path: String = str(params.get("setting", ""))
17
+
18
+ _log.info("Getting project setting: " + setting_path)
19
+
20
+ var result: Dictionary = {
21
+ "setting_path": setting_path, "exists": ProjectSettings.has_setting(setting_path)
22
+ }
23
+
24
+ if result["exists"]:
25
+ result["value"] = _values.serialize_value(ProjectSettings.get_setting(setting_path))
26
+ else:
27
+ result["value"] = null
28
+ result["message"] = "Setting does not exist"
29
+
30
+ return result
31
+
32
+
33
+ # Set a project setting value
34
+ func set_project_setting(params: Dictionary) -> Dictionary:
35
+ var setting_path: String = str(params.get("setting", ""))
36
+ if setting_path.is_empty():
37
+ return _log.failure("setting is required")
38
+ var value: Variant = params.get("value")
39
+
40
+ _log.info("Setting project setting: " + setting_path)
41
+
42
+ var old_value: Variant = null
43
+ var had_value: bool = ProjectSettings.has_setting(setting_path)
44
+ if had_value:
45
+ old_value = ProjectSettings.get_setting(setting_path)
46
+
47
+ var final_value: Variant = _values.deserialize_value(value)
48
+ ProjectSettings.set_setting(setting_path, final_value)
49
+ var err: Error = ProjectSettings.save()
50
+ if err != OK:
51
+ return _log.failure("Failed to save project.godot: " + error_string(err))
52
+
53
+ return {
54
+ "setting_path": setting_path,
55
+ "old_value": _values.serialize_value(old_value) if had_value else null,
56
+ "new_value": _values.serialize_value(final_value),
57
+ "was_new": not had_value,
58
+ "saved": true,
59
+ }
60
+
61
+
62
+ # Add an autoload singleton
63
+ func add_autoload(params: Dictionary) -> Dictionary:
64
+ var name: String = str(params.get("name", ""))
65
+ var path: String = str(params.get("path", ""))
66
+ var enabled: bool = bool(params.get("enabled", true))
67
+
68
+ if not path.begins_with("res://"):
69
+ path = "res://" + path
70
+
71
+ _log.info("Adding autoload: " + name + " -> " + path)
72
+
73
+ if not FileAccess.file_exists(path):
74
+ return _log.failure("Autoload file does not exist: " + path)
75
+
76
+ # Through ProjectSettings so the file is saved the way the editor saves it, header and
77
+ # every other line kept; a ConfigFile of project.godot drops the comments on the way out.
78
+ var setting: String = "autoload/" + name
79
+ var was_updated: bool = ProjectSettings.has_setting(setting)
80
+
81
+ # An asterisk in front of the path is how project.godot marks an autoload as enabled.
82
+ ProjectSettings.set_setting(setting, ("*" if enabled else "") + path)
83
+ var err: Error = ProjectSettings.save()
84
+ if err != OK:
85
+ return _log.failure("Failed to save project.godot: " + error_string(err))
86
+
87
+ return {"name": name, "path": path, "enabled": enabled, "action": "updated" if was_updated else "added"}
88
+
89
+
90
+ # Remove an autoload singleton
91
+ func remove_autoload(params: Dictionary) -> Dictionary:
92
+ var name: String = str(params.get("name", ""))
93
+
94
+ _log.info("Removing autoload: " + name)
95
+
96
+ var setting: String = "autoload/" + name
97
+ if not ProjectSettings.has_setting(setting):
98
+ return _log.failure("Autoload not found: " + name)
99
+
100
+ var old_value: String = str(ProjectSettings.get_setting(setting))
101
+ ProjectSettings.set_setting(setting, null)
102
+ var err: Error = ProjectSettings.save()
103
+ if err != OK:
104
+ return _log.failure("Failed to save project.godot: " + error_string(err))
105
+
106
+ return {"name": name, "removed": true, "old_path": old_value.trim_prefix("*")}
107
+
108
+
109
+ # List all autoload singletons
110
+ func list_autoloads(_params: Dictionary) -> Dictionary:
111
+ _log.info("Listing autoloads")
112
+
113
+ var autoloads: Array[Dictionary] = []
114
+
115
+ for property: Dictionary in ProjectSettings.get_property_list():
116
+ var setting: String = str(property.get("name", ""))
117
+ if not setting.begins_with("autoload/"):
118
+ continue
119
+ var value: String = str(ProjectSettings.get_setting(setting))
120
+ var path: String = value.trim_prefix("*")
121
+
122
+ var autoload_info: Dictionary = {
123
+ "name": setting.trim_prefix("autoload/"),
124
+ "path": path,
125
+ "enabled": value.begins_with("*"),
126
+ "file_exists": FileAccess.file_exists(path),
127
+ }
128
+ autoloads.append(autoload_info)
129
+
130
+ return {"autoloads": autoloads, "count": autoloads.size()}
131
+
132
+
133
+ # Set the main scene
134
+ func set_main_scene(params: Dictionary) -> Dictionary:
135
+ var scene_path: String = str(params.get("scene_path", ""))
136
+
137
+ if not scene_path.begins_with("res://"):
138
+ scene_path = "res://" + scene_path
139
+
140
+ _log.info("Setting main scene: " + scene_path)
141
+
142
+ if not FileAccess.file_exists(scene_path):
143
+ return _log.failure("Scene file does not exist: " + scene_path)
144
+
145
+ var old_main_scene: String = str(ProjectSettings.get_setting("application/run/main_scene", ""))
146
+
147
+ ProjectSettings.set_setting("application/run/main_scene", scene_path)
148
+ var err: Error = ProjectSettings.save()
149
+
150
+ if err != OK:
151
+ return _log.failure("Failed to save project settings: " + str(err))
152
+
153
+ return {"old_main_scene": old_main_scene, "new_main_scene": scene_path, "saved": true}
@@ -0,0 +1,159 @@
1
+ extends RefCounted
2
+
3
+ const FileWalk = preload("file_walk.gd")
4
+ const Log = preload("logger.gd")
5
+
6
+ var _log: Log
7
+ var _files: FileWalk = FileWalk.new()
8
+
9
+
10
+ func _init(p_log: Log) -> void:
11
+ _log = p_log
12
+
13
+
14
+ # Get comprehensive project health check with scoring
15
+ func get_project_health(params: Dictionary) -> Dictionary:
16
+ var check_categories: Array = params.get(
17
+ "categories", ["structure", "resources", "scripts", "scenes", "config"]
18
+ )
19
+
20
+ _log.info("Performing project health check")
21
+
22
+ var checks: Dictionary = {}
23
+ var recommendations: Array[String] = []
24
+ var deductions: int = 0
25
+
26
+ # 1. Project Structure Check
27
+ if "structure" in check_categories:
28
+ var passed: bool = true
29
+ var details: Array[String] = []
30
+
31
+ if not FileAccess.file_exists("res://project.godot"):
32
+ passed = false
33
+ details.append("Missing project.godot file")
34
+ deductions += 30
35
+
36
+ var recommended_dirs: Array[String] = ["res://scenes", "res://scripts", "res://assets"]
37
+ var missing_dirs: Array[String] = []
38
+ for dir: String in recommended_dirs:
39
+ if not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(dir)):
40
+ missing_dirs.append(dir)
41
+
42
+ if missing_dirs.size() > 0:
43
+ details.append("Consider organizing with directories: " + ", ".join(missing_dirs))
44
+ deductions += 2 * missing_dirs.size()
45
+
46
+ checks["structure"] = {"name": "Project Structure", "passed": passed, "details": details}
47
+
48
+ # 2. Resource Check
49
+ if "resources" in check_categories:
50
+ var details: Array[String] = []
51
+
52
+ var importable_extensions: Array[String] = ["png", "jpg", "wav", "mp3", "ogg"]
53
+ var resources: Array[String] = []
54
+ for ext: String in importable_extensions:
55
+ resources.append_array(_files.find_files("res://", "." + ext))
56
+
57
+ var missing_imports: int = 0
58
+ for res: String in resources:
59
+ if not FileAccess.file_exists(res + ".import"):
60
+ missing_imports += 1
61
+
62
+ if missing_imports > 0:
63
+ details.append(str(missing_imports) + " resources may need reimporting")
64
+ deductions += missing_imports
65
+
66
+ checks["resources"] = {
67
+ "name": "Resources", "passed": true, "details": details, "total_resources": resources.size()
68
+ }
69
+
70
+ # 3. Scripts Check
71
+ if "scripts" in check_categories:
72
+ var details: Array[String] = []
73
+
74
+ var script_files: Array[String] = _files.find_files("res://", ".gd")
75
+ var todo_count: int = 0
76
+ var empty_functions: int = 0
77
+
78
+ for script_path: String in script_files:
79
+ var file: FileAccess = FileAccess.open(script_path, FileAccess.READ)
80
+ if file:
81
+ var content: String = file.get_as_text()
82
+ file.close()
83
+
84
+ if "# TODO" in content or "# FIXME" in content:
85
+ todo_count += 1
86
+ if "pass # " in content or content.ends_with("pass\n"):
87
+ empty_functions += 1
88
+
89
+ if todo_count > 0:
90
+ details.append(str(todo_count) + " scripts have TODO/FIXME comments")
91
+ deductions += todo_count
92
+
93
+ if empty_functions > 0:
94
+ details.append(str(empty_functions) + " scripts may have empty functions")
95
+ deductions += empty_functions
96
+
97
+ checks["scripts"] = {
98
+ "name": "Scripts", "passed": true, "details": details, "total_scripts": script_files.size()
99
+ }
100
+
101
+ # 4. Scenes Check
102
+ if "scenes" in check_categories:
103
+ var details: Array[String] = []
104
+
105
+ var scene_files: Array[String] = _files.find_files("res://", ".tscn")
106
+ if scene_files.size() == 0:
107
+ details.append("No scene files found in project")
108
+ deductions += 5
109
+
110
+ checks["scenes"] = {
111
+ "name": "Scenes", "passed": true, "details": details, "total_scenes": scene_files.size()
112
+ }
113
+
114
+ # 5. Configuration Check
115
+ if "config" in check_categories:
116
+ var details: Array[String] = []
117
+
118
+ var main_scene: String = str(ProjectSettings.get_setting("application/run/main_scene", ""))
119
+ if main_scene.is_empty():
120
+ details.append("No main scene configured")
121
+ deductions += 10
122
+ elif not FileAccess.file_exists(main_scene):
123
+ details.append("Main scene file does not exist: " + main_scene)
124
+ deductions += 15
125
+
126
+ var project_name: String = str(ProjectSettings.get_setting("application/config/name", ""))
127
+ if project_name.is_empty():
128
+ details.append("No project name set")
129
+ deductions += 2
130
+
131
+ if not FileAccess.file_exists("res://export_presets.cfg"):
132
+ details.append("No export presets configured")
133
+ deductions += 3
134
+
135
+ checks["config"] = {"name": "Configuration", "passed": true, "details": details}
136
+
137
+ var score: int = maxi(0, 100 - deductions)
138
+
139
+ var grade: String
140
+ if score >= 90:
141
+ grade = "A"
142
+ elif score >= 80:
143
+ grade = "B"
144
+ elif score >= 70:
145
+ grade = "C"
146
+ elif score >= 60:
147
+ grade = "D"
148
+ else:
149
+ grade = "F"
150
+
151
+ if deductions > 0:
152
+ if score < 70:
153
+ recommendations.append("Address critical issues to improve project stability")
154
+ if not FileAccess.file_exists("res://export_presets.cfg"):
155
+ recommendations.append("Configure export presets for your target platforms")
156
+
157
+ return {
158
+ "score": score, "grade": grade, "checks": checks, "issues": [], "recommendations": recommendations
159
+ }
@@ -0,0 +1,132 @@
1
+ extends RefCounted
2
+
3
+ const FileWalk = preload("file_walk.gd")
4
+ const Log = preload("logger.gd")
5
+
6
+ var _log: Log
7
+ var _files: FileWalk = FileWalk.new()
8
+
9
+
10
+ func _init(p_log: Log) -> void:
11
+ _log = p_log
12
+
13
+
14
+ # The UID the engine gave a file, from the .uid sidecar it writes beside scripts and shaders.
15
+ func get_uid(params: Dictionary) -> Dictionary:
16
+ var file_path: String = str(params.get("resource_path", ""))
17
+ if file_path.is_empty():
18
+ return _log.failure("resource_path is required")
19
+ if not file_path.begins_with("res://"):
20
+ file_path = "res://" + file_path
21
+
22
+ _log.info("Getting UID for file: " + file_path)
23
+
24
+ var absolute_path: String = ProjectSettings.globalize_path(file_path)
25
+ if not FileAccess.file_exists(file_path):
26
+ return _log.failure("File does not exist: " + file_path)
27
+
28
+ var uid_path: String = file_path + ".uid"
29
+ var f: FileAccess = FileAccess.open(uid_path, FileAccess.READ)
30
+ if not f:
31
+ return {
32
+ "file": file_path,
33
+ "absolute_path": absolute_path,
34
+ "exists": false,
35
+ "message": "No .uid sidecar. refresh_uids writes one for every script and shader."
36
+ }
37
+
38
+ var uid_content: String = f.get_as_text()
39
+ f.close()
40
+
41
+ return {
42
+ "file": file_path, "absolute_path": absolute_path, "uid": uid_content.strip_edges(), "exists": true
43
+ }
44
+
45
+
46
+ # Resave every scene, script and shader so that UID references are current.
47
+ func resave_resources(_params: Dictionary) -> Dictionary:
48
+ _log.info("Resaving all resources to update UID references...")
49
+ var project_path: String = "res://"
50
+
51
+ var scenes: Array[String] = _files.find_files(project_path, ".tscn")
52
+ _log.debug("Found " + str(scenes.size()) + " scenes")
53
+
54
+ # Resave each scene
55
+ var success_count: int = 0
56
+ var error_count: int = 0
57
+
58
+ for scene_path: String in scenes:
59
+ _log.debug("Processing scene: " + scene_path)
60
+
61
+ if not FileAccess.file_exists(scene_path):
62
+ _log.error("Scene file does not exist at: " + scene_path)
63
+ error_count += 1
64
+ continue
65
+
66
+ var scene: Resource = load(scene_path)
67
+ if not scene:
68
+ error_count += 1
69
+ _log.error("Failed to load: " + scene_path)
70
+ continue
71
+
72
+ var error: Error = ResourceSaver.save(scene, scene_path)
73
+ if error != OK:
74
+ error_count += 1
75
+ _log.error("Failed to save: " + scene_path + ", error: " + str(error))
76
+ continue
77
+
78
+ success_count += 1
79
+ if _log.debug_mode:
80
+ _log.debug("Scene saved successfully: " + scene_path)
81
+ var file_check_after: bool = FileAccess.file_exists(scene_path)
82
+ _log.debug("File exists check after save: " + str(file_check_after))
83
+ if not file_check_after:
84
+ _log.error("File reported as saved but does not exist at: " + scene_path)
85
+
86
+ # A UID sits beside every script and shader as a .uid file. Outside the editor the save
87
+ # below answers OK and writes no sidecar, so what comes back is what was resaved and how
88
+ # many are still without one, rather than a count of UIDs this made.
89
+ var scripts: Array[String] = (
90
+ _files.find_files(project_path, ".gd")
91
+ + _files.find_files(project_path, ".shader")
92
+ + _files.find_files(project_path, ".gdshader")
93
+ )
94
+ _log.debug("Found " + str(scripts.size()) + " scripts/shaders")
95
+
96
+ var missing_uids: int = 0
97
+ var resaved_scripts: int = 0
98
+
99
+ for script_path: String in scripts:
100
+ _log.debug("Checking UID for: " + script_path)
101
+ var uid_path: String = script_path + ".uid"
102
+
103
+ var f: FileAccess = FileAccess.open(uid_path, FileAccess.READ)
104
+ if f:
105
+ _log.debug("UID file already exists for: " + script_path)
106
+ continue
107
+
108
+ missing_uids += 1
109
+ _log.debug("Missing UID file for: " + script_path + ", resaving...")
110
+
111
+ var res: Resource = load(script_path)
112
+ if not res:
113
+ _log.error("Failed to load resource: " + script_path)
114
+ continue
115
+
116
+ var error: Error = ResourceSaver.save(res, script_path)
117
+ if error != OK:
118
+ _log.error("Failed to resave: " + script_path + ", error: " + str(error))
119
+ continue
120
+
121
+ resaved_scripts += 1
122
+ _log.debug("Resaved: " + script_path)
123
+
124
+ _log.info("Resave operation complete")
125
+
126
+ return {
127
+ "scenes_processed": scenes.size(),
128
+ "scenes_saved": success_count,
129
+ "scenes_with_errors": error_count,
130
+ "scripts_missing_uids": missing_uids,
131
+ "scripts_resaved": resaved_scripts
132
+ }