gdharness 1.3.1 → 1.3.2
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 +311 -86
- package/build/godot/addons/gdharness_editor/declared_values.gd +116 -0
- package/build/godot/addons/gdharness_editor/declared_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +5 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +106 -10
- package/build/godot/addons/gdharness_editor/reading.gd +37 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +4 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +133 -39
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +9 -9
- package/build/godot/addons/gdharness_editor/value_shapes.gd +7 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +37 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +27 -1
- package/build/godot/addons/gdharness_runtime/serialisation.gd +4 -0
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +7 -0
- package/build/godot/operations/classdb_queries.gd +13 -3
- package/build/godot/operations/declared_values.gd +116 -0
- package/build/godot/operations/dependencies.gd +109 -5
- package/build/godot/operations/export_presets.gd +4 -6
- package/build/godot/operations/file_reads.gd +5 -0
- package/build/godot/operations/file_walk.gd +34 -0
- package/build/godot/operations/gdscript_analysis.gd +7 -2
- package/build/godot/operations/gdscript_authoring.gd +105 -2
- package/build/godot/operations/godot_operations.gd +2 -0
- package/build/godot/operations/import_pipeline.gd +39 -0
- package/build/godot/operations/input_actions.gd +73 -1
- package/build/godot/operations/project_config.gd +59 -3
- package/build/godot/operations/project_diagnostics.gd +27 -2
- package/build/godot/operations/property_values.gd +106 -10
- package/build/godot/operations/reading.gd +37 -0
- package/build/godot/operations/resource_files.gd +4 -1
- package/build/godot/operations/script_check.gd +16 -0
- package/build/godot/operations/serialisation.gd +4 -0
- package/build/godot/operations/value_shapes.gd +7 -0
- package/package.json +1 -1
|
@@ -75,7 +75,9 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
75
75
|
full_path = "res://" + full_path
|
|
76
76
|
|
|
77
77
|
if not FileAccess.file_exists(full_path):
|
|
78
|
-
return _log.failure(
|
|
78
|
+
return _log.failure(
|
|
79
|
+
_files.with_nearest_files("Resource file does not exist: " + full_path, full_path)
|
|
80
|
+
)
|
|
79
81
|
|
|
80
82
|
var walk: GdhDependencyWalk = GdhDependencyWalk.new(max_depth, include_built_in, circular_references)
|
|
81
83
|
dependencies[full_path] = _analyze_resource(full_path, 0, walk)
|
|
@@ -100,12 +102,19 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
100
102
|
|
|
101
103
|
var references: int = 0
|
|
102
104
|
var distinct: Dictionary = {}
|
|
105
|
+
var missing: Dictionary = {}
|
|
103
106
|
for key: String in dependencies:
|
|
104
107
|
# Through a typed local: what comes out of a Dictionary is Variant, and a project that
|
|
105
108
|
# errors on handing one to a typed parameter will not compile this script at all.
|
|
106
109
|
var walked: Array[Dictionary] = dependencies[key]
|
|
107
110
|
references += _count_recursive(walked)
|
|
108
111
|
_gather_paths(walked, key, distinct)
|
|
112
|
+
_gather_missing(walked, key, missing)
|
|
113
|
+
# Said in the summary as well as in the tree, where a file that is not there was one
|
|
114
|
+
# `exists: false` among the entries, which a caller reading the summary never met.
|
|
115
|
+
var missing_files: Array[Dictionary] = []
|
|
116
|
+
for path: String in missing:
|
|
117
|
+
missing_files.append({"path": path, "referenced_by": missing[path]})
|
|
109
118
|
|
|
110
119
|
return {
|
|
111
120
|
"dependencies": dependencies,
|
|
@@ -117,11 +126,54 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
117
126
|
# loads it, and counting its entries answered 11 for a scene with six dependencies.
|
|
118
127
|
"total_dependencies": distinct.size(),
|
|
119
128
|
"total_references": references,
|
|
120
|
-
"circular_count": circular_references.size()
|
|
129
|
+
"circular_count": circular_references.size(),
|
|
130
|
+
"missing": missing_files
|
|
121
131
|
}
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
|
|
135
|
+
## Every file a scene or resource of the project loads through an ext_resource that is not there,
|
|
136
|
+
## by its path or its uid, each with the files loading it. Only those: a script naming a path in
|
|
137
|
+
## its code may be asking whether the file exists, and the shipped addons check for files they
|
|
138
|
+
## write, so every project was told its addons referred to files that are missing. A script's
|
|
139
|
+
## preloads are the compile's to judge.
|
|
140
|
+
func missing_references() -> Array[Dictionary]:
|
|
141
|
+
var missing: Dictionary = {}
|
|
142
|
+
for file: String in _files.find_files_with_extensions("res://", GdhResourceFiles.TEXT_RESOURCE_KINDS):
|
|
143
|
+
var content: String = FileAccess.get_file_as_string(file)
|
|
144
|
+
var moved: Dictionary = _moved_by_uid(content)
|
|
145
|
+
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
|
|
146
|
+
var at: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(line.get_string())
|
|
147
|
+
if at == null or moved.has(at.get_string(1)):
|
|
148
|
+
continue
|
|
149
|
+
var path: String = _resolved(at.get_string(1), file)
|
|
150
|
+
if FileAccess.file_exists(path):
|
|
151
|
+
continue
|
|
152
|
+
var naming: Array = missing.get_or_add(path, [])
|
|
153
|
+
if not file in naming:
|
|
154
|
+
naming.append(file)
|
|
155
|
+
# Sorted, since the walk meets files in the order the filesystem lists them, which differs
|
|
156
|
+
# between platforms.
|
|
157
|
+
var paths: Array = missing.keys()
|
|
158
|
+
paths.sort()
|
|
159
|
+
var found: Array[Dictionary] = []
|
|
160
|
+
for path: String in paths:
|
|
161
|
+
var naming: Array = missing[path]
|
|
162
|
+
naming.sort()
|
|
163
|
+
found.append({"path": path, "referenced_by": naming})
|
|
164
|
+
return found
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
## One line for [param entry], one of [method missing_references].
|
|
168
|
+
static func missing_line(entry: Dictionary) -> String:
|
|
169
|
+
var referring: Array = entry.get("referenced_by", [])
|
|
170
|
+
var naming: PackedStringArray = PackedStringArray(referring)
|
|
171
|
+
return (
|
|
172
|
+
"%s does not exist, and %s %s to it"
|
|
173
|
+
% [str(entry.get("path", "")), ", ".join(naming), "refers" if naming.size() == 1 else "refer"]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
125
177
|
# What refers to a resource, and how: the scenes that instance it, the scripts that extend or
|
|
126
178
|
# preload it, and for a script with a class_name, every use of that name.
|
|
127
179
|
func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
@@ -130,8 +182,10 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
130
182
|
|
|
131
183
|
if not resource_path.begins_with("res://"):
|
|
132
184
|
resource_path = "res://" + resource_path
|
|
133
|
-
|
|
134
|
-
|
|
185
|
+
# A file that is gone is searched for all the same, since what still refers to it is what a
|
|
186
|
+
# caller asks about one, and the forward walk of a scene had just named it missing. Refused only
|
|
187
|
+
# when nothing refers to it either, which is a path written wrong.
|
|
188
|
+
var exists: bool = FileAccess.file_exists(resource_path)
|
|
135
189
|
|
|
136
190
|
_log.info("Finding usages of: " + resource_path)
|
|
137
191
|
|
|
@@ -140,7 +194,8 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
140
194
|
if not class_name_declared.is_empty():
|
|
141
195
|
by_class = GdhPatterns.compiled("\\b" + _regex_escaped(class_name_declared) + "\\b")
|
|
142
196
|
var target: Dictionary = {
|
|
143
|
-
"path": resource_path,
|
|
197
|
+
"path": resource_path,
|
|
198
|
+
"uid": str(GdhResourceFiles.uid_of(resource_path).get("uid", "")) if exists else ""
|
|
144
199
|
}
|
|
145
200
|
|
|
146
201
|
var all_files: Array[String] = _usage_files(file_types)
|
|
@@ -204,11 +259,19 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
204
259
|
usages.append({"file": file_path, "references": references})
|
|
205
260
|
total += references.size()
|
|
206
261
|
|
|
262
|
+
if not exists and usages.is_empty():
|
|
263
|
+
return _log.failure(
|
|
264
|
+
_files.with_nearest_files(
|
|
265
|
+
"Resource file does not exist: %s, and nothing in the project refers to it" % resource_path,
|
|
266
|
+
resource_path
|
|
267
|
+
)
|
|
268
|
+
)
|
|
207
269
|
var declared: Variant = null
|
|
208
270
|
if not class_name_declared.is_empty():
|
|
209
271
|
declared = class_name_declared
|
|
210
272
|
return {
|
|
211
273
|
"resource_path": resource_path,
|
|
274
|
+
"exists": exists,
|
|
212
275
|
"class_name": declared,
|
|
213
276
|
"usages": usages,
|
|
214
277
|
"summary":
|
|
@@ -357,6 +420,20 @@ func _gather_paths(deps: Array[Dictionary], root: String, into: Dictionary) -> v
|
|
|
357
420
|
_gather_paths(deeper, root, into)
|
|
358
421
|
|
|
359
422
|
|
|
423
|
+
# Each file in [param deps] that does not exist, into [param into] with the files naming it, once
|
|
424
|
+
# each however often the tree reaches them; [param parent] names the entries of [param deps].
|
|
425
|
+
func _gather_missing(deps: Array[Dictionary], parent: String, into: Dictionary) -> void:
|
|
426
|
+
for dep: Dictionary in deps:
|
|
427
|
+
var path: String = str(dep.get("path", ""))
|
|
428
|
+
if not dep.get("exists", true):
|
|
429
|
+
var naming: Array = into.get_or_add(path, [])
|
|
430
|
+
if not parent in naming:
|
|
431
|
+
naming.append(parent)
|
|
432
|
+
if dep.has("dependencies"):
|
|
433
|
+
var deeper: Array[Dictionary] = dep["dependencies"]
|
|
434
|
+
_gather_missing(deeper, path, into)
|
|
435
|
+
|
|
436
|
+
|
|
360
437
|
# A circular entry is not counted: it marks that the entry holding it was reached again, which that
|
|
361
438
|
# entry already counts as one reference.
|
|
362
439
|
func _count_recursive(deps: Array[Dictionary]) -> int:
|
|
@@ -404,14 +481,20 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
404
481
|
# about `["res://addons/x/plugin.cfg"]` was a dependency of every project with the addon.
|
|
405
482
|
if path.get_extension() == "gd":
|
|
406
483
|
content = _code_lines(content)
|
|
484
|
+
var moved: Dictionary = _moved_by_uid(content)
|
|
407
485
|
|
|
408
486
|
for pattern: String in REFERENCE_PATTERNS:
|
|
409
487
|
var regex: RegEx = GdhPatterns.compiled(pattern)
|
|
410
488
|
for m: RegExMatch in regex.search_all(content):
|
|
411
489
|
var dep_path: String = _resolved(_referenced_path(m.get_string()), path)
|
|
490
|
+
dep_path = str(moved.get(dep_path, dep_path))
|
|
412
491
|
|
|
413
492
|
if not dep_path.begins_with("res://"):
|
|
414
493
|
continue
|
|
494
|
+
# A literal naming a directory, or the start of a path a script builds as it runs
|
|
495
|
+
# ("res://levels/" + name), names no file, and walked as one it was a missing dependency.
|
|
496
|
+
if dep_path.get_extension().is_empty():
|
|
497
|
+
continue
|
|
415
498
|
|
|
416
499
|
# Skip engine-internal resources unless requested. `addons/` is not one of
|
|
417
500
|
# them: it is ordinary project content, and often shipping content, so
|
|
@@ -503,6 +586,27 @@ static func _resolved(written: String, from: String) -> String:
|
|
|
503
586
|
|
|
504
587
|
|
|
505
588
|
# The path inside a preload(), load() or ext_resource match; a bare res:// match is the path.
|
|
589
|
+
# The ext_resource paths in [param content] that nothing is at, each with the file its uid names
|
|
590
|
+
# instead. The engine loads an ext_resource by its uid first, so a file moved outside the editor
|
|
591
|
+
# still loads, and the walk called it missing; every pattern reads the old path, the bare res://
|
|
592
|
+
# one included, so the paths are mapped before any of them runs.
|
|
593
|
+
static func _moved_by_uid(content: String) -> Dictionary:
|
|
594
|
+
var moved: Dictionary = {}
|
|
595
|
+
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
|
|
596
|
+
var entry: String = line.get_string()
|
|
597
|
+
var at: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(entry)
|
|
598
|
+
var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
|
|
599
|
+
if at == null or uid == null or FileAccess.file_exists(at.get_string(1)):
|
|
600
|
+
continue
|
|
601
|
+
var id: int = ResourceUID.text_to_id(uid.get_string(1))
|
|
602
|
+
if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id):
|
|
603
|
+
continue
|
|
604
|
+
var found: String = ResourceUID.get_id_path(id)
|
|
605
|
+
if FileAccess.file_exists(found):
|
|
606
|
+
moved[at.get_string(1)] = found
|
|
607
|
+
return moved
|
|
608
|
+
|
|
609
|
+
|
|
506
610
|
func _referenced_path(matched: String) -> String:
|
|
507
611
|
var expression: String = ""
|
|
508
612
|
if "preload" in matched or "load" in matched:
|
|
@@ -113,17 +113,15 @@ func add_export_preset(params: Dictionary) -> Dictionary:
|
|
|
113
113
|
while config.has_section("preset." + str(index)):
|
|
114
114
|
var held: String = "preset." + str(index)
|
|
115
115
|
if str(config.get_value(held, "name", "")) == preset_name:
|
|
116
|
+
var path: String = str(config.get_value(held, "export_path", ""))
|
|
117
|
+
var to: String = "with no export path set" if path.is_empty() else "exporting to '%s'" % path
|
|
116
118
|
return _log.failure(
|
|
117
119
|
(
|
|
118
120
|
(
|
|
119
|
-
"There is already an export preset named '%s', for %s,
|
|
121
|
+
"There is already an export preset named '%s', for %s, %s. "
|
|
120
122
|
+ "project_export run exports with it; give another name to add a second."
|
|
121
123
|
)
|
|
122
|
-
% [
|
|
123
|
-
preset_name,
|
|
124
|
-
config.get_value(held, "platform", ""),
|
|
125
|
-
config.get_value(held, "export_path", "")
|
|
126
|
-
]
|
|
124
|
+
% [preset_name, config.get_value(held, "platform", ""), to]
|
|
127
125
|
)
|
|
128
126
|
)
|
|
129
127
|
index += 1
|
|
@@ -427,6 +427,11 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
427
427
|
}
|
|
428
428
|
if script != null:
|
|
429
429
|
answer["script"] = script.resource_path
|
|
430
|
+
# resourceType is the engine class under the script, which for a class the project declares
|
|
431
|
+
# is Resource whatever create was given.
|
|
432
|
+
var declared_class: String = str(script.get_global_name())
|
|
433
|
+
if not declared_class.is_empty():
|
|
434
|
+
answer["scriptClass"] = declared_class
|
|
430
435
|
return answer
|
|
431
436
|
|
|
432
437
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
+
const GdhRead = preload("reading.gd")
|
|
4
|
+
|
|
3
5
|
# Two walks rather than one because they disagree about hidden entries on purpose: a suffix
|
|
4
6
|
# walk descends into every visible directory and matches whole names, while the extension walk
|
|
5
7
|
# skips anything beginning with a dot, including .import sidecars that would otherwise answer
|
|
@@ -30,6 +32,38 @@ static func missing_reason(path: String) -> String:
|
|
|
30
32
|
return "" if FileAccess.file_exists(path) else path + " does not exist"
|
|
31
33
|
|
|
32
34
|
|
|
35
|
+
## [param said], the refusal for [param path], a res:// path nothing is at, with the files of its
|
|
36
|
+
## extension whose names are nearest its own, or every one when there are few and none is near, so
|
|
37
|
+
## the caller has a path to send instead.
|
|
38
|
+
func with_nearest_files(said: String, path: String) -> String:
|
|
39
|
+
var extension: String = path.get_extension()
|
|
40
|
+
if extension.is_empty():
|
|
41
|
+
return said
|
|
42
|
+
var files: Array[String] = find_files_with_extensions("res://", [extension])
|
|
43
|
+
# The walk lists a directory in the order the filesystem keeps it, which differs between
|
|
44
|
+
# platforms, so the same project was answered with its files in another order on Linux.
|
|
45
|
+
files.sort()
|
|
46
|
+
if files.is_empty():
|
|
47
|
+
return "%s. The project holds no .%s file." % [said, extension]
|
|
48
|
+
var names: PackedStringArray = []
|
|
49
|
+
for file: String in files:
|
|
50
|
+
var _named: bool = names.append(file.get_file().get_basename())
|
|
51
|
+
var close: PackedStringArray = GdhRead.nearest(path.get_file().get_basename(), names)
|
|
52
|
+
var offered: PackedStringArray = []
|
|
53
|
+
for file: String in files:
|
|
54
|
+
if file.get_file().get_basename() in close:
|
|
55
|
+
var _offered: bool = offered.append(file)
|
|
56
|
+
if not offered.is_empty():
|
|
57
|
+
var one: bool = offered.size() == 1
|
|
58
|
+
return (
|
|
59
|
+
"%s. The nearest .%s %s %s."
|
|
60
|
+
% [said, extension, "file is" if one else "files are", ", ".join(offered)]
|
|
61
|
+
)
|
|
62
|
+
if files.size() <= GdhRead.NAMED_CHILDREN:
|
|
63
|
+
return "%s. The project's .%s files are %s." % [said, extension, ", ".join(files)]
|
|
64
|
+
return "%s. None of the project's %d .%s files is named like it." % [said, files.size(), extension]
|
|
65
|
+
|
|
66
|
+
|
|
33
67
|
# A directory the engine steps over, marker file and all.
|
|
34
68
|
func is_stepped_over(path: String) -> bool:
|
|
35
69
|
return FileAccess.file_exists(path + ".gdignore")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
3
|
const GdhDependencies = preload("dependencies.gd")
|
|
4
|
+
const GdhFileWalk = preload("file_walk.gd")
|
|
4
5
|
const GdhPatterns = preload("patterns.gd")
|
|
5
6
|
const GdhRead = preload("reading.gd")
|
|
6
7
|
const GdhLog = preload("logger.gd")
|
|
@@ -25,7 +26,11 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
25
26
|
full_script_path = "res://" + full_script_path
|
|
26
27
|
|
|
27
28
|
if not FileAccess.file_exists(full_script_path):
|
|
28
|
-
return _log.failure(
|
|
29
|
+
return _log.failure(
|
|
30
|
+
GdhFileWalk.new().with_nearest_files(
|
|
31
|
+
"Script file does not exist: " + full_script_path, full_script_path
|
|
32
|
+
)
|
|
33
|
+
)
|
|
29
34
|
|
|
30
35
|
var file: FileAccess = FileAccess.open(full_script_path, FileAccess.READ)
|
|
31
36
|
if not file:
|
|
@@ -183,7 +188,7 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
183
188
|
"dependencies": dependencies,
|
|
184
189
|
"classes_used": classes_used,
|
|
185
190
|
"autoloads_used": autoloads_used,
|
|
186
|
-
"line_count": lines.size()
|
|
191
|
+
"line_count": lines.size() - 1 if content.is_empty() or content.ends_with("\n") else lines.size()
|
|
187
192
|
}
|
|
188
193
|
if GdhRead.as_bool(params.get("include_inherited", false)):
|
|
189
194
|
_add_inherited(answer, [full_script_path])
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
+
const GdhFileWalk = preload("file_walk.gd")
|
|
3
4
|
const GdhRead = preload("reading.gd")
|
|
4
5
|
const GdhLog = preload("logger.gd")
|
|
5
6
|
const GdhPatterns = preload("patterns.gd")
|
|
7
|
+
const GdhScriptCheck = preload("script_check.gd")
|
|
6
8
|
|
|
7
9
|
const MODIFICATION_TYPES: Array[String] = ["add_constant", "add_variable", "add_signal", "add_function"]
|
|
8
10
|
const BLANKS_AROUND_FUNCTIONS: int = 2
|
|
@@ -56,6 +58,13 @@ func create_gdscript(params: Dictionary) -> Dictionary:
|
|
|
56
58
|
"Script file already exists: " + full_script_path + ". script_edit modify adds to it."
|
|
57
59
|
)
|
|
58
60
|
|
|
61
|
+
# Content is left to the parse below: two scripts that name each other are written one at a time,
|
|
62
|
+
# and the first does not compile until the second exists.
|
|
63
|
+
if content.is_empty():
|
|
64
|
+
var header: String = _header_refusal(cls_name_param, extends_class)
|
|
65
|
+
if not header.is_empty():
|
|
66
|
+
return _log.failure(header + " Nothing was written.")
|
|
67
|
+
|
|
59
68
|
var dir: DirAccess = DirAccess.open("res://")
|
|
60
69
|
var script_dir: String = full_script_path.get_base_dir()
|
|
61
70
|
if script_dir != "res://" and not dir.dir_exists(script_dir.substr(6)):
|
|
@@ -111,7 +120,11 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
111
120
|
full_script_path = "res://" + full_script_path
|
|
112
121
|
|
|
113
122
|
if not FileAccess.file_exists(full_script_path):
|
|
114
|
-
return _log.failure(
|
|
123
|
+
return _log.failure(
|
|
124
|
+
GdhFileWalk.new().with_nearest_files(
|
|
125
|
+
"Script file does not exist: " + full_script_path, full_script_path
|
|
126
|
+
)
|
|
127
|
+
)
|
|
115
128
|
|
|
116
129
|
var file: FileAccess = FileAccess.open(full_script_path, FileAccess.READ)
|
|
117
130
|
if not file:
|
|
@@ -122,6 +135,8 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
122
135
|
|
|
123
136
|
var lines: Array[String] = []
|
|
124
137
|
lines.assign(original_content.split("\n"))
|
|
138
|
+
var before: Script = ResourceLoader.load(full_script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
|
|
139
|
+
var parsed_before: bool = _parses(before)
|
|
125
140
|
|
|
126
141
|
# All of them checked before any is made, so a call is made whole or not at all: one skipped for a
|
|
127
142
|
# missing name used to leave the rest written and the answer saying success over an empty list.
|
|
@@ -166,16 +181,99 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
166
181
|
if not rewrote:
|
|
167
182
|
return _log.failure("Failed to write to script file: " + full_script_path)
|
|
168
183
|
|
|
184
|
+
var caught: GdhScriptCheck.GdhCaught = GdhScriptCheck.GdhCaught.new()
|
|
185
|
+
OS.add_logger(caught)
|
|
169
186
|
var made: Script = ResourceLoader.load(full_script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
|
|
187
|
+
OS.remove_logger(caught)
|
|
188
|
+
var parses: bool = _parses(made)
|
|
189
|
+
# The names checked above are the script's own, and a member the base class declares, or a body
|
|
190
|
+
# that does not compile, is found only by the engine; a script left broken by an addition breaks
|
|
191
|
+
# every scene that uses it, so it goes back as it was. One already broken is written, as the
|
|
192
|
+
# addition can be what mends it.
|
|
193
|
+
if parsed_before and not parses:
|
|
194
|
+
file = FileAccess.open(full_script_path, FileAccess.WRITE)
|
|
195
|
+
var restored: bool = file != null and file.store_string(original_content)
|
|
196
|
+
if file != null:
|
|
197
|
+
file.close()
|
|
198
|
+
if not restored:
|
|
199
|
+
return _log.failure(
|
|
200
|
+
"%s would not parse with these additions, and could not be put back as it was" % script_path
|
|
201
|
+
)
|
|
202
|
+
return _log.failure(
|
|
203
|
+
(
|
|
204
|
+
"Nothing was changed: %s parses as it is and would not with these additions: %s"
|
|
205
|
+
% [script_path, _reasons(caught.raised, full_script_path)]
|
|
206
|
+
)
|
|
207
|
+
)
|
|
170
208
|
return {
|
|
171
209
|
"success": true,
|
|
172
210
|
"script_path": script_path,
|
|
173
211
|
"modifications_applied": modifications_applied,
|
|
174
212
|
"total_modifications": modifications_applied.size(),
|
|
175
|
-
"parses":
|
|
213
|
+
"parses": parses,
|
|
176
214
|
}
|
|
177
215
|
|
|
178
216
|
|
|
217
|
+
# The engine's errors in [param raised] for [param path], each with its line, as one sentence part.
|
|
218
|
+
static func _reasons(raised: Array[Dictionary], path: String) -> String:
|
|
219
|
+
var reasons: PackedStringArray = []
|
|
220
|
+
for error: Dictionary in raised:
|
|
221
|
+
var reason: String = "line %d, %s" % [GdhRead.as_int(error["line"]), str(error["message"])]
|
|
222
|
+
if str(error["path"]) == path and not reason in reasons:
|
|
223
|
+
var _added: bool = reasons.append(reason)
|
|
224
|
+
if reasons.is_empty():
|
|
225
|
+
return "the engine would not load it as a script."
|
|
226
|
+
var last: String = reasons[-1]
|
|
227
|
+
return "; ".join(reasons) + ("" if last.ends_with(".") or last.ends_with(".)") else ".")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# Why a script with [param named] as its class_name and extending [param base] cannot be written,
|
|
231
|
+
# or "". A class_name another script holds does not compile ("hides a global script class"), and nor
|
|
232
|
+
# does a base no class answers to; both were written and answered as made. Classes are read off the
|
|
233
|
+
# scripts as well as the class list, which a fresh clone has not written yet.
|
|
234
|
+
static func _header_refusal(named: String, base: String) -> String:
|
|
235
|
+
var declared: Dictionary = _declared_classes()
|
|
236
|
+
if not named.is_empty():
|
|
237
|
+
if ClassDB.class_exists(named):
|
|
238
|
+
return "class_name %s is the name of an engine class, which a script cannot take." % named
|
|
239
|
+
if declared.has(named):
|
|
240
|
+
return (
|
|
241
|
+
(
|
|
242
|
+
"class_name %s is taken: %s declares it, and a second script of that name does not "
|
|
243
|
+
+ "compile. Give this one another class_name, or script_edit rename the first."
|
|
244
|
+
)
|
|
245
|
+
% [named, declared[named]]
|
|
246
|
+
)
|
|
247
|
+
if base.begins_with('"') or base.begins_with("'") or "." in base:
|
|
248
|
+
return ""
|
|
249
|
+
if ClassDB.class_exists(base) or declared.has(base):
|
|
250
|
+
return ""
|
|
251
|
+
var said: String = (
|
|
252
|
+
"extends names %s, which is neither an engine class nor a class_name this project declares" % base
|
|
253
|
+
)
|
|
254
|
+
var close: PackedStringArray = GdhRead.nearest(base, PackedStringArray(declared.keys()))
|
|
255
|
+
if close.is_empty():
|
|
256
|
+
close = GdhRead.nearest(base, ClassDB.get_class_list())
|
|
257
|
+
if close.is_empty():
|
|
258
|
+
return said + "."
|
|
259
|
+
return said + "; the nearest %s %s." % ["is" if close.size() == 1 else "are", ", ".join(close)]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# The class_name each script of the project declares, by name, with its path.
|
|
263
|
+
static func _declared_classes() -> Dictionary:
|
|
264
|
+
var declared: Dictionary = {}
|
|
265
|
+
for entry: Dictionary in ProjectSettings.get_global_class_list():
|
|
266
|
+
var path: String = str(entry.get("path", ""))
|
|
267
|
+
if FileAccess.file_exists(path):
|
|
268
|
+
declared[str(entry.get("class", ""))] = path
|
|
269
|
+
for path: String in GdhFileWalk.new().find_files("res://", ".gd"):
|
|
270
|
+
for line: String in FileAccess.get_file_as_string(path).split("\n"):
|
|
271
|
+
var found: RegExMatch = GdhPatterns.declared_class(GdhPatterns.without_annotations(line))
|
|
272
|
+
if found != null and not declared.has(found.get_string(1)):
|
|
273
|
+
declared[found.get_string(1)] = path
|
|
274
|
+
return declared
|
|
275
|
+
|
|
276
|
+
|
|
179
277
|
# Every reason [param modifications] cannot be made as asked, each naming the entry by position.
|
|
180
278
|
#
|
|
181
279
|
# A name the script already declares is one of them, as is one the call adds twice. GDScript gives a
|
|
@@ -341,6 +439,11 @@ func _add_variable(lines: Array[String], mod: Dictionary, placed: Array[Dictiona
|
|
|
341
439
|
var_line += ": Variant"
|
|
342
440
|
|
|
343
441
|
var insert_line: int = _variable_insertion_point(lines)
|
|
442
|
+
# The first variable below the header or the signals is set apart from them by a blank line, as
|
|
443
|
+
# the style guide and gdformat lay a script out.
|
|
444
|
+
var above: String = lines[insert_line - 1].strip_edges() if insert_line > 0 else ""
|
|
445
|
+
if not above.is_empty() and not GdhPatterns.without_annotations(above).begins_with("var "):
|
|
446
|
+
return insert_line + 1 if _insert(lines, insert_line, ["", var_line], placed) else -1
|
|
344
447
|
return insert_line if _insert(lines, insert_line, [var_line], placed) else -1
|
|
345
448
|
|
|
346
449
|
|
|
@@ -277,6 +277,8 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
|
|
|
277
277
|
payload = GdhInputActions.new(_log).add_input_action(params)
|
|
278
278
|
"remove_input_action":
|
|
279
279
|
payload = GdhInputActions.new(_log).remove_input_action(params)
|
|
280
|
+
"list_input_actions":
|
|
281
|
+
payload = GdhInputActions.new(_log).list_input_actions(params)
|
|
280
282
|
|
|
281
283
|
# Audio
|
|
282
284
|
"create_audio_bus":
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
+
const GdhDependencies = preload("dependencies.gd")
|
|
3
4
|
const GdhExportPresets = preload("export_presets.gd")
|
|
5
|
+
const GdhScriptCheck = preload("script_check.gd")
|
|
4
6
|
const GdhRead = preload("reading.gd")
|
|
5
7
|
const GdhSerialisation = preload("serialisation.gd")
|
|
6
8
|
const GdhFileWalk = preload("file_walk.gd")
|
|
@@ -85,6 +87,14 @@ func get_import_status(params: Dictionary) -> Dictionary:
|
|
|
85
87
|
if not full_path.begins_with("res://"):
|
|
86
88
|
full_path = "res://" + full_path
|
|
87
89
|
|
|
90
|
+
# missing_source is a sidecar whose source has gone; with neither, the path names nothing the
|
|
91
|
+
# engine ever imported, and answering it as a lost source sent a caller to restore a file.
|
|
92
|
+
if not FileAccess.file_exists(full_path) and not FileAccess.file_exists(full_path + ".import"):
|
|
93
|
+
return _log.failure(
|
|
94
|
+
_files.with_nearest_files(
|
|
95
|
+
"Nothing is at %s, and no import sidecar names it" % full_path, full_path
|
|
96
|
+
)
|
|
97
|
+
)
|
|
88
98
|
var status: Dictionary = _import_status_of(full_path, full_path + ".import")
|
|
89
99
|
resources.append(status)
|
|
90
100
|
_tally(summary, status)
|
|
@@ -457,6 +467,35 @@ func validate_project(params: Dictionary) -> Dictionary:
|
|
|
457
467
|
if "pass # TODO" in content:
|
|
458
468
|
script_issues.append({"path": script_path, "issue": "Contains unimplemented functions"})
|
|
459
469
|
|
|
470
|
+
# Compiled by the server and handed in, as for the health check: a script the engine refuses
|
|
471
|
+
# stops the export's game at the first scene using it, which the comment check above never saw.
|
|
472
|
+
var compiled: Dictionary = params.get("compiled", {})
|
|
473
|
+
if compiled.has("checked"):
|
|
474
|
+
checks_performed.append("compiles")
|
|
475
|
+
var failures: Array = compiled.get("failures", [])
|
|
476
|
+
for failure: Dictionary in failures:
|
|
477
|
+
issues.append(
|
|
478
|
+
_finding(
|
|
479
|
+
"error",
|
|
480
|
+
"compiles",
|
|
481
|
+
GdhScriptCheck.failure_line(failure),
|
|
482
|
+
"script_diagnostics gives every error in it with its line",
|
|
483
|
+
include_suggestions
|
|
484
|
+
)
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
checks_performed.append("references")
|
|
488
|
+
for entry: Dictionary in GdhDependencies.new(_log).missing_references():
|
|
489
|
+
issues.append(
|
|
490
|
+
_finding(
|
|
491
|
+
"error",
|
|
492
|
+
"references",
|
|
493
|
+
GdhDependencies.missing_line(entry),
|
|
494
|
+
"Restore the file, or point the reference at the file that replaced it",
|
|
495
|
+
include_suggestions
|
|
496
|
+
)
|
|
497
|
+
)
|
|
498
|
+
|
|
460
499
|
if script_issues.size() > 0:
|
|
461
500
|
var warning: Dictionary = _finding(
|
|
462
501
|
"warning",
|
|
@@ -111,6 +111,24 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
111
111
|
var deadzone: float = GdhRead.as_float(params.get("deadzone", 0.5), 0.5)
|
|
112
112
|
|
|
113
113
|
_log.info("Adding input action: " + action_name)
|
|
114
|
+
# The editor's Input Map refuses these, since project.godot keys an action by its name and
|
|
115
|
+
# the characters below are its own syntax there; a name holding one is written escaped and
|
|
116
|
+
# reads back as a name nobody typed.
|
|
117
|
+
for index: int in action_name.length():
|
|
118
|
+
var code: int = action_name.unicode_at(index)
|
|
119
|
+
if code < 32 or action_name[index] in ["/", ":", "=", "\\", '"']:
|
|
120
|
+
return (
|
|
121
|
+
_log
|
|
122
|
+
. failure(
|
|
123
|
+
(
|
|
124
|
+
(
|
|
125
|
+
"%s is not an action name the engine's Input Map takes: a name cannot hold / : = \\ "
|
|
126
|
+
+ "or a double quote, or a control character. jump, move_left or ui_menu are names it takes."
|
|
127
|
+
)
|
|
128
|
+
% JSON.stringify(action_name)
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
)
|
|
114
132
|
_log.debug("Events: " + JSON.stringify(events))
|
|
115
133
|
_log.debug("Deadzone: " + str(deadzone))
|
|
116
134
|
|
|
@@ -130,7 +148,7 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
130
148
|
return _log.failure("A key event needs keycode")
|
|
131
149
|
var key_value: int = _keycode_value(keycode)
|
|
132
150
|
if key_value == KEY_NONE:
|
|
133
|
-
return _log.failure(
|
|
151
|
+
return _log.failure(_unknown_key(keycode))
|
|
134
152
|
|
|
135
153
|
var event_config: Dictionary = {"class_name": "InputEventKey", "keycode": key_value}
|
|
136
154
|
if fields.get("ctrl", false):
|
|
@@ -229,6 +247,42 @@ static func _shaped(event: InputEvent) -> Dictionary:
|
|
|
229
247
|
return shaped
|
|
230
248
|
|
|
231
249
|
|
|
250
|
+
## The input actions project.godot holds, each with its deadzone and events in the form
|
|
251
|
+
## add_input_action answers with. An engine ui_ action is listed only when the project rebinds it,
|
|
252
|
+
## as engineAction, since the engine's own eighty are there in every project.
|
|
253
|
+
func list_input_actions(_params: Dictionary) -> Dictionary:
|
|
254
|
+
var actions: Array[Dictionary] = []
|
|
255
|
+
for info: Dictionary in ProjectSettings.get_property_list():
|
|
256
|
+
var setting: String = str(info.get("name", ""))
|
|
257
|
+
if not setting.begins_with("input/"):
|
|
258
|
+
continue
|
|
259
|
+
var engine_default: Variant = ProjectSettings.property_get_revert(setting)
|
|
260
|
+
var held: Variant = ProjectSettings.get_setting(setting)
|
|
261
|
+
if engine_default != null and held == engine_default:
|
|
262
|
+
continue
|
|
263
|
+
var action: Dictionary = held if held is Dictionary else {}
|
|
264
|
+
var events: Array[Dictionary] = []
|
|
265
|
+
var listed: Variant = action.get("events", [])
|
|
266
|
+
if listed is Array:
|
|
267
|
+
var bound: Array = listed
|
|
268
|
+
for event: Variant in bound:
|
|
269
|
+
if event is InputEvent:
|
|
270
|
+
var shaped: InputEvent = event
|
|
271
|
+
events.append(_shaped(shaped))
|
|
272
|
+
(
|
|
273
|
+
actions
|
|
274
|
+
. append(
|
|
275
|
+
{
|
|
276
|
+
"name": setting.trim_prefix("input/"),
|
|
277
|
+
"deadzone": GdhRead.as_float(action.get("deadzone", 0.5), 0.5),
|
|
278
|
+
"events": events,
|
|
279
|
+
"engineAction": engine_default != null,
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
return {"actions": actions, "count": actions.size()}
|
|
284
|
+
|
|
285
|
+
|
|
232
286
|
# Remove an input action the project defines, which add_input_action had no way back from.
|
|
233
287
|
func remove_input_action(params: Dictionary) -> Dictionary:
|
|
234
288
|
var action_name: String = str(params.get("action_name", ""))
|
|
@@ -305,6 +359,24 @@ func build_input_action(deadzone: float, events: Array) -> Dictionary:
|
|
|
305
359
|
|
|
306
360
|
|
|
307
361
|
# The keycode for a key name as the tool spells it, or KEY_NONE for a name the table lacks.
|
|
362
|
+
## The refusal for [param key_name], which names no key: the names nearest it, then every name taken,
|
|
363
|
+
## since a name the caller has to guess at is the same guess again.
|
|
364
|
+
static func _unknown_key(key_name: String) -> String:
|
|
365
|
+
var every: PackedStringArray = []
|
|
366
|
+
var named: PackedStringArray = []
|
|
367
|
+
for name: String in KEY_NAMES:
|
|
368
|
+
var _every: bool = every.append(name)
|
|
369
|
+
if name.length() > 1 and not (name.begins_with("F") and name.substr(1).is_valid_int()):
|
|
370
|
+
var _named: bool = named.append(name)
|
|
371
|
+
var nearest: PackedStringArray = GdhRead.nearest(key_name, every)
|
|
372
|
+
var said: String = "Unknown key: %s." % key_name
|
|
373
|
+
if nearest.size() == 1:
|
|
374
|
+
said += " The nearest key name is %s." % nearest[0]
|
|
375
|
+
elif not nearest.is_empty():
|
|
376
|
+
said += " The nearest key names are %s." % ", ".join(nearest)
|
|
377
|
+
return said + " A key is named by its letter, its digit, F1 to F12, or one of: %s." % ", ".join(named)
|
|
378
|
+
|
|
379
|
+
|
|
308
380
|
func _keycode_value(key_name: String) -> int:
|
|
309
381
|
var wanted: String = key_name.to_lower()
|
|
310
382
|
for name: String in KEY_NAMES:
|