gdharness 1.1.21 → 1.1.23
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/build/cli.js +1470 -726
- package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
- package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
- package/build/godot/addons/gdharness_editor/reading.gd +17 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
- package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
- package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +15 -5
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +89 -16
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +107 -343
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +61 -28
- package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
- package/build/godot/operations/audio_buses.gd +47 -11
- package/build/godot/operations/class_cache.gd +18 -2
- package/build/godot/operations/classdb_queries.gd +28 -17
- package/build/godot/operations/dependencies.gd +128 -17
- package/build/godot/operations/file_walk.gd +20 -0
- package/build/godot/operations/gdscript_analysis.gd +59 -20
- package/build/godot/operations/gdscript_authoring.gd +127 -60
- package/build/godot/operations/godot_operations.gd +24 -5
- package/build/godot/operations/import_pipeline.gd +179 -19
- package/build/godot/operations/logger.gd +2 -2
- package/build/godot/operations/plugins.gd +88 -70
- package/build/godot/operations/project_config.gd +41 -6
- package/build/godot/operations/project_diagnostics.gd +27 -11
- package/build/godot/operations/reading.gd +17 -0
- package/build/godot/operations/resource_files.gd +60 -17
- package/build/godot/operations/serialisation.gd +116 -7
- package/build/index.js +1385 -553
- package/build/keeper.js +4 -0
- package/package.json +1 -1
|
@@ -24,48 +24,63 @@ func list_plugins(_params: Dictionary) -> Dictionary:
|
|
|
24
24
|
"message": "No addons directory found in the project"
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
var
|
|
28
|
-
_log.debug("Enabled plugins: " + str(
|
|
27
|
+
var enabled_paths: Array[String] = _enabled_paths()
|
|
28
|
+
_log.debug("Enabled plugins: " + str(enabled_paths))
|
|
29
29
|
|
|
30
30
|
var plugins: Array[Dictionary] = []
|
|
31
31
|
var enabled_count: int = 0
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
var
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 {
|
|
32
|
+
for folder: String in _plugin_folders(addons_path):
|
|
33
|
+
var plugin_cfg_path: String = folder.path_join("plugin.cfg")
|
|
34
|
+
var name: String = folder.trim_prefix(addons_path)
|
|
35
|
+
var enabled: bool = plugin_cfg_path in enabled_paths
|
|
36
|
+
var plugin_info: Dictionary = {"name": name, "path": folder, "enabled": enabled}
|
|
37
|
+
|
|
38
|
+
var plugin_config: ConfigFile = ConfigFile.new()
|
|
39
|
+
if plugin_config.load(plugin_cfg_path) == OK:
|
|
40
|
+
plugin_info["display_name"] = plugin_config.get_value("plugin", "name", name)
|
|
41
|
+
plugin_info["description"] = plugin_config.get_value("plugin", "description", "")
|
|
42
|
+
plugin_info["author"] = plugin_config.get_value("plugin", "author", "")
|
|
43
|
+
plugin_info["version"] = plugin_config.get_value("plugin", "version", "")
|
|
44
|
+
plugin_info["script"] = plugin_config.get_value("plugin", "script", "")
|
|
45
|
+
|
|
46
|
+
plugins.append(plugin_info)
|
|
47
|
+
if enabled:
|
|
48
|
+
enabled_count += 1
|
|
49
|
+
|
|
50
|
+
var answer: Dictionary = {
|
|
64
51
|
"plugins": plugins,
|
|
65
52
|
"addons_directory_exists": true,
|
|
66
53
|
"enabled_count": enabled_count,
|
|
67
54
|
"disabled_count": plugins.size() - enabled_count
|
|
68
55
|
}
|
|
56
|
+
# Enabled in the project with no plugin.cfg where the entry points: the editor reports each one
|
|
57
|
+
# as an error at every start and loads nothing for it.
|
|
58
|
+
var missing: Array[String] = []
|
|
59
|
+
for path: String in enabled_paths:
|
|
60
|
+
if not FileAccess.file_exists(path):
|
|
61
|
+
missing.append(path)
|
|
62
|
+
if not missing.is_empty():
|
|
63
|
+
answer["enabled_but_missing"] = missing
|
|
64
|
+
return answer
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Every folder under [param root] holding a plugin.cfg, at any depth, the way the editor's plugin
|
|
68
|
+
# list finds them: a folder with one is a plugin and is not looked inside, and one without is
|
|
69
|
+
# searched. Only the top level was read, so a plugin one folder down was never listed.
|
|
70
|
+
func _plugin_folders(root: String) -> Array[String]:
|
|
71
|
+
var found: Array[String] = []
|
|
72
|
+
var dir: DirAccess = DirAccess.open(root)
|
|
73
|
+
if dir == null:
|
|
74
|
+
return found
|
|
75
|
+
for folder_name: String in dir.get_directories():
|
|
76
|
+
if folder_name.begins_with("."):
|
|
77
|
+
continue
|
|
78
|
+
var folder: String = root.path_join(folder_name)
|
|
79
|
+
if FileAccess.file_exists(folder.path_join("plugin.cfg")):
|
|
80
|
+
found.append(folder)
|
|
81
|
+
else:
|
|
82
|
+
found.append_array(_plugin_folders(folder))
|
|
83
|
+
return found
|
|
69
84
|
|
|
70
85
|
|
|
71
86
|
# Enable a plugin
|
|
@@ -79,19 +94,19 @@ func enable_plugin(params: Dictionary) -> Dictionary:
|
|
|
79
94
|
_log.error("Plugin not found: " + plugin_name)
|
|
80
95
|
return _log.failure("Expected plugin.cfg at: " + plugin_cfg_path)
|
|
81
96
|
|
|
82
|
-
var
|
|
97
|
+
var enabled_paths: Array[String] = _enabled_paths()
|
|
83
98
|
|
|
84
|
-
if
|
|
99
|
+
if plugin_cfg_path in enabled_paths:
|
|
85
100
|
return {
|
|
86
101
|
"plugin_name": plugin_name, "action": "already_enabled", "message": "Plugin is already enabled"
|
|
87
102
|
}
|
|
88
103
|
|
|
89
|
-
|
|
90
|
-
var err: Error =
|
|
104
|
+
enabled_paths.append(plugin_cfg_path)
|
|
105
|
+
var err: Error = _save_enabled_paths(enabled_paths)
|
|
91
106
|
if err != OK:
|
|
92
107
|
return _log.failure("Failed to save project.godot: " + error_string(err))
|
|
93
108
|
|
|
94
|
-
return {"plugin_name": plugin_name, "action": "enabled", "enabled_plugins":
|
|
109
|
+
return {"plugin_name": plugin_name, "action": "enabled", "enabled_plugins": _names_of(enabled_paths)}
|
|
95
110
|
|
|
96
111
|
|
|
97
112
|
# Disable a plugin
|
|
@@ -100,42 +115,50 @@ func disable_plugin(params: Dictionary) -> Dictionary:
|
|
|
100
115
|
|
|
101
116
|
_log.info("Disabling plugin: " + plugin_name)
|
|
102
117
|
|
|
103
|
-
var
|
|
118
|
+
var enabled_paths: Array[String] = _enabled_paths()
|
|
119
|
+
var plugin_cfg_path: String = "res://addons/" + plugin_name + "/plugin.cfg"
|
|
104
120
|
|
|
105
|
-
if not
|
|
121
|
+
if not plugin_cfg_path in enabled_paths:
|
|
106
122
|
return {
|
|
107
123
|
"plugin_name": plugin_name,
|
|
108
124
|
"action": "already_disabled",
|
|
109
125
|
"message": "Plugin is not currently enabled"
|
|
110
126
|
}
|
|
111
127
|
|
|
112
|
-
|
|
113
|
-
var err: Error =
|
|
128
|
+
enabled_paths.erase(plugin_cfg_path)
|
|
129
|
+
var err: Error = _save_enabled_paths(enabled_paths)
|
|
114
130
|
if err != OK:
|
|
115
131
|
return _log.failure("Failed to save project.godot: " + error_string(err))
|
|
116
132
|
|
|
117
|
-
return {"plugin_name": plugin_name, "action": "disabled", "enabled_plugins":
|
|
133
|
+
return {"plugin_name": plugin_name, "action": "disabled", "enabled_plugins": _names_of(enabled_paths)}
|
|
118
134
|
|
|
119
135
|
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
136
|
+
# Every entry in [editor_plugins], as the plugin.cfg path the editor stores. Paths rather than
|
|
137
|
+
# folder names, because the list is written back whole: reading names off a pattern for
|
|
138
|
+
# `res://addons/<name>/plugin.cfg` dropped a plugin one folder down, and the next enable or disable
|
|
139
|
+
# wrote the list without it, disabling a plugin nobody named. The editor stores the engine
|
|
140
|
+
# expression PackedStringArray(...), which loads as that type; the String form is what an older
|
|
141
|
+
# hand-edited file can carry.
|
|
142
|
+
func _enabled_paths() -> Array[String]:
|
|
143
|
+
var paths: Array[String] = []
|
|
127
144
|
var enabled_value: Variant = ProjectSettings.get_setting("editor_plugins/enabled", PackedStringArray())
|
|
128
145
|
if enabled_value is PackedStringArray:
|
|
129
|
-
var
|
|
130
|
-
|
|
131
|
-
var m: RegExMatch = regex.search(path)
|
|
132
|
-
if m:
|
|
133
|
-
names.append(m.get_string(1))
|
|
146
|
+
var packed: PackedStringArray = enabled_value
|
|
147
|
+
paths.assign(packed)
|
|
134
148
|
elif enabled_value is String:
|
|
135
149
|
var listed: String = enabled_value
|
|
136
|
-
for m: RegExMatch in
|
|
137
|
-
|
|
150
|
+
for m: RegExMatch in Patterns.compiled("res://[^\"',)]+?plugin\\.cfg").search_all(listed):
|
|
151
|
+
paths.append(m.get_string())
|
|
152
|
+
return paths
|
|
138
153
|
|
|
154
|
+
|
|
155
|
+
# The names callers use for [param paths]: the folder under addons/, or the path itself for an
|
|
156
|
+
# entry somewhere else.
|
|
157
|
+
static func _names_of(paths: Array[String]) -> Array[String]:
|
|
158
|
+
var names: Array[String] = []
|
|
159
|
+
for path: String in paths:
|
|
160
|
+
var inside: bool = path.begins_with("res://addons/") and path.ends_with("/plugin.cfg")
|
|
161
|
+
names.append(path.trim_prefix("res://addons/").trim_suffix("/plugin.cfg") if inside else path)
|
|
139
162
|
return names
|
|
140
163
|
|
|
141
164
|
|
|
@@ -145,19 +168,14 @@ func _enabled_plugin_names() -> Array[String]:
|
|
|
145
168
|
# the unquoted expression the editor reads, whereas the same text handed over as a String is
|
|
146
169
|
# written quoted and loads as a String the editor does not recognise as a plugin list. An
|
|
147
170
|
# empty list is the setting removed, which is what the editor writes for no plugins.
|
|
148
|
-
func
|
|
149
|
-
if
|
|
171
|
+
func _save_enabled_paths(paths: Array[String]) -> Error:
|
|
172
|
+
if paths.is_empty():
|
|
150
173
|
ProjectSettings.set_setting("editor_plugins/enabled", null)
|
|
151
174
|
return ProjectSettings.save()
|
|
152
175
|
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
var enabled_paths: Array[String] = []
|
|
159
|
-
for name: String in names:
|
|
160
|
-
enabled_paths.append("res://addons/" + name + "/plugin.cfg")
|
|
161
|
-
|
|
162
|
-
ProjectSettings.set_setting("editor_plugins/enabled", PackedStringArray(enabled_paths))
|
|
176
|
+
# Kept in an Array[String] and converted here, because appending to a packed array answers with
|
|
177
|
+
# whether it worked and a project holding return_value_discarded at error level will not compile
|
|
178
|
+
# a script that drops that answer. The conversion is what matters: an Array[String] handed to
|
|
179
|
+
# set_setting is written as Array[String]([...]), which the editor does not read as a plugin list.
|
|
180
|
+
ProjectSettings.set_setting("editor_plugins/enabled", PackedStringArray(paths))
|
|
163
181
|
return ProjectSettings.save()
|
|
@@ -34,6 +34,19 @@ func get_project_setting(params: Dictionary) -> Dictionary:
|
|
|
34
34
|
result["value"] = null
|
|
35
35
|
result["message"] = "Setting does not exist"
|
|
36
36
|
|
|
37
|
+
# The value under this name is not the one every build reads: `name.windows` replaces it on
|
|
38
|
+
# Windows, `name.editor` in the editor. Read alone, a caller saw the plain value and not the one
|
|
39
|
+
# their game showed.
|
|
40
|
+
var overrides: Dictionary = {}
|
|
41
|
+
for info: Dictionary in ProjectSettings.get_property_list():
|
|
42
|
+
var name: String = str(info.get("name", ""))
|
|
43
|
+
if name.begins_with(setting_path + "."):
|
|
44
|
+
overrides[name.substr(setting_path.length() + 1)] = _values.serialize_value(
|
|
45
|
+
ProjectSettings.get_setting(name)
|
|
46
|
+
)
|
|
47
|
+
if not overrides.is_empty():
|
|
48
|
+
result["overrides"] = overrides
|
|
49
|
+
|
|
37
50
|
return result
|
|
38
51
|
|
|
39
52
|
|
|
@@ -139,7 +152,19 @@ func _converts_faithfully(value: Variant, to: int) -> bool:
|
|
|
139
152
|
func add_autoload(params: Dictionary) -> Dictionary:
|
|
140
153
|
var name: String = str(params.get("name", ""))
|
|
141
154
|
var path: String = str(params.get("path", ""))
|
|
142
|
-
var
|
|
155
|
+
var global: bool = Read.as_bool(params.get("global", true), true)
|
|
156
|
+
|
|
157
|
+
# There is no disabled autoload in Godot 4: every entry in the list is loaded under the root at
|
|
158
|
+
# every start. `enabled: false` used to write the entry without its asterisk and answer that it
|
|
159
|
+
# was disabled, and the autoload ran all the same.
|
|
160
|
+
if not Read.as_bool(params.get("enabled", true), true):
|
|
161
|
+
return _log.failure(
|
|
162
|
+
(
|
|
163
|
+
"An autoload cannot be added disabled: Godot loads every autoload in the list at every"
|
|
164
|
+
+ " start. global: false keeps its name out of the global scope and still loads it;"
|
|
165
|
+
+ " remove_autoload is what stops it loading"
|
|
166
|
+
)
|
|
167
|
+
)
|
|
143
168
|
|
|
144
169
|
if not path.begins_with("res://"):
|
|
145
170
|
path = "res://" + path
|
|
@@ -154,14 +179,21 @@ func add_autoload(params: Dictionary) -> Dictionary:
|
|
|
154
179
|
var setting: String = "autoload/" + name
|
|
155
180
|
var was_updated: bool = ProjectSettings.has_setting(setting)
|
|
156
181
|
|
|
157
|
-
# An asterisk in front of the path makes the autoload a global
|
|
158
|
-
# node still comes up under the root; only the name
|
|
159
|
-
|
|
182
|
+
# An asterisk in front of the path makes the autoload's name a global variable, the editor's
|
|
183
|
+
# "Global Variable" column. Without it the node still comes up under the root; only the name
|
|
184
|
+
# stops resolving.
|
|
185
|
+
ProjectSettings.set_setting(setting, ("*" if global else "") + path)
|
|
160
186
|
var err: Error = ProjectSettings.save()
|
|
161
187
|
if err != OK:
|
|
162
188
|
return _log.failure("Failed to save project.godot: " + error_string(err))
|
|
163
189
|
|
|
164
|
-
return {
|
|
190
|
+
return {
|
|
191
|
+
"name": name,
|
|
192
|
+
"path": path,
|
|
193
|
+
"enabled": true,
|
|
194
|
+
"global": global,
|
|
195
|
+
"action": "updated" if was_updated else "added",
|
|
196
|
+
}
|
|
165
197
|
|
|
166
198
|
|
|
167
199
|
# Remove an autoload singleton
|
|
@@ -196,10 +228,13 @@ func list_autoloads(_params: Dictionary) -> Dictionary:
|
|
|
196
228
|
var value: String = str(ProjectSettings.get_setting(setting))
|
|
197
229
|
var path: String = value.trim_prefix("*")
|
|
198
230
|
|
|
231
|
+
# Enabled whatever the asterisk says, because every autoload listed is loaded; the asterisk
|
|
232
|
+
# is whether its name is a global variable.
|
|
199
233
|
var autoload_info: Dictionary = {
|
|
200
234
|
"name": setting.trim_prefix("autoload/"),
|
|
201
235
|
"path": path,
|
|
202
|
-
"enabled":
|
|
236
|
+
"enabled": true,
|
|
237
|
+
"global": value.begins_with("*"),
|
|
203
238
|
"file_exists": FileAccess.file_exists(path),
|
|
204
239
|
}
|
|
205
240
|
autoloads.append(autoload_info)
|
|
@@ -25,11 +25,9 @@ func get_project_health(params: Dictionary) -> Dictionary:
|
|
|
25
25
|
|
|
26
26
|
# 1. Project Structure Check
|
|
27
27
|
if "structure" in check_categories:
|
|
28
|
-
var passed: bool = true
|
|
29
28
|
var details: Array[String] = []
|
|
30
29
|
|
|
31
30
|
if not FileAccess.file_exists("res://project.godot"):
|
|
32
|
-
passed = false
|
|
33
31
|
details.append("Missing project.godot file")
|
|
34
32
|
deductions += 30
|
|
35
33
|
|
|
@@ -43,7 +41,7 @@ func get_project_health(params: Dictionary) -> Dictionary:
|
|
|
43
41
|
details.append("Consider organizing with directories: " + ", ".join(missing_dirs))
|
|
44
42
|
deductions += 2 * missing_dirs.size()
|
|
45
43
|
|
|
46
|
-
checks["structure"] = {"name": "Project Structure", "passed":
|
|
44
|
+
checks["structure"] = {"name": "Project Structure", "passed": details.is_empty(), "details": details}
|
|
47
45
|
|
|
48
46
|
# 2. Resource Check
|
|
49
47
|
if "resources" in check_categories:
|
|
@@ -64,7 +62,10 @@ func get_project_health(params: Dictionary) -> Dictionary:
|
|
|
64
62
|
deductions += missing_imports
|
|
65
63
|
|
|
66
64
|
checks["resources"] = {
|
|
67
|
-
"name": "Resources",
|
|
65
|
+
"name": "Resources",
|
|
66
|
+
"passed": details.is_empty(),
|
|
67
|
+
"details": details,
|
|
68
|
+
"total_resources": resources.size()
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
# 3. Scripts Check
|
|
@@ -95,20 +96,27 @@ func get_project_health(params: Dictionary) -> Dictionary:
|
|
|
95
96
|
deductions += empty_functions
|
|
96
97
|
|
|
97
98
|
checks["scripts"] = {
|
|
98
|
-
"name": "Scripts",
|
|
99
|
+
"name": "Scripts",
|
|
100
|
+
"passed": details.is_empty(),
|
|
101
|
+
"details": details,
|
|
102
|
+
"total_scripts": script_files.size()
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
# 4. Scenes Check
|
|
102
106
|
if "scenes" in check_categories:
|
|
103
107
|
var details: Array[String] = []
|
|
104
108
|
|
|
105
|
-
|
|
109
|
+
# Binary scenes too, which a project may save instead and which counted as none.
|
|
110
|
+
var scene_files: Array[String] = _files.find_files_with_extensions("res://", ["tscn", "scn"])
|
|
106
111
|
if scene_files.size() == 0:
|
|
107
112
|
details.append("No scene files found in project")
|
|
108
113
|
deductions += 5
|
|
109
114
|
|
|
110
115
|
checks["scenes"] = {
|
|
111
|
-
"name": "Scenes",
|
|
116
|
+
"name": "Scenes",
|
|
117
|
+
"passed": details.is_empty(),
|
|
118
|
+
"details": details,
|
|
119
|
+
"total_scenes": scene_files.size()
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
# 5. Configuration Check
|
|
@@ -119,8 +127,8 @@ func get_project_health(params: Dictionary) -> Dictionary:
|
|
|
119
127
|
if main_scene.is_empty():
|
|
120
128
|
details.append("No main scene configured")
|
|
121
129
|
deductions += 10
|
|
122
|
-
elif not
|
|
123
|
-
details.append("Main scene
|
|
130
|
+
elif not FileWalk.missing_reason(main_scene).is_empty():
|
|
131
|
+
details.append("Main scene " + FileWalk.missing_reason(main_scene))
|
|
124
132
|
deductions += 15
|
|
125
133
|
|
|
126
134
|
var project_name: String = str(ProjectSettings.get_setting("application/config/name", ""))
|
|
@@ -132,7 +140,7 @@ func get_project_health(params: Dictionary) -> Dictionary:
|
|
|
132
140
|
details.append("No export presets configured")
|
|
133
141
|
deductions += 3
|
|
134
142
|
|
|
135
|
-
checks["config"] = {"name": "Configuration", "passed":
|
|
143
|
+
checks["config"] = {"name": "Configuration", "passed": details.is_empty(), "details": details}
|
|
136
144
|
|
|
137
145
|
var score: int = maxi(0, 100 - deductions)
|
|
138
146
|
|
|
@@ -154,6 +162,14 @@ func get_project_health(params: Dictionary) -> Dictionary:
|
|
|
154
162
|
if not FileAccess.file_exists("res://export_presets.cfg"):
|
|
155
163
|
recommendations.append("Configure export presets for your target platforms")
|
|
156
164
|
|
|
165
|
+
# Every detail costs points, so each is an issue; `issues` was always empty beside them, and a
|
|
166
|
+
# category said passed next to the failure it had just found.
|
|
167
|
+
var issues: Array[Dictionary] = []
|
|
168
|
+
for category: String in checks:
|
|
169
|
+
var check: Dictionary = checks[category]
|
|
170
|
+
var found: Array[String] = check["details"]
|
|
171
|
+
for detail: String in found:
|
|
172
|
+
issues.append({"check": category, "detail": detail})
|
|
157
173
|
return {
|
|
158
|
-
"score": score, "grade": grade, "checks": checks, "issues":
|
|
174
|
+
"score": score, "grade": grade, "checks": checks, "issues": issues, "recommendations": recommendations
|
|
159
175
|
}
|
|
@@ -52,6 +52,23 @@ static func as_float(value: Variant, fallback: float = 0.0) -> float:
|
|
|
52
52
|
if value is String:
|
|
53
53
|
var text: String = value
|
|
54
54
|
return text.to_float()
|
|
55
|
+
if value is Dictionary:
|
|
56
|
+
var tagged: Dictionary = value
|
|
57
|
+
if str(tagged.get("_type", "")) == "float":
|
|
58
|
+
return non_finite(str(tagged.get("value", "")), fallback)
|
|
59
|
+
return fallback
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## The float a non-finite tag names, "inf", "-inf" or "nan", or [param fallback] for anything else.
|
|
63
|
+
## JSON has no spelling for these, so the serialiser tags them rather than let them arrive as null.
|
|
64
|
+
static func non_finite(word: String, fallback: float = 0.0) -> float:
|
|
65
|
+
match word:
|
|
66
|
+
"inf":
|
|
67
|
+
return INF
|
|
68
|
+
"-inf":
|
|
69
|
+
return -INF
|
|
70
|
+
"nan":
|
|
71
|
+
return NAN
|
|
55
72
|
return fallback
|
|
56
73
|
|
|
57
74
|
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
3
|
const Log = preload("logger.gd")
|
|
4
|
+
const Patterns = preload("patterns.gd")
|
|
5
|
+
|
|
6
|
+
## Where the engine keeps a file's UID, by what kind of file it is. Measured on 4.7.2: a script or
|
|
7
|
+
## shader has a `.uid` file beside it, an imported file has it in its `.import`, and a text scene or
|
|
8
|
+
## resource has it in its own header. `ResourceLoader.get_resource_uid` answers the first two in a
|
|
9
|
+
## headless run and not the third, so each place is read here rather than asking it.
|
|
10
|
+
const SIDECAR_KINDS: Array[String] = ["gd", "gdshader", "gdshaderinc", "cs"]
|
|
11
|
+
const TEXT_RESOURCE_KINDS: Array[String] = ["tscn", "tres", "escn"]
|
|
4
12
|
|
|
5
13
|
var _log: Log
|
|
6
14
|
|
|
@@ -9,7 +17,7 @@ func _init(p_log: Log) -> void:
|
|
|
9
17
|
_log = p_log
|
|
10
18
|
|
|
11
19
|
|
|
12
|
-
# The UID the engine gave a file, from
|
|
20
|
+
# The UID the engine gave a file, read from wherever that kind of file keeps it.
|
|
13
21
|
func get_uid(params: Dictionary) -> Dictionary:
|
|
14
22
|
var file_path: String = str(params.get("resource_path", ""))
|
|
15
23
|
if file_path.is_empty():
|
|
@@ -23,19 +31,54 @@ func get_uid(params: Dictionary) -> Dictionary:
|
|
|
23
31
|
if not FileAccess.file_exists(file_path):
|
|
24
32
|
return _log.failure("File does not exist: " + file_path)
|
|
25
33
|
|
|
26
|
-
var
|
|
27
|
-
var
|
|
28
|
-
if
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
var answer: Dictionary = {"file": file_path, "absolute_path": absolute_path}
|
|
35
|
+
var found: Dictionary = uid_of(file_path)
|
|
36
|
+
if found.is_empty():
|
|
37
|
+
answer["exists"] = false
|
|
38
|
+
answer["message"] = _why_none(file_path)
|
|
39
|
+
return answer
|
|
40
|
+
answer["exists"] = true
|
|
41
|
+
answer.merge(found)
|
|
42
|
+
return answer
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# The UID and where it was read, or empty when the file has none anywhere the engine keeps one.
|
|
46
|
+
static func uid_of(file_path: String) -> Dictionary:
|
|
47
|
+
var sidecar: String = file_path + ".uid"
|
|
48
|
+
if FileAccess.file_exists(sidecar):
|
|
49
|
+
var text: String = FileAccess.get_file_as_string(sidecar).strip_edges()
|
|
50
|
+
if not text.is_empty():
|
|
51
|
+
return {"uid": text, "from": sidecar}
|
|
52
|
+
var import_file: String = file_path + ".import"
|
|
53
|
+
if FileAccess.file_exists(import_file):
|
|
54
|
+
var config: ConfigFile = ConfigFile.new()
|
|
55
|
+
if config.load(import_file) == OK and config.has_section_key("remap", "uid"):
|
|
56
|
+
return {"uid": str(config.get_value("remap", "uid")), "from": import_file}
|
|
57
|
+
if file_path.get_extension().to_lower() in TEXT_RESOURCE_KINDS:
|
|
58
|
+
var file: FileAccess = FileAccess.open(file_path, FileAccess.READ)
|
|
59
|
+
if file != null:
|
|
60
|
+
var header: String = file.get_line()
|
|
61
|
+
file.close()
|
|
62
|
+
var matched: RegExMatch = Patterns.compiled('\\buid="(uid://[^"]+)"').search(header)
|
|
63
|
+
if matched != null:
|
|
64
|
+
return {"uid": matched.get_string(1), "from": "the file's header"}
|
|
65
|
+
var id: int = ResourceLoader.get_resource_uid(file_path)
|
|
66
|
+
if id != ResourceUID.INVALID_ID:
|
|
67
|
+
return {"uid": ResourceUID.id_to_text(id), "from": "the engine"}
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Why [param file_path] has no UID, and what gives it one, which depends on what kind of file it is:
|
|
72
|
+
# refresh_uids writes a .uid for scripts and shaders only, and was named for every file.
|
|
73
|
+
func _why_none(file_path: String) -> String:
|
|
74
|
+
var kind: String = file_path.get_extension().to_lower()
|
|
75
|
+
if kind in SIDECAR_KINDS:
|
|
76
|
+
return "No .uid file beside it. refresh_uids writes one for every script and shader."
|
|
77
|
+
if kind in TEXT_RESOURCE_KINDS:
|
|
78
|
+
return (
|
|
79
|
+
"Its header carries no uid. The editor writes one when it saves the file; refresh_uids does not,"
|
|
80
|
+
+ " and a headless save does not either."
|
|
81
|
+
)
|
|
82
|
+
if ResourceLoader.exists(file_path):
|
|
83
|
+
return "The engine keeps no UID for it."
|
|
84
|
+
return "It has not been imported, and the import is what gives it a UID: project_import reimport imports it."
|