gdharness 1.3.8 → 1.3.10

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 (78) hide show
  1. package/build/cli.js +4668 -3229
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
  7. package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
  8. package/build/godot/addons/gdharness_editor/reading.gd +112 -6
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
  10. package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
  11. package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
  13. package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
  14. package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
  17. package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
  18. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  20. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  23. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
  25. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  26. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
  27. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  28. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
  29. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
  30. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  31. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
  32. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  33. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  35. package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
  36. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  37. package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
  38. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  39. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
  40. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
  41. package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
  42. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  43. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  44. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
  45. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
  46. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  47. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
  49. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  50. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  51. package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
  52. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  53. package/build/godot/operations/animation_reads.gd +115 -4
  54. package/build/godot/operations/audio_buses.gd +93 -7
  55. package/build/godot/operations/classdb_queries.gd +119 -7
  56. package/build/godot/operations/declared_values.gd +272 -0
  57. package/build/godot/operations/dependencies.gd +154 -41
  58. package/build/godot/operations/editor_tools.gd +29 -13
  59. package/build/godot/operations/export_presets.gd +602 -4
  60. package/build/godot/operations/file_reads.gd +115 -18
  61. package/build/godot/operations/gdscript_analysis.gd +14 -0
  62. package/build/godot/operations/gdscript_authoring.gd +12 -2
  63. package/build/godot/operations/godot_operations.gd +18 -2
  64. package/build/godot/operations/import_pipeline.gd +20 -221
  65. package/build/godot/operations/input_actions.gd +127 -51
  66. package/build/godot/operations/project_config.gd +113 -68
  67. package/build/godot/operations/project_diagnostics.gd +4 -0
  68. package/build/godot/operations/project_validation.gd +266 -0
  69. package/build/godot/operations/property_paths.gd +89 -16
  70. package/build/godot/operations/property_values.gd +180 -121
  71. package/build/godot/operations/reading.gd +112 -6
  72. package/build/godot/operations/scene_connections.gd +222 -0
  73. package/build/godot/operations/serialisation.gd +19 -29
  74. package/build/godot/operations/setting_reads.gd +121 -0
  75. package/build/godot/operations/value_shapes.gd +128 -1
  76. package/build/godot/tests/listed.gd +20 -13
  77. package/build/keeper.js +1402 -707
  78. package/package.json +1 -1
@@ -0,0 +1,266 @@
1
+ extends RefCounted
2
+
3
+ ## Whether a project is ready to run and export: its settings, main scene, imports, presets,
4
+ ## scripts, references and signal connections, each named under checks_performed.
5
+
6
+ const GdhDependencies = preload("dependencies.gd")
7
+ const GdhExportPresets = preload("export_presets.gd")
8
+ const GdhFileWalk = preload("file_walk.gd")
9
+ const GdhImportPipeline = preload("import_pipeline.gd")
10
+ const GdhLog = preload("logger.gd")
11
+ const GdhRead = preload("reading.gd")
12
+ const GdhSceneConnections = preload("scene_connections.gd")
13
+ const GdhScriptCheck = preload("script_check.gd")
14
+ const CONNECTION_FIX: String = (
15
+ "Write the method or signal, or point the connection at the one that replaced it with" + " scene_signal"
16
+ )
17
+
18
+ var _log: GdhLog
19
+ var _files: GdhFileWalk = GdhFileWalk.new()
20
+
21
+
22
+ func _init(p_log: GdhLog) -> void:
23
+ _log = p_log
24
+
25
+
26
+ func validate_project(params: Dictionary) -> Dictionary:
27
+ var preset_name: String = str(params.get("preset", ""))
28
+ var include_suggestions: bool = GdhRead.as_bool(params.get("include_suggestions", true), true)
29
+
30
+ _log.info("Validating project" + (" for preset: " + preset_name if not preset_name.is_empty() else ""))
31
+
32
+ var issues: Array[Dictionary] = []
33
+ var warnings: Array[Dictionary] = []
34
+ var checks_performed: Array[String] = []
35
+
36
+ checks_performed.append("project_file")
37
+ if not FileAccess.file_exists("res://project.godot"):
38
+ issues.append(
39
+ _finding(
40
+ "error",
41
+ "project_file",
42
+ "project.godot not found",
43
+ "Ensure you are running this from a valid Godot project directory",
44
+ include_suggestions
45
+ )
46
+ )
47
+
48
+ checks_performed.append("main_scene")
49
+ var main_scene: String = str(ProjectSettings.get_setting("application/run/main_scene", ""))
50
+ if main_scene.is_empty():
51
+ issues.append(
52
+ _finding(
53
+ "error",
54
+ "main_scene",
55
+ "No main scene set",
56
+ "Set a main scene in Project Settings > Application > Run > Main Scene",
57
+ include_suggestions
58
+ )
59
+ )
60
+ elif not GdhFileWalk.missing_reason(main_scene).is_empty():
61
+ issues.append(
62
+ _finding(
63
+ "error",
64
+ "main_scene",
65
+ "Main scene " + GdhFileWalk.missing_reason(main_scene),
66
+ "Update the main scene setting or create the missing scene file",
67
+ include_suggestions
68
+ )
69
+ )
70
+
71
+ # A file never imported is on disk, so the reference checks pass it, and still nothing loads it:
72
+ # a scene using one fails to load in a run or an export.
73
+ checks_performed.append("imports")
74
+ var never: Array[String] = []
75
+ for path: String in _files.find_files_with_extensions("res://", GdhImportPipeline.IMPORTABLE_EXTENSIONS):
76
+ if not FileAccess.file_exists(path + ".import"):
77
+ never.append(path)
78
+ if not never.is_empty():
79
+ never.sort()
80
+ warnings.append(
81
+ _finding(
82
+ "warning",
83
+ "imports",
84
+ GdhImportPipeline.never_imported_line(never),
85
+ "Run project_import reimport, or open the project in the editor, whose scan imports them",
86
+ include_suggestions
87
+ )
88
+ )
89
+
90
+ checks_performed.append("export_presets")
91
+ if not FileAccess.file_exists("res://export_presets.cfg"):
92
+ warnings.append(
93
+ _finding(
94
+ "warning",
95
+ "export_presets",
96
+ "No export presets configured",
97
+ "Add a preset with project_export add_preset, or in the editor: Project > Export",
98
+ include_suggestions
99
+ )
100
+ )
101
+
102
+ # The preset asked about is looked for rather than only logged: validating "for" a preset that
103
+ # does not exist answered valid, as though it had been checked.
104
+ if not preset_name.is_empty():
105
+ checks_performed.append("export_preset")
106
+ var named: Array[String] = GdhExportPresets.names()
107
+ if preset_name not in named:
108
+ issues.append(
109
+ _finding(
110
+ "error",
111
+ "export_preset",
112
+ (
113
+ "No export preset is named "
114
+ + preset_name
115
+ + (
116
+ ": the project has none"
117
+ if named.is_empty()
118
+ else ": the presets are " + ", ".join(named)
119
+ )
120
+ ),
121
+ "Pass one of the preset names, or add the preset with project_export add_preset",
122
+ include_suggestions
123
+ )
124
+ )
125
+ else:
126
+ var gone: Array[String] = GdhExportPresets.missing_named(preset_name)
127
+ if not gone.is_empty():
128
+ warnings.append(
129
+ _finding(
130
+ "warning",
131
+ "export_preset_files",
132
+ "The preset %s names files that are not there: %s" % [preset_name, "; ".join(gone)],
133
+ "Add the files, or point the options at others with project_export set_preset",
134
+ include_suggestions
135
+ )
136
+ )
137
+
138
+ checks_performed.append("icon")
139
+ var icon_path: String = str(ProjectSettings.get_setting("application/config/icon", ""))
140
+ if icon_path.is_empty():
141
+ warnings.append(
142
+ _finding(
143
+ "warning",
144
+ "icon",
145
+ "No application icon set",
146
+ "Set an icon in Project Settings > Application > Config > Icon",
147
+ include_suggestions
148
+ )
149
+ )
150
+ elif not FileAccess.file_exists(icon_path):
151
+ warnings.append(
152
+ _finding(
153
+ "warning",
154
+ "icon",
155
+ "Icon file does not exist: " + icon_path,
156
+ "Update the icon path or add the missing icon file",
157
+ include_suggestions
158
+ )
159
+ )
160
+
161
+ checks_performed.append("project_name")
162
+ var project_name: String = str(ProjectSettings.get_setting("application/config/name", ""))
163
+ if project_name.is_empty():
164
+ warnings.append(
165
+ _finding(
166
+ "warning",
167
+ "project_name",
168
+ "No project name set",
169
+ "Set a project name in Project Settings > Application > Config > Name",
170
+ include_suggestions
171
+ )
172
+ )
173
+
174
+ checks_performed.append("scripts")
175
+ var script_files: Array[String] = _files.find_files_with_extensions("res://", ["gd"])
176
+ var scripts_checked: int = 0
177
+ var script_issues: Array[Dictionary] = []
178
+
179
+ # A hundred scripts is enough to say whether the project is tidy without a large one
180
+ # turning validation into a full read of its source tree.
181
+ for script_path: String in script_files.slice(0, 100):
182
+ scripts_checked += 1
183
+ var file: FileAccess = FileAccess.open(script_path, FileAccess.READ)
184
+ if file:
185
+ var content: String = file.get_as_text()
186
+ file.close()
187
+
188
+ if "# TODO" in content or "# FIXME" in content:
189
+ script_issues.append({"path": script_path, "issue": "Contains TODO/FIXME comments"})
190
+ if "pass # TODO" in content:
191
+ script_issues.append({"path": script_path, "issue": "Contains unimplemented functions"})
192
+
193
+ # Compiled by the server and handed in, as for the health check: a script the engine refuses
194
+ # stops the export's game at the first scene using it, which the comment check above never saw.
195
+ var compiled: Dictionary = params.get("compiled", {})
196
+ if compiled.has("checked"):
197
+ checks_performed.append("compiles")
198
+ var failures: Array = compiled.get("failures", [])
199
+ for failure: Dictionary in failures:
200
+ issues.append(
201
+ _finding(
202
+ "error",
203
+ "compiles",
204
+ GdhScriptCheck.failure_line(failure),
205
+ "script_diagnostics gives every error in it with its line",
206
+ include_suggestions
207
+ )
208
+ )
209
+
210
+ checks_performed.append("references")
211
+ for entry: Dictionary in GdhDependencies.new(_log).missing_references():
212
+ issues.append(
213
+ _finding(
214
+ "error",
215
+ "references",
216
+ GdhDependencies.missing_line(entry),
217
+ "Restore the file, or point the reference at the file that replaced it",
218
+ include_suggestions
219
+ )
220
+ )
221
+
222
+ # A connection whose method or signal is not there loads without a word and fails only once the
223
+ # game runs, on every emission or not at all; the references check sees files, not members.
224
+ checks_performed.append("connections")
225
+ for entry: Dictionary in GdhSceneConnections.new(_log).broken():
226
+ warnings.append(
227
+ _finding(
228
+ "warning",
229
+ "connections",
230
+ GdhSceneConnections.broken_line(entry),
231
+ CONNECTION_FIX,
232
+ include_suggestions
233
+ )
234
+ )
235
+
236
+ if script_issues.size() > 0:
237
+ var warning: Dictionary = _finding(
238
+ "warning",
239
+ "scripts",
240
+ str(script_issues.size()) + " script issues found",
241
+ "Review and resolve TODO/FIXME items before release",
242
+ include_suggestions
243
+ )
244
+ warning["details"] = script_issues.slice(0, 5)
245
+ warnings.append(warning)
246
+
247
+ return {
248
+ "valid": issues.is_empty(),
249
+ "issues": issues,
250
+ "warnings": warnings,
251
+ "checks_performed": checks_performed,
252
+ "scripts_checked": scripts_checked,
253
+ # Said beside the count, so a project of four hundred scripts is not read as checked whole.
254
+ "scripts_total": script_files.size(),
255
+ "issue_count": issues.size(),
256
+ "warning_count": warnings.size()
257
+ }
258
+
259
+
260
+ func _finding(
261
+ type: String, check: String, message: String, suggestion: String, include_suggestion: bool
262
+ ) -> Dictionary:
263
+ var finding: Dictionary = {"type": type, "check": check, "message": message}
264
+ if include_suggestion:
265
+ finding["suggestion"] = suggestion
266
+ return finding
@@ -26,38 +26,109 @@ static func declared_on(target: Object, property: String) -> Dictionary:
26
26
  ## start with the name of one holding a resource built into it, then a slash and a key of that
27
27
  ## resource: "sources/0/0:0/0/probability" is the probability of tile 0:0 in the atlas source a
28
28
  ## TileSet keeps as sources/0. A colon path cannot reach it, since the tile's own name holds colons.
29
- ## A resource kept in a file of its own is not reached, nor, given the file [param home] that
30
- ## [param target] is saved in, one built into another file, since writing it here would leave that
31
- ## file as it was.
29
+ ## A list of resources is stepped into by an element's index: "lines/1/text" is the text of the
30
+ ## second resource in lines. A resource kept in a file of its own is not reached, nor, given the
31
+ ## file [param home] that [param target] is saved in, one built into another file, since writing it
32
+ ## here would leave that file as it was.
32
33
  static func holder_of(target: Object, key: String, home: String = "") -> Array:
33
34
  var through: Array[Resource] = []
34
35
  var holder: Object = target
35
36
  var name: String = key
36
37
  while declared_on(holder, name).is_empty():
37
38
  var reached: Resource = null
39
+ var rest: int = -1
38
40
  var at: int = name.find("/")
39
41
  while at > 0:
40
- var held: Resource = built_in_at(holder, name.left(at), home)
41
- if held != null:
42
+ var stepped: Array = _stepped(holder, name, at)
43
+ var held: Resource = stepped[0]
44
+ if held != null and _built_into(held, home):
42
45
  reached = held
46
+ rest = GdhRead.as_int(stepped[1])
43
47
  break
44
48
  at = name.find("/", at + 1)
45
49
  if reached == null:
46
50
  break
47
51
  through.append(reached)
48
52
  holder = reached
49
- name = name.substr(at + 1)
53
+ name = name.substr(rest)
50
54
  return [holder, name, through]
51
55
 
52
56
 
53
- ## The resource [param target] holds under [param property] when it is built into a file, or null;
54
- ## given [param home], only one built into that file or into none yet.
55
- static func built_in_at(target: Object, property: String, home: String = "") -> Resource:
56
- var held: Resource = held_at(target, property)
57
- if held == null or not held.is_built_in():
58
- return null
57
+ ## The resource the part of [param name] before the slash at [param at] reaches on [param target],
58
+ ## as [resource, where the rest of [param name] starts], or [null, -1]: the one held under that key,
59
+ ## or the element of the list held there that the step after the slash indexes.
60
+ static func _stepped(target: Object, name: String, at: int) -> Array:
61
+ var head: String = name.left(at)
62
+ var held: Resource = held_at(target, head)
63
+ if held != null:
64
+ return [held, at + 1]
65
+ var end: int = name.find("/", at + 1)
66
+ if end < 0 or declared_on(target, head).is_empty():
67
+ return [null, -1]
68
+ var index: String = name.substr(at + 1, end - at - 1)
69
+ var listed: Variant = target.get(head)
70
+ if not listed is Array or not index.is_valid_int():
71
+ return [null, -1]
72
+ var items: Array = listed
73
+ var place: int = index.to_int()
74
+ if place < 0 or place >= items.size() or not items[place] is Resource:
75
+ return [null, -1]
76
+ return [items[place], end + 1]
77
+
78
+
79
+ ## The refusal for [param name], which [param target] does not declare, when a step of it indexes a
80
+ ## list [param target] holds and names no resource in it; or "".
81
+ static func unlisted(target: Object, name: String) -> String:
82
+ var at: int = name.find("/")
83
+ while at > 0:
84
+ var head: String = name.left(at)
85
+ var listed: Variant = null if declared_on(target, head).is_empty() else target.get(head)
86
+ if listed is Array:
87
+ var items: Array = listed
88
+ return _unlisted_in(items, "%s.%s" % [class_called(target), head], name, at)
89
+ at = name.find("/", at + 1)
90
+ return ""
91
+
92
+
93
+ static func _unlisted_in(items: Array, called: String, name: String, at: int) -> String:
94
+ var head: String = name.left(at)
95
+ var end: int = name.find("/", at + 1)
96
+ var index: String = name.substr(at + 1) if end < 0 else name.substr(at + 1, end - at - 1)
97
+ if items.is_empty():
98
+ return "%s holds no elements, so %s names none: give %s a whole list first." % [called, name, head]
99
+ var last: int = items.size() - 1
100
+ if not index.is_valid_int():
101
+ return (
102
+ "%s is a list, so the step after it is the index of one of its elements, from 0 to %d, not %s."
103
+ % [called, last, index]
104
+ )
105
+ var place: int = index.to_int()
106
+ if place < 0 or place > last:
107
+ return (
108
+ "%s holds %d element%s, so %s names none: an index goes from 0 to %d."
109
+ % [called, items.size(), "" if items.size() == 1 else "s", name, last]
110
+ )
111
+ if not items[place] is Resource:
112
+ return (
113
+ "%s/%d holds %s, which has no properties to write: give %s a whole list instead."
114
+ % [head, place, type_string(typeof(items[place])), head]
115
+ )
116
+ if end < 0:
117
+ return (
118
+ (
119
+ "%s/%d is an element of %s, which a path does not replace alone: give %s a whole list, or "
120
+ + "write a property of the element, as %s/%d/<property>."
121
+ )
122
+ % [head, place, called, head, head, place]
123
+ )
124
+ return ""
125
+
126
+
127
+ static func _built_into(held: Resource, home: String) -> bool:
128
+ if not held.is_built_in():
129
+ return false
59
130
  var file: String = held.resource_path.get_slice("::", 0)
60
- return held if home.is_empty() or file.is_empty() or file == home else null
131
+ return home.is_empty() or file.is_empty() or file == home
61
132
 
62
133
 
63
134
  ## The file [param target] is saved in: its scene's for a node, as the nodes of an instance are
@@ -184,9 +255,11 @@ static func class_called(target: Object) -> String:
184
255
  static func held_elsewhere(target: Object, name: String, home: String) -> String:
185
256
  var at: int = name.find("/")
186
257
  while at > 0:
187
- var head: String = name.left(at)
188
- var rest: String = name.substr(at + 1)
189
- var held: Resource = held_at(target, head)
258
+ var stepped: Array = _stepped(target, name, at)
259
+ var held: Resource = stepped[0]
260
+ var past: int = GdhRead.as_int(stepped[1])
261
+ var head: String = "" if held == null else name.left(past - 1)
262
+ var rest: String = "" if held == null else name.substr(past)
190
263
  var file: String = "" if held == null else held.resource_path.get_slice("::", 0)
191
264
  if held != null and not held.is_built_in():
192
265
  return (