gdharness 1.2.33 → 1.2.35
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 +134 -33
- package/build/godot/addons/gdharness_editor/animation_reads.gd +224 -0
- package/build/godot/addons/gdharness_editor/animation_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +76 -5
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +30 -143
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +4 -0
- package/build/godot/operations/animation_reads.gd +224 -0
- package/build/godot/operations/export_presets.gd +232 -0
- package/build/godot/operations/file_reads.gd +76 -5
- package/build/godot/operations/godot_operations.gd +21 -1
- package/build/godot/operations/import_pipeline.gd +4 -60
- package/build/godot/operations/input_actions.gd +51 -7
- package/build/godot/operations/project_diagnostics.gd +3 -1
- package/build/index.js +132 -31
- package/package.json +1 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## export_presets.cfg, read and written in the format the editor's Project > Export saves it in.
|
|
4
|
+
|
|
5
|
+
const Read = preload("reading.gd")
|
|
6
|
+
const Log = preload("logger.gd")
|
|
7
|
+
|
|
8
|
+
const PRESETS_FILE: String = "res://export_presets.cfg"
|
|
9
|
+
## Where the editor names the one preset of each platform it runs on a device.
|
|
10
|
+
const RUNNABLE_SECTION: String = "runnable_presets"
|
|
11
|
+
## The platforms the editor's exporter offers, by the name a preset gives its platform, each with
|
|
12
|
+
## how the names of its files in the export templates directory begin.
|
|
13
|
+
const PLATFORMS: Dictionary[String, String] = {
|
|
14
|
+
"Windows Desktop": "windows_",
|
|
15
|
+
"Linux": "linux_",
|
|
16
|
+
"macOS": "macos",
|
|
17
|
+
"Android": "android_",
|
|
18
|
+
"iOS": "ios",
|
|
19
|
+
"Web": "web_",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var _log: Log
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
func _init(p_log: Log) -> void:
|
|
26
|
+
_log = p_log
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## The names of the project's export presets, in the order export_presets.cfg holds them.
|
|
30
|
+
static func names() -> Array[String]:
|
|
31
|
+
var named: Array[String] = []
|
|
32
|
+
var config: ConfigFile = ConfigFile.new()
|
|
33
|
+
if config.load(PRESETS_FILE) != OK:
|
|
34
|
+
return named
|
|
35
|
+
var index: int = 0
|
|
36
|
+
while config.has_section("preset." + str(index)):
|
|
37
|
+
named.append(str(config.get_value("preset." + str(index), "name", "")))
|
|
38
|
+
index += 1
|
|
39
|
+
return named
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
func list_export_presets(_params: Dictionary) -> Dictionary:
|
|
43
|
+
if not FileAccess.file_exists(PRESETS_FILE):
|
|
44
|
+
return {
|
|
45
|
+
"presets": [],
|
|
46
|
+
"presets_file_exists": false,
|
|
47
|
+
"note":
|
|
48
|
+
(
|
|
49
|
+
"No export_presets.cfg found. project_export add_preset adds a preset, as the editor's "
|
|
50
|
+
+ "Project > Export does."
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
var config: ConfigFile = ConfigFile.new()
|
|
54
|
+
var err: Error = config.load(PRESETS_FILE)
|
|
55
|
+
if err != OK:
|
|
56
|
+
return _log.failure("Failed to parse export_presets.cfg: " + error_string(err))
|
|
57
|
+
|
|
58
|
+
var templates: String = _templates_dir()
|
|
59
|
+
var presets: Array[Dictionary] = []
|
|
60
|
+
var index: int = 0
|
|
61
|
+
while config.has_section("preset." + str(index)):
|
|
62
|
+
var section: String = "preset." + str(index)
|
|
63
|
+
var platform: String = str(config.get_value(section, "platform", "Unknown"))
|
|
64
|
+
var preset: Dictionary = {
|
|
65
|
+
"index": index,
|
|
66
|
+
"name": config.get_value(section, "name", "Unknown"),
|
|
67
|
+
"platform": platform,
|
|
68
|
+
"runnable": _runnable(config, section),
|
|
69
|
+
"export_path": config.get_value(section, "export_path", ""),
|
|
70
|
+
"export_filter": config.get_value(section, "export_filter", "all_resources"),
|
|
71
|
+
"include_filter": config.get_value(section, "include_filter", ""),
|
|
72
|
+
"exclude_filter": config.get_value(section, "exclude_filter", "")
|
|
73
|
+
}
|
|
74
|
+
if config.has_section_key(section, "custom_features"):
|
|
75
|
+
preset["custom_features"] = config.get_value(section, "custom_features", "")
|
|
76
|
+
# Left out for a platform this file names and the exporter does not know, which has no
|
|
77
|
+
# templates to look for.
|
|
78
|
+
if platform in PLATFORMS:
|
|
79
|
+
preset["templates_installed"] = _installed(templates, platform)
|
|
80
|
+
presets.append(preset)
|
|
81
|
+
index += 1
|
|
82
|
+
return {"presets": presets, "presets_file_exists": true, "total_presets": index, "templates": templates}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## Whether the preset in [param section] is the one its platform runs. The editor names that preset
|
|
86
|
+
## in a section of its own, and kept a key in the preset's section only in files saved before that.
|
|
87
|
+
static func _runnable(config: ConfigFile, section: String) -> bool:
|
|
88
|
+
var platform: String = str(config.get_value(section, "platform", ""))
|
|
89
|
+
if config.has_section_key(RUNNABLE_SECTION, platform):
|
|
90
|
+
return str(config.get_value(RUNNABLE_SECTION, platform)) == str(config.get_value(section, "name", ""))
|
|
91
|
+
return Read.as_bool(config.get_value(section, "runnable", false))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## Adds an export preset to export_presets.cfg, written with the keys the editor's Project > Export
|
|
95
|
+
## writes for a new one. The editor's exporter fills every platform option the file leaves out with
|
|
96
|
+
## its default when it loads the file, which is what a preset fresh from that dialog holds too.
|
|
97
|
+
func add_export_preset(params: Dictionary) -> Dictionary:
|
|
98
|
+
var preset_name: String = str(params.get("preset", "")).strip_edges()
|
|
99
|
+
var platform: String = str(params.get("platform", ""))
|
|
100
|
+
var export_path: String = str(params.get("output_path", "")).trim_prefix("res://")
|
|
101
|
+
if preset_name.is_empty():
|
|
102
|
+
return _log.failure("preset is required: the name the preset is exported by")
|
|
103
|
+
if platform not in PLATFORMS:
|
|
104
|
+
return _log.failure(
|
|
105
|
+
"Unknown platform '%s'. The platforms are %s." % [platform, ", ".join(PLATFORMS.keys())]
|
|
106
|
+
)
|
|
107
|
+
var config: ConfigFile = ConfigFile.new()
|
|
108
|
+
if FileAccess.file_exists(PRESETS_FILE):
|
|
109
|
+
var loading: Error = config.load(PRESETS_FILE)
|
|
110
|
+
if loading != OK:
|
|
111
|
+
return _log.failure("Failed to parse export_presets.cfg: " + error_string(loading))
|
|
112
|
+
var index: int = 0
|
|
113
|
+
while config.has_section("preset." + str(index)):
|
|
114
|
+
var held: String = "preset." + str(index)
|
|
115
|
+
if str(config.get_value(held, "name", "")) == preset_name:
|
|
116
|
+
return _log.failure(
|
|
117
|
+
(
|
|
118
|
+
(
|
|
119
|
+
"There is already an export preset named '%s', for %s, exporting to '%s'. "
|
|
120
|
+
+ "project_export run exports with it; give another name to add a second."
|
|
121
|
+
)
|
|
122
|
+
% [
|
|
123
|
+
preset_name,
|
|
124
|
+
config.get_value(held, "platform", ""),
|
|
125
|
+
config.get_value(held, "export_path", "")
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
index += 1
|
|
130
|
+
# A platform with no preset to run takes the new one, as the editor's dialog does.
|
|
131
|
+
var runnable: bool = not config.has_section_key(RUNNABLE_SECTION, platform)
|
|
132
|
+
if runnable:
|
|
133
|
+
config.set_value(RUNNABLE_SECTION, platform, preset_name)
|
|
134
|
+
|
|
135
|
+
var section: String = "preset." + str(index)
|
|
136
|
+
var values: Dictionary = {
|
|
137
|
+
"name": preset_name,
|
|
138
|
+
"platform": platform,
|
|
139
|
+
"dedicated_server": false,
|
|
140
|
+
"custom_features": "",
|
|
141
|
+
"export_filter": "all_resources",
|
|
142
|
+
"include_filter": "",
|
|
143
|
+
"exclude_filter": "",
|
|
144
|
+
"export_path": export_path,
|
|
145
|
+
"patches": PackedStringArray(),
|
|
146
|
+
"encryption_include_filters": "",
|
|
147
|
+
"encryption_exclude_filters": "",
|
|
148
|
+
"seed": 0,
|
|
149
|
+
"encrypt_pck": false,
|
|
150
|
+
"encrypt_directory": false,
|
|
151
|
+
"script_export_mode": 2,
|
|
152
|
+
}
|
|
153
|
+
for key: String in values:
|
|
154
|
+
config.set_value(section, key, values[key])
|
|
155
|
+
# A section with nothing in it is not written, and the editor's loader reports a missing options
|
|
156
|
+
# section as an error; these two are options of every platform, empty in a new preset.
|
|
157
|
+
config.set_value(section + ".options", "custom_template/debug", "")
|
|
158
|
+
config.set_value(section + ".options", "custom_template/release", "")
|
|
159
|
+
var saving: Error = config.save(PRESETS_FILE)
|
|
160
|
+
if saving != OK:
|
|
161
|
+
return _log.failure("Failed to save export_presets.cfg: " + error_string(saving))
|
|
162
|
+
|
|
163
|
+
var templates: String = _templates_dir()
|
|
164
|
+
var installed: bool = _installed(templates, platform)
|
|
165
|
+
var answer: Dictionary = {
|
|
166
|
+
"preset": preset_name,
|
|
167
|
+
"platform": platform,
|
|
168
|
+
"index": index,
|
|
169
|
+
"runnable": runnable,
|
|
170
|
+
"export_path": export_path,
|
|
171
|
+
"file": PRESETS_FILE,
|
|
172
|
+
"templates": templates,
|
|
173
|
+
"templates_installed": installed,
|
|
174
|
+
}
|
|
175
|
+
if not installed:
|
|
176
|
+
answer["templates_note"] = (
|
|
177
|
+
(
|
|
178
|
+
"No %s export templates for this engine are at %s, and an export for %s needs them. The "
|
|
179
|
+
+ "editor installs them in Editor > Manage Export Templates, or they can be unpacked "
|
|
180
|
+
+ "there from the export templates archive of this engine's release."
|
|
181
|
+
)
|
|
182
|
+
% [platform, templates, platform]
|
|
183
|
+
)
|
|
184
|
+
return answer
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
## Where the editor of this engine keeps its export templates: beside the executable for a
|
|
188
|
+
## self-contained one, otherwise in the user's data directory, as the editor's own paths have it.
|
|
189
|
+
static func _templates_dir() -> String:
|
|
190
|
+
var version: Dictionary = Engine.get_version_info()
|
|
191
|
+
var named: String = "%d.%d" % [Read.as_int(version.get("major", 0)), Read.as_int(version.get("minor", 0))]
|
|
192
|
+
if Read.as_int(version.get("patch", 0)) != 0:
|
|
193
|
+
named += "." + str(Read.as_int(version.get("patch", 0)))
|
|
194
|
+
named += "." + str(version.get("status", ""))
|
|
195
|
+
if ClassDB.class_exists("CSharpScript"):
|
|
196
|
+
named += ".mono"
|
|
197
|
+
var beside: String = OS.get_executable_path().get_base_dir()
|
|
198
|
+
var contained: bool = _marks_self_contained(beside)
|
|
199
|
+
# A macOS bundle is read-only, so its editor looks for the marker beside the bundle too, and keeps
|
|
200
|
+
# its data there whichever of the two places had it.
|
|
201
|
+
if (
|
|
202
|
+
OS.has_feature("macos")
|
|
203
|
+
and beside.ends_with("MacOS")
|
|
204
|
+
and beside.path_join("..").simplify_path().ends_with("Contents")
|
|
205
|
+
):
|
|
206
|
+
beside = beside.path_join("../../..").simplify_path()
|
|
207
|
+
contained = contained or _marks_self_contained(beside)
|
|
208
|
+
if contained:
|
|
209
|
+
return beside.path_join("editor_data").path_join("export_templates").path_join(named)
|
|
210
|
+
var home: String = (
|
|
211
|
+
"godot" if OS.get_name() in ["Linux", "FreeBSD", "NetBSD", "OpenBSD", "BSD"] else "Godot"
|
|
212
|
+
)
|
|
213
|
+
return OS.get_data_dir().path_join(home).path_join("export_templates").path_join(named)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
## Whether [param templates] holds any of [param platform]'s templates. The directory is one per
|
|
217
|
+
## engine version and holds whichever platforms were installed into it, so its being there says
|
|
218
|
+
## nothing about this platform.
|
|
219
|
+
static func _installed(templates: String, platform: String) -> bool:
|
|
220
|
+
if not DirAccess.dir_exists_absolute(templates):
|
|
221
|
+
return false
|
|
222
|
+
for file: String in DirAccess.get_files_at(templates):
|
|
223
|
+
if file.begins_with(PLATFORMS[platform]):
|
|
224
|
+
return true
|
|
225
|
+
return false
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
static func _marks_self_contained(directory: String) -> bool:
|
|
229
|
+
return (
|
|
230
|
+
FileAccess.file_exists(directory.path_join("._sc_"))
|
|
231
|
+
or FileAccess.file_exists(directory.path_join("_sc_"))
|
|
232
|
+
)
|
|
@@ -19,6 +19,30 @@ const Serialisation = preload("serialisation.gd")
|
|
|
19
19
|
## instances another scene and every stored property when [param include_properties] is set, the
|
|
20
20
|
## file's value or the default where it sets none. [param depth] -1 reads every level.
|
|
21
21
|
static func scene_tree(scene_path: String, depth: int, include_properties: bool) -> Dictionary:
|
|
22
|
+
var laid: Dictionary = _laid(scene_path)
|
|
23
|
+
if not laid.get("ok", false):
|
|
24
|
+
return laid
|
|
25
|
+
var nodes: Dictionary = laid["nodes"]
|
|
26
|
+
return {"ok": true, "tree": _entry(nodes, ".", include_properties, depth, 0)}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## The node at [param node_path] in [param scene_path] as {ok, name, type, properties}, laid as
|
|
30
|
+
## [method scene_tree] lays it, with only the properties the files set; {} when the scene has no
|
|
31
|
+
## node there, and a refusal when the file does not read as a scene.
|
|
32
|
+
static func scene_node(scene_path: String, node_path: String) -> Dictionary:
|
|
33
|
+
var laid: Dictionary = _laid(scene_path)
|
|
34
|
+
if not laid.get("ok", false):
|
|
35
|
+
return laid
|
|
36
|
+
var nodes: Dictionary = laid["nodes"]
|
|
37
|
+
var path: String = _joined(".", node_path)
|
|
38
|
+
if not nodes.has(path):
|
|
39
|
+
return {}
|
|
40
|
+
var found: Dictionary = nodes[path]
|
|
41
|
+
return {"ok": true, "name": found["name"], "type": found["type"], "properties": found["properties"]}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Every node of [param scene_path] as {ok, nodes}, keyed by path, or a refusal.
|
|
45
|
+
static func _laid(scene_path: String) -> Dictionary:
|
|
22
46
|
if not FileAccess.file_exists(scene_path):
|
|
23
47
|
return {"ok": false, "error": "Scene not found: " + scene_path}
|
|
24
48
|
# Loaded without naming the type, so a resource handed in by mistake loads as what it is and the
|
|
@@ -42,7 +66,7 @@ static func scene_tree(scene_path: String, depth: int, include_properties: bool)
|
|
|
42
66
|
return {"ok": false, "error": refused}
|
|
43
67
|
if not nodes.has("."):
|
|
44
68
|
return {"ok": false, "error": "%s holds no nodes" % scene_path}
|
|
45
|
-
return {"ok": true, "
|
|
69
|
+
return {"ok": true, "nodes": nodes}
|
|
46
70
|
|
|
47
71
|
|
|
48
72
|
## Lays the nodes of [param state] into [param nodes] under [param prefix], keyed by their path
|
|
@@ -192,12 +216,22 @@ static func _stored(type: String, given: Dictionary) -> Dictionary:
|
|
|
192
216
|
## What a resource file holds, in the form modify takes, so a value read here can be written back.
|
|
193
217
|
## Read from the file rather than a cached copy, since the file is what the writes change and what
|
|
194
218
|
## the project loads.
|
|
219
|
+
##
|
|
220
|
+
## [param res_path] may name a resource kept inside a file, as "res://hero.tscn::Animation_jump",
|
|
221
|
+
## the path a property holding one is read back with.
|
|
195
222
|
static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
223
|
+
var file: String = res_path.get_slice("::", 0)
|
|
224
|
+
if not FileAccess.file_exists(file):
|
|
225
|
+
return {"ok": false, "error": "No resource at " + file}
|
|
226
|
+
var loaded: Resource = ResourceLoader.load(file, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
199
227
|
if loaded == null:
|
|
200
|
-
return {"ok": false, "error": "%s did not load as a resource." %
|
|
228
|
+
return {"ok": false, "error": "%s did not load as a resource." % file}
|
|
229
|
+
if file != res_path:
|
|
230
|
+
var id: String = res_path.trim_prefix(file + "::")
|
|
231
|
+
var inner: Resource = _kept_in(loaded, file, id, {})
|
|
232
|
+
if inner == null:
|
|
233
|
+
return {"ok": false, "error": "%s keeps no resource with the id %s." % [file, id]}
|
|
234
|
+
loaded = inner
|
|
201
235
|
# A scene's own properties are its packed bundle, which says nothing a caller can use.
|
|
202
236
|
if loaded is PackedScene:
|
|
203
237
|
return {
|
|
@@ -235,3 +269,40 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
235
269
|
if script != null:
|
|
236
270
|
answer["script"] = script.resource_path
|
|
237
271
|
return answer
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
## The resource [param file] keeps under [param id], found in [param value] or anything it holds.
|
|
275
|
+
## The engine loads such a path only while the file is cached, and a read loads the file uncached,
|
|
276
|
+
## so it is looked for among what the file holds. A resource of another file is neither taken nor
|
|
277
|
+
## walked into: ids are unique only within one file, and a scene instancing another holds that
|
|
278
|
+
## file's resources under ids of the other file's.
|
|
279
|
+
static func _kept_in(value: Variant, file: String, id: String, seen: Dictionary) -> Resource:
|
|
280
|
+
if value is Resource:
|
|
281
|
+
var held: Resource = value
|
|
282
|
+
if seen.has(held.get_instance_id()):
|
|
283
|
+
return null
|
|
284
|
+
seen[held.get_instance_id()] = true
|
|
285
|
+
var at: String = held.resource_path
|
|
286
|
+
if not at.is_empty() and at.get_slice("::", 0) != file:
|
|
287
|
+
return null
|
|
288
|
+
if held.resource_scene_unique_id == id:
|
|
289
|
+
return held
|
|
290
|
+
for p: Dictionary in held.get_property_list():
|
|
291
|
+
if Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
292
|
+
var found: Resource = _kept_in(held.get(str(p.get("name", ""))), file, id, seen)
|
|
293
|
+
if found != null:
|
|
294
|
+
return found
|
|
295
|
+
elif value is Array:
|
|
296
|
+
var items: Array = value
|
|
297
|
+
for item: Variant in items:
|
|
298
|
+
var found: Resource = _kept_in(item, file, id, seen)
|
|
299
|
+
if found != null:
|
|
300
|
+
return found
|
|
301
|
+
elif value is Dictionary:
|
|
302
|
+
var entries: Dictionary = value
|
|
303
|
+
for key: Variant in entries:
|
|
304
|
+
for side: Variant in [key, entries[key]]:
|
|
305
|
+
var found: Resource = _kept_in(side, file, id, seen)
|
|
306
|
+
if found != null:
|
|
307
|
+
return found
|
|
308
|
+
return null
|
|
@@ -14,10 +14,12 @@ extends SceneTree
|
|
|
14
14
|
# the operations directory ships inside the server package and is handed to the engine as an
|
|
15
15
|
# absolute path outside the project. A res:// path would only resolve in the test fixture.
|
|
16
16
|
|
|
17
|
+
const AnimationReads = preload("animation_reads.gd")
|
|
17
18
|
const AudioBuses = preload("audio_buses.gd")
|
|
18
19
|
const ClassCache = preload("class_cache.gd")
|
|
19
20
|
const ClassDbQueries = preload("classdb_queries.gd")
|
|
20
21
|
const Dependencies = preload("dependencies.gd")
|
|
22
|
+
const ExportPresets = preload("export_presets.gd")
|
|
21
23
|
const FileReads = preload("file_reads.gd")
|
|
22
24
|
const GdscriptAnalysis = preload("gdscript_analysis.gd")
|
|
23
25
|
const GdscriptAuthoring = preload("gdscript_authoring.gd")
|
|
@@ -153,6 +155,22 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
|
|
|
153
155
|
str(params.get("resource_path", "")), Read.as_bool(params.get("include_defaults", false))
|
|
154
156
|
)
|
|
155
157
|
)
|
|
158
|
+
"read_animations":
|
|
159
|
+
payload = _read(
|
|
160
|
+
AnimationReads.scene_animations(
|
|
161
|
+
str(params.get("scene_path", "")),
|
|
162
|
+
str(params.get("player_node_path", ".")),
|
|
163
|
+
str(params.get("animation_name", ""))
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
"read_animation_states":
|
|
167
|
+
payload = _read(
|
|
168
|
+
AnimationReads.scene_states(
|
|
169
|
+
str(params.get("scene_path", "")),
|
|
170
|
+
str(params.get("anim_tree_path", "")),
|
|
171
|
+
str(params.get("state_machine_path", ""))
|
|
172
|
+
)
|
|
173
|
+
)
|
|
156
174
|
|
|
157
175
|
# Import and export pipeline
|
|
158
176
|
"get_import_status":
|
|
@@ -162,7 +180,9 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
|
|
|
162
180
|
"set_import_options":
|
|
163
181
|
payload = ImportPipeline.new(_log).set_import_options(params)
|
|
164
182
|
"list_export_presets":
|
|
165
|
-
payload =
|
|
183
|
+
payload = ExportPresets.new(_log).list_export_presets(params)
|
|
184
|
+
"add_export_preset":
|
|
185
|
+
payload = ExportPresets.new(_log).add_export_preset(params)
|
|
166
186
|
"validate_project":
|
|
167
187
|
payload = ImportPipeline.new(_log).validate_project(params)
|
|
168
188
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
+
const ExportPresets = preload("export_presets.gd")
|
|
3
4
|
const Read = preload("reading.gd")
|
|
4
5
|
const Serialisation = preload("serialisation.gd")
|
|
5
6
|
const FileWalk = preload("file_walk.gd")
|
|
@@ -319,63 +320,6 @@ static func _all_ints(held: Variant, keys: bool) -> bool:
|
|
|
319
320
|
return true
|
|
320
321
|
|
|
321
322
|
|
|
322
|
-
# The names of the project's export presets, in the order export_presets.cfg holds them.
|
|
323
|
-
static func _preset_names() -> Array[String]:
|
|
324
|
-
var names: Array[String] = []
|
|
325
|
-
var config: ConfigFile = ConfigFile.new()
|
|
326
|
-
if config.load("res://export_presets.cfg") != OK:
|
|
327
|
-
return names
|
|
328
|
-
var index: int = 0
|
|
329
|
-
while config.has_section("preset." + str(index)):
|
|
330
|
-
names.append(str(config.get_value("preset." + str(index), "name", "")))
|
|
331
|
-
index += 1
|
|
332
|
-
return names
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
# List export presets
|
|
336
|
-
func list_export_presets(_params: Dictionary) -> Dictionary:
|
|
337
|
-
_log.info("Listing export presets")
|
|
338
|
-
|
|
339
|
-
var presets_file: String = "res://export_presets.cfg"
|
|
340
|
-
|
|
341
|
-
if not FileAccess.file_exists(presets_file):
|
|
342
|
-
return {
|
|
343
|
-
"presets": [],
|
|
344
|
-
"presets_file_exists": false,
|
|
345
|
-
"note": "No export_presets.cfg found. Configure export presets in the Godot editor."
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
var config: ConfigFile = ConfigFile.new()
|
|
349
|
-
var err: Error = config.load(presets_file)
|
|
350
|
-
|
|
351
|
-
if err != OK:
|
|
352
|
-
return _log.failure("Failed to parse export_presets.cfg: " + str(err))
|
|
353
|
-
|
|
354
|
-
# Export presets are stored as [preset.0], [preset.1], etc.
|
|
355
|
-
var presets: Array[Dictionary] = []
|
|
356
|
-
var preset_idx: int = 0
|
|
357
|
-
while config.has_section("preset." + str(preset_idx)):
|
|
358
|
-
var section: String = "preset." + str(preset_idx)
|
|
359
|
-
var preset: Dictionary = {
|
|
360
|
-
"index": preset_idx,
|
|
361
|
-
"name": config.get_value(section, "name", "Unknown"),
|
|
362
|
-
"platform": config.get_value(section, "platform", "Unknown"),
|
|
363
|
-
"runnable": config.get_value(section, "runnable", false),
|
|
364
|
-
"export_path": config.get_value(section, "export_path", ""),
|
|
365
|
-
"export_filter": config.get_value(section, "export_filter", "all_resources"),
|
|
366
|
-
"include_filter": config.get_value(section, "include_filter", ""),
|
|
367
|
-
"exclude_filter": config.get_value(section, "exclude_filter", "")
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if config.has_section_key(section, "custom_features"):
|
|
371
|
-
preset["custom_features"] = config.get_value(section, "custom_features", "")
|
|
372
|
-
|
|
373
|
-
presets.append(preset)
|
|
374
|
-
preset_idx += 1
|
|
375
|
-
|
|
376
|
-
return {"presets": presets, "presets_file_exists": true, "total_presets": preset_idx}
|
|
377
|
-
|
|
378
|
-
|
|
379
323
|
# Validate project for export
|
|
380
324
|
func validate_project(params: Dictionary) -> Dictionary:
|
|
381
325
|
var preset_name: String = str(params.get("preset", ""))
|
|
@@ -429,7 +373,7 @@ func validate_project(params: Dictionary) -> Dictionary:
|
|
|
429
373
|
"warning",
|
|
430
374
|
"export_presets",
|
|
431
375
|
"No export presets configured",
|
|
432
|
-
"
|
|
376
|
+
"Add a preset with project_export add_preset, or in the editor: Project > Export",
|
|
433
377
|
include_suggestions
|
|
434
378
|
)
|
|
435
379
|
)
|
|
@@ -438,7 +382,7 @@ func validate_project(params: Dictionary) -> Dictionary:
|
|
|
438
382
|
# does not exist answered valid, as though it had been checked.
|
|
439
383
|
if not preset_name.is_empty():
|
|
440
384
|
checks_performed.append("export_preset")
|
|
441
|
-
var named: Array[String] =
|
|
385
|
+
var named: Array[String] = ExportPresets.names()
|
|
442
386
|
if preset_name not in named:
|
|
443
387
|
issues.append(
|
|
444
388
|
_finding(
|
|
@@ -453,7 +397,7 @@ func validate_project(params: Dictionary) -> Dictionary:
|
|
|
453
397
|
else ": the presets are " + ", ".join(named)
|
|
454
398
|
)
|
|
455
399
|
),
|
|
456
|
-
"Pass one of the preset names, or add the preset
|
|
400
|
+
"Pass one of the preset names, or add the preset with project_export add_preset",
|
|
457
401
|
include_suggestions
|
|
458
402
|
)
|
|
459
403
|
)
|
|
@@ -170,19 +170,63 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
170
170
|
if action.is_empty():
|
|
171
171
|
return {}
|
|
172
172
|
|
|
173
|
+
# An action that exists is rebound, which is how one of the engine's ui_ actions is changed, so
|
|
174
|
+
# what it held is answered rather than lost without a word.
|
|
175
|
+
var setting: String = "input/" + action_name
|
|
176
|
+
var answer: Dictionary = {
|
|
177
|
+
"action_name": action_name,
|
|
178
|
+
"events_count": events_config.size(),
|
|
179
|
+
"deadzone": deadzone,
|
|
180
|
+
"events": events_config,
|
|
181
|
+
"was_new": not ProjectSettings.has_setting(setting),
|
|
182
|
+
}
|
|
183
|
+
if not answer["was_new"]:
|
|
184
|
+
var held: Variant = ProjectSettings.get_setting(setting)
|
|
185
|
+
var old: Dictionary = held if held is Dictionary else {}
|
|
186
|
+
var old_events: Array[Dictionary] = []
|
|
187
|
+
var listed: Variant = old.get("events", [])
|
|
188
|
+
if listed is Array:
|
|
189
|
+
var events_held: Array = listed
|
|
190
|
+
for event: Variant in events_held:
|
|
191
|
+
if event is InputEvent:
|
|
192
|
+
var bound: InputEvent = event
|
|
193
|
+
old_events.append(_shaped(bound))
|
|
194
|
+
answer["old_events"] = old_events
|
|
195
|
+
answer["old_deadzone"] = Read.as_float(old.get("deadzone", 0.5), 0.5)
|
|
196
|
+
|
|
173
197
|
# Through ProjectSettings so the file is saved the way the editor saves it, header and
|
|
174
198
|
# every other line kept; a ConfigFile of project.godot drops the comments on the way out.
|
|
175
|
-
ProjectSettings.set_setting(
|
|
199
|
+
ProjectSettings.set_setting(setting, action)
|
|
176
200
|
var err: Error = ProjectFile.save()
|
|
177
201
|
if err != OK:
|
|
178
202
|
return _log.failure("Failed to save project.godot: " + error_string(err))
|
|
179
203
|
|
|
180
|
-
return
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
204
|
+
return answer
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
## [param event] in the form add_input_action answers its events with, and as the engine names it
|
|
208
|
+
## under text, which also covers a kind of event that form has no fields for.
|
|
209
|
+
static func _shaped(event: InputEvent) -> Dictionary:
|
|
210
|
+
var shaped: Dictionary = {"class_name": event.get_class(), "text": event.as_text()}
|
|
211
|
+
if event is InputEventKey:
|
|
212
|
+
var key: InputEventKey = event
|
|
213
|
+
shaped["keycode"] = key.keycode
|
|
214
|
+
if key.physical_keycode != KEY_NONE:
|
|
215
|
+
shaped["physical_keycode"] = key.physical_keycode
|
|
216
|
+
for modifier: String in ["ctrl_pressed", "alt_pressed", "shift_pressed"]:
|
|
217
|
+
if key.get(modifier):
|
|
218
|
+
shaped[modifier] = true
|
|
219
|
+
elif event is InputEventMouseButton:
|
|
220
|
+
var mouse: InputEventMouseButton = event
|
|
221
|
+
shaped["button_index"] = mouse.button_index
|
|
222
|
+
elif event is InputEventJoypadButton:
|
|
223
|
+
var pad: InputEventJoypadButton = event
|
|
224
|
+
shaped["button_index"] = pad.button_index
|
|
225
|
+
elif event is InputEventJoypadMotion:
|
|
226
|
+
var motion: InputEventJoypadMotion = event
|
|
227
|
+
shaped["axis"] = motion.axis
|
|
228
|
+
shaped["axis_value"] = motion.axis_value
|
|
229
|
+
return shaped
|
|
186
230
|
|
|
187
231
|
|
|
188
232
|
# Remove an input action the project defines, which add_input_action had no way back from.
|
|
@@ -148,7 +148,9 @@ func get_project_health(params: Dictionary) -> Dictionary:
|
|
|
148
148
|
recommendations.append("Address critical issues to improve project stability")
|
|
149
149
|
# A project still being made has no presets yet, which is no fault in it.
|
|
150
150
|
if "config" in check_categories and not FileAccess.file_exists("res://export_presets.cfg"):
|
|
151
|
-
recommendations.append(
|
|
151
|
+
recommendations.append(
|
|
152
|
+
"No export presets are configured: project_export add_preset adds one before exporting"
|
|
153
|
+
)
|
|
152
154
|
|
|
153
155
|
# Every detail costs points, so each is an issue; `issues` was always empty beside them, and a
|
|
154
156
|
# category said passed next to the failure it had just found.
|