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.
- package/LICENSE +22 -0
- package/README.md +125 -0
- package/build/cli.js +30353 -0
- package/build/godot/addons/auto_reload/auto_reload.gd +89 -0
- package/build/godot/addons/auto_reload/plugin.cfg +6 -0
- package/build/godot/addons/gdharness_editor/bridge_client.gd +197 -0
- package/build/godot/addons/gdharness_editor/plugin.cfg +6 -0
- package/build/godot/addons/gdharness_editor/plugin.gd +88 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +104 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +397 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +85 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +427 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +885 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +292 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +77 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +254 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +283 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +169 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +119 -0
- package/build/godot/operations/audio_buses.gd +125 -0
- package/build/godot/operations/class_cache.gd +180 -0
- package/build/godot/operations/classdb_queries.gd +252 -0
- package/build/godot/operations/dependencies.gd +293 -0
- package/build/godot/operations/file_walk.gd +55 -0
- package/build/godot/operations/gdscript_analysis.gd +288 -0
- package/build/godot/operations/gdscript_authoring.gd +419 -0
- package/build/godot/operations/godot_operations.gd +186 -0
- package/build/godot/operations/import_pipeline.gd +390 -0
- package/build/godot/operations/input_actions.gd +237 -0
- package/build/godot/operations/logger.gd +33 -0
- package/build/godot/operations/plugins.gd +159 -0
- package/build/godot/operations/project_config.gd +153 -0
- package/build/godot/operations/project_diagnostics.gd +159 -0
- package/build/godot/operations/resource_files.gd +132 -0
- package/build/godot/operations/serialisation.gd +154 -0
- package/build/index.js +29252 -0
- 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
|
+
}
|