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
|
@@ -23,6 +23,24 @@ func create_gdscript(params: Dictionary) -> Dictionary:
|
|
|
23
23
|
|
|
24
24
|
_log.info("Creating GDScript: " + script_path)
|
|
25
25
|
|
|
26
|
+
# Content is the whole file, so a header written above it would be a second one: a content that
|
|
27
|
+
# starts with its own `extends` did not parse, and one without it was not the file that was sent.
|
|
28
|
+
if not content.is_empty():
|
|
29
|
+
var alongside: Array[String] = []
|
|
30
|
+
for name: String in ["class_name", "extends", "template"]:
|
|
31
|
+
if not str(params.get(name, "")).is_empty():
|
|
32
|
+
alongside.append(name)
|
|
33
|
+
if not alongside.is_empty():
|
|
34
|
+
return _log.failure(
|
|
35
|
+
(
|
|
36
|
+
"content is the whole file, so "
|
|
37
|
+
+ " and ".join(alongside)
|
|
38
|
+
+ " cannot be given with it: write "
|
|
39
|
+
+ ("them" if alongside.size() > 1 else "it")
|
|
40
|
+
+ " into content instead"
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
26
44
|
var full_script_path: String = script_path
|
|
27
45
|
if not full_script_path.begins_with("res://"):
|
|
28
46
|
full_script_path = "res://" + full_script_path
|
|
@@ -41,20 +59,16 @@ func create_gdscript(params: Dictionary) -> Dictionary:
|
|
|
41
59
|
if error != OK:
|
|
42
60
|
return _log.failure("Failed to create directory: " + script_dir + ", error: " + str(error))
|
|
43
61
|
|
|
44
|
-
var script_content: String =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
script_content += content
|
|
55
|
-
else:
|
|
56
|
-
script_content += "func _ready() -> void:\n"
|
|
57
|
-
script_content += "\tpass\n"
|
|
62
|
+
var script_content: String = content
|
|
63
|
+
if content.is_empty():
|
|
64
|
+
if not cls_name_param.is_empty():
|
|
65
|
+
script_content += "class_name " + cls_name_param + "\n"
|
|
66
|
+
script_content += "extends " + extends_class + "\n\n"
|
|
67
|
+
if not template.is_empty():
|
|
68
|
+
script_content += _script_template(template)
|
|
69
|
+
else:
|
|
70
|
+
script_content += "func _ready() -> void:\n"
|
|
71
|
+
script_content += "\tpass\n"
|
|
58
72
|
|
|
59
73
|
var file: FileAccess = FileAccess.open(full_script_path, FileAccess.WRITE)
|
|
60
74
|
if not file:
|
|
@@ -65,21 +79,20 @@ func create_gdscript(params: Dictionary) -> Dictionary:
|
|
|
65
79
|
if not stored:
|
|
66
80
|
return _log.failure("Failed to write to script file: " + full_script_path)
|
|
67
81
|
|
|
68
|
-
# Whether the engine accepts what was written, parsed under this project's own warning
|
|
69
|
-
# settings: a script that does not load is a script the caller wants to hear about now,
|
|
70
|
-
# and the reason is on stderr, which comes back with the answer.
|
|
71
82
|
var written: Script = ResourceLoader.load(full_script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
|
|
72
|
-
|
|
73
|
-
return {
|
|
83
|
+
var answer: Dictionary = {
|
|
74
84
|
"success": true,
|
|
75
85
|
"script_path": script_path,
|
|
76
86
|
"full_path": full_script_path,
|
|
77
87
|
"absolute_path": ProjectSettings.globalize_path(full_script_path),
|
|
78
|
-
"registered": not cls_name_param.is_empty(),
|
|
79
|
-
"extends": extends_class,
|
|
80
88
|
"template_used": template if not template.is_empty() else "none",
|
|
81
|
-
"parses": written
|
|
89
|
+
"parses": _parses(written),
|
|
82
90
|
}
|
|
91
|
+
# Read off the file as the engine parsed it rather than off the arguments, which content leaves out.
|
|
92
|
+
if written != null:
|
|
93
|
+
answer["extends"] = _base_named(written)
|
|
94
|
+
answer["class_name"] = str(written.get_global_name())
|
|
95
|
+
return answer
|
|
83
96
|
|
|
84
97
|
|
|
85
98
|
# Modify an existing GDScript file by adding functions, variables, or signals
|
|
@@ -103,34 +116,40 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
103
116
|
var original_content: String = file.get_as_text()
|
|
104
117
|
file.close()
|
|
105
118
|
|
|
119
|
+
# All of them checked before any is made, so a call is made whole or not at all: one skipped for a
|
|
120
|
+
# missing name used to leave the rest written and the answer saying success over an empty list.
|
|
121
|
+
var problems: Array[String] = _modification_problems(modifications)
|
|
122
|
+
if not problems.is_empty():
|
|
123
|
+
return _log.failure("Nothing was changed: " + "; ".join(problems))
|
|
124
|
+
|
|
106
125
|
var lines: Array[String] = []
|
|
107
126
|
lines.assign(original_content.split("\n"))
|
|
108
|
-
|
|
127
|
+
# Each placed line as a zero-based index that moves down whenever a later insertion lands above
|
|
128
|
+
# it, so the numbers answered are where the lines are in the file written, not where each went
|
|
129
|
+
# at the moment it was placed.
|
|
130
|
+
var placed: Array[Dictionary] = []
|
|
109
131
|
|
|
110
132
|
for mod: Variant in modifications:
|
|
111
|
-
if not mod is Dictionary:
|
|
112
|
-
_log.error("Modification must be an object")
|
|
113
|
-
continue
|
|
114
133
|
var fields: Dictionary = mod
|
|
115
134
|
var mod_type: String = str(fields.get("type", ""))
|
|
116
|
-
var
|
|
117
|
-
|
|
118
|
-
if mod_name.is_empty():
|
|
119
|
-
_log.error("Modification missing 'name' field")
|
|
120
|
-
continue
|
|
121
|
-
|
|
135
|
+
var at: int = -1
|
|
122
136
|
match mod_type:
|
|
123
137
|
"add_variable":
|
|
124
|
-
|
|
125
|
-
modifications_applied.append({"type": "add_variable", "name": mod_name, "line": line})
|
|
138
|
+
at = _add_variable(lines, fields, placed)
|
|
126
139
|
"add_signal":
|
|
127
|
-
|
|
128
|
-
modifications_applied.append({"type": "add_signal", "name": mod_name, "line": line})
|
|
140
|
+
at = _add_signal(lines, fields, placed)
|
|
129
141
|
"add_function":
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
at = _add_function(lines, fields, placed)
|
|
143
|
+
if at == -1:
|
|
144
|
+
return _log.failure(
|
|
145
|
+
"Nothing was changed: " + mod_type + " " + str(fields.get("name")) + " could not be placed"
|
|
146
|
+
)
|
|
147
|
+
placed.append({"type": mod_type, "name": str(fields.get("name")), "index": at})
|
|
148
|
+
|
|
149
|
+
var modifications_applied: Array[Dictionary] = []
|
|
150
|
+
for entry: Dictionary in placed:
|
|
151
|
+
var index: int = entry["index"]
|
|
152
|
+
modifications_applied.append({"type": entry["type"], "name": entry["name"], "line": index + 1})
|
|
134
153
|
|
|
135
154
|
file = FileAccess.open(full_script_path, FileAccess.WRITE)
|
|
136
155
|
if not file:
|
|
@@ -141,17 +160,75 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
141
160
|
if not rewrote:
|
|
142
161
|
return _log.failure("Failed to write to script file: " + full_script_path)
|
|
143
162
|
|
|
163
|
+
var made: Script = ResourceLoader.load(full_script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
|
|
144
164
|
return {
|
|
145
165
|
"success": true,
|
|
146
166
|
"script_path": script_path,
|
|
147
167
|
"modifications_applied": modifications_applied,
|
|
148
|
-
"total_modifications": modifications_applied.size()
|
|
168
|
+
"total_modifications": modifications_applied.size(),
|
|
169
|
+
"parses": _parses(made),
|
|
149
170
|
}
|
|
150
171
|
|
|
151
172
|
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
173
|
+
# Every reason [param modifications] cannot be made as asked, each naming the entry by position.
|
|
174
|
+
static func _modification_problems(modifications: Array) -> Array[String]:
|
|
175
|
+
var problems: Array[String] = []
|
|
176
|
+
if modifications.is_empty():
|
|
177
|
+
problems.append("modifications is empty")
|
|
178
|
+
for index: int in range(modifications.size()):
|
|
179
|
+
var mod: Variant = modifications[index]
|
|
180
|
+
var which: String = "modification " + str(index + 1)
|
|
181
|
+
if not mod is Dictionary:
|
|
182
|
+
problems.append(which + " is not an object")
|
|
183
|
+
continue
|
|
184
|
+
var fields: Dictionary = mod
|
|
185
|
+
var mod_type: String = str(fields.get("type", ""))
|
|
186
|
+
var mod_name: String = str(fields.get("name", ""))
|
|
187
|
+
if mod_type not in ["add_variable", "add_signal", "add_function"]:
|
|
188
|
+
problems.append(
|
|
189
|
+
which + " has type '" + mod_type + "', not add_variable, add_signal or add_function"
|
|
190
|
+
)
|
|
191
|
+
if mod_name.is_empty():
|
|
192
|
+
problems.append(which + " has no name")
|
|
193
|
+
elif not mod_name.is_valid_unicode_identifier():
|
|
194
|
+
problems.append(which + " is named '" + mod_name + "', which GDScript does not accept as a name")
|
|
195
|
+
return problems
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# Inserts [param new_lines] at [param at] and moves every placed line at or below it down by as many.
|
|
199
|
+
static func _insert(
|
|
200
|
+
lines: Array[String], at: int, new_lines: Array[String], placed: Array[Dictionary]
|
|
201
|
+
) -> bool:
|
|
202
|
+
for i: int in range(new_lines.size() - 1, -1, -1):
|
|
203
|
+
if lines.insert(at, new_lines[i]) != OK:
|
|
204
|
+
push_error("gdharness: could not place a line at " + str(at))
|
|
205
|
+
return false
|
|
206
|
+
for entry: Dictionary in placed:
|
|
207
|
+
var index: int = entry["index"]
|
|
208
|
+
if index >= at:
|
|
209
|
+
entry["index"] = index + new_lines.size()
|
|
210
|
+
return true
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# Whether the engine accepts [param script], parsed under this project's own warning settings: a
|
|
214
|
+
# script that does not load is one the caller wants to hear about now, and the reason is on stderr,
|
|
215
|
+
# which comes back with the answer. A script that fails to parse still loads, as one that cannot be
|
|
216
|
+
# instantiated; an abstract one that parses can be, on 4.7.
|
|
217
|
+
static func _parses(script: Script) -> bool:
|
|
218
|
+
return script != null and script.can_instantiate()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# What [param script] extends: the base script by class name or path, or the native class.
|
|
222
|
+
static func _base_named(script: Script) -> String:
|
|
223
|
+
var base: Script = script.get_base_script()
|
|
224
|
+
if base == null:
|
|
225
|
+
return str(script.get_instance_base_type())
|
|
226
|
+
var named: String = str(base.get_global_name())
|
|
227
|
+
return named if not named.is_empty() else base.resource_path
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# Inserts the declaration and answers with its zero-based index, or -1 for one that could not be placed.
|
|
231
|
+
func _add_variable(lines: Array[String], mod: Dictionary, placed: Array[Dictionary]) -> int:
|
|
155
232
|
var var_name: String = str(mod.get("name", ""))
|
|
156
233
|
var var_type: String = str(mod.get("varType", ""))
|
|
157
234
|
var default_value: String = str(mod.get("defaultValue", ""))
|
|
@@ -186,10 +263,7 @@ func _add_variable(lines: Array[String], mod: Dictionary) -> int:
|
|
|
186
263
|
var_line += ": Variant"
|
|
187
264
|
|
|
188
265
|
var insert_line: int = _variable_insertion_point(lines)
|
|
189
|
-
if lines
|
|
190
|
-
push_error("gdharness: could not place a declaration at line " + str(insert_line))
|
|
191
|
-
return 0
|
|
192
|
-
return insert_line + 1
|
|
266
|
+
return insert_line if _insert(lines, insert_line, [var_line], placed) else -1
|
|
193
267
|
|
|
194
268
|
|
|
195
269
|
# The type name a constant expression evaluates to, or Variant when it is not one: a call
|
|
@@ -208,7 +282,7 @@ func _type_of_literal(expression: String) -> String:
|
|
|
208
282
|
return type_string(typeof(value))
|
|
209
283
|
|
|
210
284
|
|
|
211
|
-
func _add_signal(lines: Array[String], mod: Dictionary) -> int:
|
|
285
|
+
func _add_signal(lines: Array[String], mod: Dictionary, placed: Array[Dictionary]) -> int:
|
|
212
286
|
var signal_name: String = str(mod.get("name", ""))
|
|
213
287
|
var signal_params: String = str(mod.get("params", ""))
|
|
214
288
|
|
|
@@ -217,13 +291,11 @@ func _add_signal(lines: Array[String], mod: Dictionary) -> int:
|
|
|
217
291
|
signal_line += "(" + signal_params + ")"
|
|
218
292
|
|
|
219
293
|
var insert_line: int = _signal_insertion_point(lines)
|
|
220
|
-
if lines
|
|
221
|
-
push_error("gdharness: could not place a signal at line " + str(insert_line))
|
|
222
|
-
return 0
|
|
223
|
-
return insert_line + 1
|
|
294
|
+
return insert_line if _insert(lines, insert_line, [signal_line], placed) else -1
|
|
224
295
|
|
|
225
296
|
|
|
226
|
-
func
|
|
297
|
+
# Answers with the zero-based index of the `func` line, below the blank line that separates it.
|
|
298
|
+
func _add_function(lines: Array[String], mod: Dictionary, placed: Array[Dictionary]) -> int:
|
|
227
299
|
var func_name: String = str(mod.get("name", ""))
|
|
228
300
|
var func_params: String = str(mod.get("params", ""))
|
|
229
301
|
# A function that names no return type returns nothing, and says so, for the same reason
|
|
@@ -248,12 +320,7 @@ func _add_function(lines: Array[String], mod: Dictionary) -> int:
|
|
|
248
320
|
if insert_line < lines.size():
|
|
249
321
|
func_lines.append("")
|
|
250
322
|
|
|
251
|
-
|
|
252
|
-
if lines.insert(insert_line, func_lines[i]) != OK:
|
|
253
|
-
push_error("gdharness: could not place a function at line " + str(insert_line))
|
|
254
|
-
return 0
|
|
255
|
-
|
|
256
|
-
return insert_line + 1
|
|
323
|
+
return insert_line + 1 if _insert(lines, insert_line, func_lines, placed) else -1
|
|
257
324
|
|
|
258
325
|
|
|
259
326
|
func _variable_insertion_point(lines: Array[String]) -> int:
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
extends SceneTree
|
|
3
3
|
|
|
4
4
|
# The command line the server runs is `godot --headless --path <project> --script <this>
|
|
5
|
-
# <operation> @file:<params.json>`, and one JSON object
|
|
6
|
-
# operation does lives in a sibling module; this file is the
|
|
5
|
+
# <operation> @file:<params.json> <answer.json>`, and the one JSON object written to the answer
|
|
6
|
+
# file is the answer. Everything an operation does lives in a sibling module; this file is the
|
|
7
|
+
# command table and the wire format.
|
|
8
|
+
#
|
|
9
|
+
# A file rather than a line on stdout, because the project's autoloads run after `_init` and print
|
|
10
|
+
# into the same stream: an autoload printing a dictionary in `_process` was the last JSON line out
|
|
11
|
+
# and was taken as the answer. Nothing the project prints can reach the file.
|
|
7
12
|
#
|
|
8
13
|
# Each module is preloaded by a path relative to this script rather than a res:// one, because
|
|
9
14
|
# the operations directory ships inside the server package and is handed to the engine as an
|
|
@@ -39,8 +44,9 @@ func _init() -> void:
|
|
|
39
44
|
return
|
|
40
45
|
|
|
41
46
|
var params_index: int = script_index + 3
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
var answer_index: int = script_index + 4
|
|
48
|
+
if args.size() <= answer_index:
|
|
49
|
+
_log.error("Usage: godot_operations.gd <operation> @file:<params.json> <answer.json>")
|
|
44
50
|
_log.error("Not enough command-line arguments provided.")
|
|
45
51
|
quit(1)
|
|
46
52
|
return
|
|
@@ -65,7 +71,20 @@ func _init() -> void:
|
|
|
65
71
|
quit(1)
|
|
66
72
|
return
|
|
67
73
|
|
|
68
|
-
|
|
74
|
+
var answer_path: String = args[answer_index]
|
|
75
|
+
var answer: FileAccess = FileAccess.open(answer_path, FileAccess.WRITE)
|
|
76
|
+
if answer == null:
|
|
77
|
+
_log.error(
|
|
78
|
+
"Could not write the answer to " + answer_path + ": " + error_string(FileAccess.get_open_error())
|
|
79
|
+
)
|
|
80
|
+
quit(1)
|
|
81
|
+
return
|
|
82
|
+
var written: bool = answer.store_string(JSON.stringify(payload))
|
|
83
|
+
answer.close()
|
|
84
|
+
if not written:
|
|
85
|
+
_log.error("Could not write the answer to " + answer_path)
|
|
86
|
+
quit(1)
|
|
87
|
+
return
|
|
69
88
|
quit()
|
|
70
89
|
|
|
71
90
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
3
|
const Read = preload("reading.gd")
|
|
4
|
+
const Serialisation = preload("serialisation.gd")
|
|
4
5
|
const FileWalk = preload("file_walk.gd")
|
|
5
6
|
const Log = preload("logger.gd")
|
|
6
7
|
|
|
@@ -37,11 +38,20 @@ const IMPORTABLE_EXTENSIONS: Array[String] = [
|
|
|
37
38
|
"obj",
|
|
38
39
|
"dae",
|
|
39
40
|
]
|
|
41
|
+
## The format version each importer writes as `importer_version` where it is above 0, which the
|
|
42
|
+
## editor reimports anything older than. Read off sidecars 4.7.2 writes: scene and animation_library
|
|
43
|
+
## write 1, and the texture importers write none. A case imports one fresh and compares, so an engine
|
|
44
|
+
## that raises one fails it rather than leaving this behind.
|
|
45
|
+
const IMPORTER_VERSIONS: Dictionary[String, int] = {"scene": 1, "animation_library": 1}
|
|
46
|
+
## The VRAM compression formats a project can ask for, each behind its
|
|
47
|
+
## `rendering/textures/vram_compression/import_<format>` setting.
|
|
48
|
+
const VRAM_FORMATS: Array[String] = ["s3tc_bptc", "etc2_astc"]
|
|
40
49
|
const EDITOR_DIRECTORY: String = "res://.godot/editor"
|
|
41
50
|
const EDITOR_CACHE_PREFIX: String = "filesystem_cache"
|
|
42
51
|
|
|
43
52
|
var _log: Log
|
|
44
53
|
var _files: FileWalk = FileWalk.new()
|
|
54
|
+
var _values: Serialisation = Serialisation.new()
|
|
45
55
|
var _sidecar_md5s: Variant = null
|
|
46
56
|
|
|
47
57
|
|
|
@@ -63,7 +73,7 @@ func get_import_status(params: Dictionary) -> Dictionary:
|
|
|
63
73
|
|
|
64
74
|
var resources: Array[Dictionary] = []
|
|
65
75
|
var summary: Dictionary = {
|
|
66
|
-
"total": 0, "needs_reimport": 0, "failed": 0, "up_to_date": 0, "missing_source": 0
|
|
76
|
+
"total": 0, "needs_reimport": 0, "failed": 0, "up_to_date": 0, "missing_source": 0, "not_imported": 0
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
if not resource_path.is_empty():
|
|
@@ -107,8 +117,7 @@ func get_import_options(params: Dictionary) -> Dictionary:
|
|
|
107
117
|
var import_file_path: String = resource_path + ".import"
|
|
108
118
|
|
|
109
119
|
if not FileAccess.file_exists(import_file_path):
|
|
110
|
-
_log.
|
|
111
|
-
return _log.failure("This resource may not have been imported yet")
|
|
120
|
+
return _log.failure(_no_sidecar(resource_path))
|
|
112
121
|
|
|
113
122
|
var config: ConfigFile = ConfigFile.new()
|
|
114
123
|
var err: Error = config.load(import_file_path)
|
|
@@ -120,11 +129,14 @@ func get_import_options(params: Dictionary) -> Dictionary:
|
|
|
120
129
|
"resource_path": resource_path, "import_file": import_file_path, "remap": {}, "deps": {}, "params": {}
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
# Through the serialiser, as every other read answers: an engine type written straight into
|
|
133
|
+
# JSON is its own text, so a Vector3 option came back as "(1, 1, 1)", and writing that back put
|
|
134
|
+
# a string in the sidecar where the importer reads a vector.
|
|
123
135
|
for section: String in ["remap", "deps", "params"]:
|
|
124
136
|
if config.has_section(section):
|
|
125
137
|
var values: Dictionary = result[section]
|
|
126
138
|
for key: String in config.get_section_keys(section):
|
|
127
|
-
values[key] = config.get_value(section, key)
|
|
139
|
+
values[key] = _values.serialize_value(config.get_value(section, key))
|
|
128
140
|
|
|
129
141
|
return result
|
|
130
142
|
|
|
@@ -142,8 +154,12 @@ func set_import_options(params: Dictionary) -> Dictionary:
|
|
|
142
154
|
var import_file_path: String = resource_path + ".import"
|
|
143
155
|
|
|
144
156
|
if not FileAccess.file_exists(import_file_path):
|
|
145
|
-
_log.
|
|
146
|
-
|
|
157
|
+
return _log.failure(_no_sidecar(resource_path))
|
|
158
|
+
# A sidecar left behind by a source that has gone takes options that nothing will ever apply.
|
|
159
|
+
if not FileAccess.file_exists(resource_path):
|
|
160
|
+
return _log.failure(
|
|
161
|
+
resource_path + " is not on disk, so there is nothing its import options apply to"
|
|
162
|
+
)
|
|
147
163
|
|
|
148
164
|
var config: ConfigFile = ConfigFile.new()
|
|
149
165
|
var err: Error = config.load(import_file_path)
|
|
@@ -151,13 +167,37 @@ func set_import_options(params: Dictionary) -> Dictionary:
|
|
|
151
167
|
if err != OK:
|
|
152
168
|
return _log.failure("Failed to parse import file: " + str(err))
|
|
153
169
|
|
|
154
|
-
|
|
170
|
+
# Every option checked before any is written, so a refused one leaves the sidecar as it was.
|
|
171
|
+
var fitted_values: Dictionary = {}
|
|
172
|
+
var refused: Array[String] = []
|
|
155
173
|
for key: Variant in options:
|
|
156
174
|
var name: String = str(key)
|
|
157
|
-
var
|
|
158
|
-
|
|
175
|
+
var held: Variant = config.get_value("params", name, null)
|
|
176
|
+
var given: Variant = _values.deserialize_value(options[key])
|
|
177
|
+
var value: Variant = _as_option(_values.fitted(options[key], typeof(held)), held)
|
|
178
|
+
# A number the option cannot hold whole is refused rather than truncated: 2.5 fitted to an
|
|
179
|
+
# enum option became 2, which is another setting.
|
|
180
|
+
var lossy: bool = (
|
|
181
|
+
(given is int or given is float)
|
|
182
|
+
and (value is int or value is float)
|
|
183
|
+
and type_convert(value, typeof(given)) != given
|
|
184
|
+
)
|
|
185
|
+
if held != null and (lossy or not Serialisation.acceptable(value, typeof(held))):
|
|
186
|
+
var held_type: String = type_string(typeof(held))
|
|
187
|
+
var article: String = "an" if held_type.to_lower()[0] in "aeiou" else "a"
|
|
188
|
+
refused.append(
|
|
189
|
+
"%s holds %s %s, and %s is not one" % [name, article, held_type, JSON.stringify(options[key])]
|
|
190
|
+
)
|
|
191
|
+
continue
|
|
192
|
+
fitted_values[name] = value
|
|
193
|
+
if not refused.is_empty():
|
|
194
|
+
return _log.failure("Nothing was written: " + "; ".join(refused))
|
|
195
|
+
|
|
196
|
+
var updated_keys: Array[String] = []
|
|
197
|
+
for name: String in fitted_values:
|
|
198
|
+
config.set_value("params", name, fitted_values[name])
|
|
159
199
|
updated_keys.append(name)
|
|
160
|
-
_log.debug("Set " + name + " = " + str(
|
|
200
|
+
_log.debug("Set " + name + " = " + str(fitted_values[name]))
|
|
161
201
|
|
|
162
202
|
err = config.save(import_file_path)
|
|
163
203
|
if err != OK:
|
|
@@ -178,6 +218,19 @@ static func _as_option(given: Variant, held: Variant) -> Variant:
|
|
|
178
218
|
return given
|
|
179
219
|
|
|
180
220
|
|
|
221
|
+
# The names of the project's export presets, in the order export_presets.cfg holds them.
|
|
222
|
+
static func _preset_names() -> Array[String]:
|
|
223
|
+
var names: Array[String] = []
|
|
224
|
+
var config: ConfigFile = ConfigFile.new()
|
|
225
|
+
if config.load("res://export_presets.cfg") != OK:
|
|
226
|
+
return names
|
|
227
|
+
var index: int = 0
|
|
228
|
+
while config.has_section("preset." + str(index)):
|
|
229
|
+
names.append(str(config.get_value("preset." + str(index), "name", "")))
|
|
230
|
+
index += 1
|
|
231
|
+
return names
|
|
232
|
+
|
|
233
|
+
|
|
181
234
|
# List export presets
|
|
182
235
|
func list_export_presets(_params: Dictionary) -> Dictionary:
|
|
183
236
|
_log.info("Listing export presets")
|
|
@@ -257,12 +310,12 @@ func validate_project(params: Dictionary) -> Dictionary:
|
|
|
257
310
|
include_suggestions
|
|
258
311
|
)
|
|
259
312
|
)
|
|
260
|
-
elif not
|
|
313
|
+
elif not FileWalk.missing_reason(main_scene).is_empty():
|
|
261
314
|
issues.append(
|
|
262
315
|
_finding(
|
|
263
316
|
"error",
|
|
264
317
|
"main_scene",
|
|
265
|
-
"Main scene
|
|
318
|
+
"Main scene " + FileWalk.missing_reason(main_scene),
|
|
266
319
|
"Update the main scene setting or create the missing scene file",
|
|
267
320
|
include_suggestions
|
|
268
321
|
)
|
|
@@ -280,6 +333,30 @@ func validate_project(params: Dictionary) -> Dictionary:
|
|
|
280
333
|
)
|
|
281
334
|
)
|
|
282
335
|
|
|
336
|
+
# The preset asked about is looked for rather than only logged: validating "for" a preset that
|
|
337
|
+
# does not exist answered valid, as though it had been checked.
|
|
338
|
+
if not preset_name.is_empty():
|
|
339
|
+
checks_performed.append("export_preset")
|
|
340
|
+
var named: Array[String] = _preset_names()
|
|
341
|
+
if preset_name not in named:
|
|
342
|
+
issues.append(
|
|
343
|
+
_finding(
|
|
344
|
+
"error",
|
|
345
|
+
"export_preset",
|
|
346
|
+
(
|
|
347
|
+
"No export preset is named "
|
|
348
|
+
+ preset_name
|
|
349
|
+
+ (
|
|
350
|
+
": the project has none"
|
|
351
|
+
if named.is_empty()
|
|
352
|
+
else ": the presets are " + ", ".join(named)
|
|
353
|
+
)
|
|
354
|
+
),
|
|
355
|
+
"Pass one of the preset names, or add the preset in the editor: Project > Export",
|
|
356
|
+
include_suggestions
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
|
|
283
360
|
checks_performed.append("icon")
|
|
284
361
|
var icon_path: String = str(ProjectSettings.get_setting("application/config/icon", ""))
|
|
285
362
|
if icon_path.is_empty():
|
|
@@ -323,11 +400,8 @@ func validate_project(params: Dictionary) -> Dictionary:
|
|
|
323
400
|
|
|
324
401
|
# A hundred scripts is enough to say whether the project is tidy without a large one
|
|
325
402
|
# turning validation into a full read of its source tree.
|
|
326
|
-
for script_path: String in script_files:
|
|
403
|
+
for script_path: String in script_files.slice(0, 100):
|
|
327
404
|
scripts_checked += 1
|
|
328
|
-
if scripts_checked > 100:
|
|
329
|
-
break
|
|
330
|
-
|
|
331
405
|
var file: FileAccess = FileAccess.open(script_path, FileAccess.READ)
|
|
332
406
|
if file:
|
|
333
407
|
var content: String = file.get_as_text()
|
|
@@ -355,6 +429,8 @@ func validate_project(params: Dictionary) -> Dictionary:
|
|
|
355
429
|
"warnings": warnings,
|
|
356
430
|
"checks_performed": checks_performed,
|
|
357
431
|
"scripts_checked": scripts_checked,
|
|
432
|
+
# Said beside the count, so a project of four hundred scripts is not read as checked whole.
|
|
433
|
+
"scripts_total": script_files.size(),
|
|
358
434
|
"issue_count": issues.size(),
|
|
359
435
|
"warning_count": warnings.size()
|
|
360
436
|
}
|
|
@@ -389,10 +465,14 @@ func _import_status_of(resource_path: String, import_file_path: String) -> Dicti
|
|
|
389
465
|
|
|
390
466
|
var import_file_exists: bool = FileAccess.file_exists(import_file_path)
|
|
391
467
|
if not import_file_exists:
|
|
468
|
+
# A file with no sidecar has not been imported only if it is something the engine imports.
|
|
469
|
+
# A scene answered as needing an import, and a reimport of it ran a pass and then reported
|
|
470
|
+
# the scene as not reimported, which reads as the import failing.
|
|
471
|
+
var never: String = _never_imported(resource_path)
|
|
392
472
|
return {
|
|
393
473
|
"path": resource_path,
|
|
394
|
-
"status": "needs_reimport",
|
|
395
|
-
"reason": "it has not been imported",
|
|
474
|
+
"status": "needs_reimport" if never.is_empty() else "not_imported",
|
|
475
|
+
"reason": "it has not been imported" if never.is_empty() else never,
|
|
396
476
|
"import_file_exists": false,
|
|
397
477
|
"source_exists": true
|
|
398
478
|
}
|
|
@@ -402,7 +482,9 @@ func _import_status_of(resource_path: String, import_file_path: String) -> Dicti
|
|
|
402
482
|
}
|
|
403
483
|
|
|
404
484
|
# The checks are the ones EditorFileSystem::_test_for_reimport makes, in its order, so a resource
|
|
405
|
-
# reads as needing an import
|
|
485
|
+
# reads as needing an import when the editor's next scan would import it. The importer's own two
|
|
486
|
+
# cannot be asked here, where no importer is loaded, so they are made from what was measured of
|
|
487
|
+
# the built-in ones; an importer a plugin adds is judged by its files alone.
|
|
406
488
|
var sidecar: ConfigFile = ConfigFile.new()
|
|
407
489
|
var stale: Dictionary = (
|
|
408
490
|
_stale("its import file cannot be read")
|
|
@@ -416,6 +498,41 @@ func _import_status_of(resource_path: String, import_file_path: String) -> Dicti
|
|
|
416
498
|
return status
|
|
417
499
|
|
|
418
500
|
|
|
501
|
+
# Why [param resource_path] has no import file, with what to do about it where anything can be done.
|
|
502
|
+
func _no_sidecar(resource_path: String) -> String:
|
|
503
|
+
if not FileAccess.file_exists(resource_path):
|
|
504
|
+
return resource_path + " is not on disk"
|
|
505
|
+
var never: String = _never_imported(resource_path)
|
|
506
|
+
if not never.is_empty():
|
|
507
|
+
return resource_path + " has no import options: " + never
|
|
508
|
+
return (
|
|
509
|
+
resource_path
|
|
510
|
+
+ " has not been imported yet, so it has no import file: project_import reimport imports it"
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# Why the engine will never import [param resource_path], or empty when it would.
|
|
515
|
+
func _never_imported(resource_path: String) -> String:
|
|
516
|
+
var folder: String = resource_path.get_base_dir()
|
|
517
|
+
while folder.begins_with("res://"):
|
|
518
|
+
if _files.is_stepped_over(folder if folder.ends_with("/") else folder + "/"):
|
|
519
|
+
return "a .gdignore in " + folder + " keeps the engine from importing anything under it"
|
|
520
|
+
if folder == "res://":
|
|
521
|
+
break
|
|
522
|
+
folder = folder.get_base_dir()
|
|
523
|
+
var extension: String = resource_path.get_extension().to_lower()
|
|
524
|
+
if extension in IMPORTABLE_EXTENSIONS:
|
|
525
|
+
return ""
|
|
526
|
+
# Loadable without a sidecar is what a scene, a script or a saved resource is: read as it is.
|
|
527
|
+
if ResourceLoader.exists(resource_path):
|
|
528
|
+
return "the engine loads ." + extension + " files as they are, without importing them"
|
|
529
|
+
return (
|
|
530
|
+
"no importer built into the engine takes ."
|
|
531
|
+
+ extension
|
|
532
|
+
+ " files; one added by a plugin or an extension is known only to the editor, whose scan imports them"
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
419
536
|
static func _stale(reason: String) -> Dictionary:
|
|
420
537
|
return {"status": "needs_reimport", "reason": reason}
|
|
421
538
|
|
|
@@ -443,6 +560,27 @@ func _sidecar_staleness(resource_path: String, import_file_path: String, sidecar
|
|
|
443
560
|
## Whether the import itself is still the one its sidecar and source describe: an answer for a
|
|
444
561
|
## stale resource, empty for a current one.
|
|
445
562
|
static func _import_staleness(resource_path: String, sidecar: ConfigFile) -> Dictionary:
|
|
563
|
+
# The importer is asked first, as the editor asks it: whether it still writes this format, and
|
|
564
|
+
# whether the settings the import was made under still hold. Both are true of an import whose
|
|
565
|
+
# files are all current, so they were answered as up to date while the editor's next scan
|
|
566
|
+
# imported them again: every scene after an engine upgrade raised the importer's version, and
|
|
567
|
+
# every VRAM texture once the project asked for another compression format.
|
|
568
|
+
var importer: String = str(sidecar.get_value("remap", "importer", ""))
|
|
569
|
+
var version: int = Read.as_int(sidecar.get_value("remap", "importer_version", 0))
|
|
570
|
+
var current: int = Read.as_int(IMPORTER_VERSIONS.get(importer, 0))
|
|
571
|
+
if current > version:
|
|
572
|
+
return _stale(
|
|
573
|
+
(
|
|
574
|
+
"it was imported at version %d of the %s importer, and this engine imports at version %d"
|
|
575
|
+
% [version, importer, current]
|
|
576
|
+
)
|
|
577
|
+
)
|
|
578
|
+
var lacking: Array[String] = _vram_formats_lacking(sidecar.get_value("remap", "metadata", {}))
|
|
579
|
+
if not lacking.is_empty():
|
|
580
|
+
return _stale(
|
|
581
|
+
"the project now asks for " + ", ".join(lacking) + " textures, and this import did not write them"
|
|
582
|
+
)
|
|
583
|
+
|
|
446
584
|
if not sidecar.has_section_key("remap", "uid"):
|
|
447
585
|
return _stale("its import file has no uid, which the import writes")
|
|
448
586
|
|
|
@@ -505,6 +643,28 @@ static func _import_staleness(resource_path: String, sidecar: ConfigFile) -> Dic
|
|
|
505
643
|
return built
|
|
506
644
|
|
|
507
645
|
|
|
646
|
+
## The VRAM formats the project asks textures to be imported in that an import recorded in
|
|
647
|
+
## [param metadata] did not write, as the texture importers judge whether their settings still hold.
|
|
648
|
+
## Measured on 4.7.2 for the texture and texture array importers: turning a format on reimports every
|
|
649
|
+
## VRAM texture without it, and turning one off reimports nothing.
|
|
650
|
+
static func _vram_formats_lacking(metadata: Variant) -> Array[String]:
|
|
651
|
+
var lacking: Array[String] = []
|
|
652
|
+
if not metadata is Dictionary:
|
|
653
|
+
return lacking
|
|
654
|
+
var meta: Dictionary = metadata
|
|
655
|
+
if not Read.as_bool(meta.get("vram_texture", false)):
|
|
656
|
+
return lacking
|
|
657
|
+
var written: Variant = meta.get("imported_formats", [])
|
|
658
|
+
var formats: Array = written if written is Array else []
|
|
659
|
+
for format: String in VRAM_FORMATS:
|
|
660
|
+
var asked: bool = Read.as_bool(
|
|
661
|
+
ProjectSettings.get_setting("rendering/textures/vram_compression/import_" + format, false)
|
|
662
|
+
)
|
|
663
|
+
if asked and format not in formats:
|
|
664
|
+
lacking.append(format)
|
|
665
|
+
return lacking
|
|
666
|
+
|
|
667
|
+
|
|
508
668
|
## The hashes [param resource_path]'s last import recorded, [code]source_md5[/code] of its source
|
|
509
669
|
## and [code]dest_md5[/code] of its outputs, empty when there is no record: the editor keeps them
|
|
510
670
|
## beside the outputs, under the same base name with [code].md5[/code].
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# Everything printed is diagnostics; the answer goes to a file. The server reads `[ERROR]` lines as
|
|
4
|
+
# the operation's own errors, and the prefixes let a human read the rest without parsing it.
|
|
5
5
|
#
|
|
6
6
|
# Every module preloads this as `Log`. `Logger` is a native class from Godot 4.5 on, and a
|
|
7
7
|
# constant of that name is a parse error rather than a shadowing warning.
|