gdharness 1.3.8 → 1.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +4668 -3229
- package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
- package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
- package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
- package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
- package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
- package/build/godot/addons/gdharness_editor/reading.gd +112 -6
- package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
- package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
- package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
- package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
- package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
- package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
- package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
- package/build/godot/operations/animation_reads.gd +115 -4
- package/build/godot/operations/audio_buses.gd +93 -7
- package/build/godot/operations/classdb_queries.gd +119 -7
- package/build/godot/operations/declared_values.gd +272 -0
- package/build/godot/operations/dependencies.gd +154 -41
- package/build/godot/operations/editor_tools.gd +29 -13
- package/build/godot/operations/export_presets.gd +602 -4
- package/build/godot/operations/file_reads.gd +115 -18
- package/build/godot/operations/gdscript_analysis.gd +14 -0
- package/build/godot/operations/gdscript_authoring.gd +12 -2
- package/build/godot/operations/godot_operations.gd +18 -2
- package/build/godot/operations/import_pipeline.gd +20 -221
- package/build/godot/operations/input_actions.gd +127 -51
- package/build/godot/operations/project_config.gd +113 -68
- package/build/godot/operations/project_diagnostics.gd +4 -0
- package/build/godot/operations/project_validation.gd +266 -0
- package/build/godot/operations/property_paths.gd +89 -16
- package/build/godot/operations/property_values.gd +180 -121
- package/build/godot/operations/reading.gd +112 -6
- package/build/godot/operations/scene_connections.gd +222 -0
- package/build/godot/operations/serialisation.gd +19 -29
- package/build/godot/operations/setting_reads.gd +121 -0
- package/build/godot/operations/value_shapes.gd +128 -1
- package/build/godot/tests/listed.gd +20 -13
- package/build/keeper.js +1402 -707
- package/package.json +1 -1
|
@@ -19,6 +19,27 @@ const PLATFORMS: Dictionary[String, String] = {
|
|
|
19
19
|
"Web": "web_",
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
## Each platform's exporter, which only an editor can make: the options a preset of it takes are its.
|
|
23
|
+
const EXPORTERS: Dictionary[String, String] = {
|
|
24
|
+
"Windows Desktop": "EditorExportPlatformWindows",
|
|
25
|
+
"Linux": "EditorExportPlatformLinuxBSD",
|
|
26
|
+
"macOS": "EditorExportPlatformMacOS",
|
|
27
|
+
"Android": "EditorExportPlatformAndroid",
|
|
28
|
+
"iOS": "EditorExportPlatformIOS",
|
|
29
|
+
"Web": "EditorExportPlatformWeb",
|
|
30
|
+
}
|
|
31
|
+
## What export_filter takes, as the Export dialog's Resources tab names them: everything, the scenes
|
|
32
|
+
## listed under export_files and what they use, the resources listed, or everything but those listed.
|
|
33
|
+
const FILTERS: Array[String] = ["all_resources", "scenes", "resources", "exclude"]
|
|
34
|
+
## Where the editor keeps a preset's secret options, such as a signing password, out of
|
|
35
|
+
## export_presets.cfg, which a project commits.
|
|
36
|
+
const CREDENTIALS_FILE: String = "res://.godot/export_credentials.cfg"
|
|
37
|
+
## Options of every platform that a preset lists only with the Export dialog's Advanced Options on,
|
|
38
|
+
## a switch no script can reach.
|
|
39
|
+
const TEMPLATE_OPTIONS: Array[String] = ["custom_template/debug", "custom_template/release"]
|
|
40
|
+
## The types of option a value is written for. An option of another type is left to the editor.
|
|
41
|
+
const WRITABLE: Array[int] = [TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING, TYPE_PACKED_STRING_ARRAY]
|
|
42
|
+
|
|
22
43
|
var _log: GdhLog
|
|
23
44
|
|
|
24
45
|
|
|
@@ -92,8 +113,10 @@ static func _runnable(config: ConfigFile, section: String) -> bool:
|
|
|
92
113
|
|
|
93
114
|
|
|
94
115
|
## Adds an export preset to export_presets.cfg, written with the keys the editor's Project > Export
|
|
95
|
-
## writes for a new one
|
|
96
|
-
##
|
|
116
|
+
## writes for a new one, and every option in [code]known_options[/code] (what
|
|
117
|
+
## [method export_options] answers for the platform) at its default. The editor also writes the
|
|
118
|
+
## options a new preset hides until another option shows them; its exporter fills those, and any
|
|
119
|
+
## option the file leaves out, with their defaults when it loads the file.
|
|
97
120
|
func add_export_preset(params: Dictionary) -> Dictionary:
|
|
98
121
|
var preset_name: String = str(params.get("preset", "")).strip_edges()
|
|
99
122
|
var platform: String = str(params.get("platform", ""))
|
|
@@ -150,10 +173,23 @@ func add_export_preset(params: Dictionary) -> Dictionary:
|
|
|
150
173
|
}
|
|
151
174
|
for key: String in values:
|
|
152
175
|
config.set_value(section, key, values[key])
|
|
176
|
+
# A secret's default stays out of the file, as the editor keeps secrets in CREDENTIALS_FILE.
|
|
177
|
+
var known: Variant = params.get("known_options", [])
|
|
178
|
+
if known is Array:
|
|
179
|
+
for option: Variant in known:
|
|
180
|
+
if option is Dictionary:
|
|
181
|
+
var described: Dictionary = option
|
|
182
|
+
if not GdhRead.as_bool(described.get("secret", false)):
|
|
183
|
+
config.set_value(
|
|
184
|
+
section + ".options",
|
|
185
|
+
str(described.get("name", "")),
|
|
186
|
+
str_to_var(str(described.get("defaultText", "null")))
|
|
187
|
+
)
|
|
153
188
|
# A section with nothing in it is not written, and the editor's loader reports a missing options
|
|
154
189
|
# section as an error; these two are options of every platform, empty in a new preset.
|
|
155
|
-
|
|
156
|
-
|
|
190
|
+
for name: String in TEMPLATE_OPTIONS:
|
|
191
|
+
if not config.has_section_key(section + ".options", name):
|
|
192
|
+
config.set_value(section + ".options", name, "")
|
|
157
193
|
var saving: Error = config.save(PRESETS_FILE)
|
|
158
194
|
if saving != OK:
|
|
159
195
|
return _log.failure("Failed to save export_presets.cfg: " + error_string(saving))
|
|
@@ -230,3 +266,565 @@ static func _marks_self_contained(directory: String) -> bool:
|
|
|
230
266
|
FileAccess.file_exists(directory.path_join("._sc_"))
|
|
231
267
|
or FileAccess.file_exists(directory.path_join("_sc_"))
|
|
232
268
|
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
## The options a preset of [param params]'s platform lists: the name, the type, the hint and hint
|
|
272
|
+
## string of one that is an enum or a list, the default (also as Godot text, which keeps its type),
|
|
273
|
+
## and whether the editor keeps it as a secret. An exporter lists an option only while the preset's
|
|
274
|
+
## other options make it visible, as codesign/identity shows once codesign/enable is on, so the
|
|
275
|
+
## options the preset [code]preset[/code] holds in [code]presets_file[/code] are applied first, then
|
|
276
|
+
## those [code]asked[/code] gives, until nothing more comes into view.
|
|
277
|
+
func export_options(params: Dictionary) -> Dictionary:
|
|
278
|
+
var platform: String = str(params.get("platform", ""))
|
|
279
|
+
if platform not in EXPORTERS:
|
|
280
|
+
return _log.failure(
|
|
281
|
+
"Unknown platform '%s'. The platforms are %s." % [platform, ", ".join(EXPORTERS.keys())]
|
|
282
|
+
)
|
|
283
|
+
if not Engine.is_editor_hint():
|
|
284
|
+
return _log.failure("The export options are read in an engine started with --editor.")
|
|
285
|
+
var exporter: Variant = ClassDB.instantiate(EXPORTERS[platform])
|
|
286
|
+
if not exporter is Object:
|
|
287
|
+
return _log.failure("The engine made no %s exporter." % platform)
|
|
288
|
+
var made: Object = exporter
|
|
289
|
+
var preset: Variant = made.call("create_preset")
|
|
290
|
+
var standing: Variant = made.call("create_preset")
|
|
291
|
+
if not preset is Object or not standing is Object:
|
|
292
|
+
return _log.failure("The %s exporter made no preset." % platform)
|
|
293
|
+
var fresh: Object = preset
|
|
294
|
+
var state: Object = standing
|
|
295
|
+
var presets_file: String = str(params.get("presets_file", ""))
|
|
296
|
+
var config: ConfigFile = ConfigFile.new()
|
|
297
|
+
if not presets_file.is_empty() and config.load(presets_file) == OK:
|
|
298
|
+
var index: int = 0
|
|
299
|
+
while config.has_section("preset." + str(index)):
|
|
300
|
+
var section: String = "preset." + str(index)
|
|
301
|
+
if str(config.get_value(section, "name", "")) == str(params.get("preset", "")):
|
|
302
|
+
if config.has_section(section + ".options"):
|
|
303
|
+
for key: String in config.get_section_keys(section + ".options"):
|
|
304
|
+
state.set(key, config.get_value(section + ".options", key))
|
|
305
|
+
break
|
|
306
|
+
index += 1
|
|
307
|
+
var asked: Dictionary = {}
|
|
308
|
+
if params.get("asked") is Dictionary:
|
|
309
|
+
asked = params["asked"]
|
|
310
|
+
var pending: Array = asked.keys()
|
|
311
|
+
var listed: Dictionary = _options_shown(state, fresh)
|
|
312
|
+
var came: bool = true
|
|
313
|
+
while came and not pending.is_empty():
|
|
314
|
+
came = false
|
|
315
|
+
for key: Variant in pending.duplicate():
|
|
316
|
+
var name: String = str(key)
|
|
317
|
+
if not listed.has(name):
|
|
318
|
+
continue
|
|
319
|
+
pending.erase(key)
|
|
320
|
+
var option: Dictionary = listed[name]
|
|
321
|
+
var fitted: Variant = _fitted(name, asked[key], option)
|
|
322
|
+
if not _is_refusal(fitted):
|
|
323
|
+
state.set(name, fitted)
|
|
324
|
+
came = true
|
|
325
|
+
listed = _options_shown(state, fresh)
|
|
326
|
+
# Every asked option the preset would not list, including one an earlier round listed and a later
|
|
327
|
+
# one hid, as turning codesign/enable off in the same call hides codesign/identity.
|
|
328
|
+
var hidden: Dictionary = {}
|
|
329
|
+
for key: Variant in asked:
|
|
330
|
+
if listed.has(str(key)):
|
|
331
|
+
continue
|
|
332
|
+
var shown_by: String = _shown_by(state, str(key), listed)
|
|
333
|
+
if not shown_by.is_empty():
|
|
334
|
+
hidden[str(key)] = shown_by
|
|
335
|
+
return {"platform": platform, "options": listed.values(), "hidden": hidden}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
## Which one change to a listed true-or-false or enum option of [param state] would list
|
|
339
|
+
## [param name], as "codesign/enable is true", or an empty string when none would.
|
|
340
|
+
static func _shown_by(state: Object, name: String, listed: Dictionary) -> String:
|
|
341
|
+
for option: Dictionary in listed.values():
|
|
342
|
+
var other: String = option["name"]
|
|
343
|
+
var held: Variant = state.get(other)
|
|
344
|
+
var tries: Dictionary = {}
|
|
345
|
+
if option["type"] == TYPE_BOOL:
|
|
346
|
+
tries[not held] = str(not held)
|
|
347
|
+
elif option["type"] == TYPE_INT and option["hint"] == PROPERTY_HINT_ENUM:
|
|
348
|
+
var choices: PackedStringArray = str(option["hintString"]).split(",", false)
|
|
349
|
+
for at: int in choices.size():
|
|
350
|
+
var label: String = choices[at].get_slice(":", 0)
|
|
351
|
+
tries[int(choices[at].get_slice(":", 1)) if choices[at].contains(":") else at] = label
|
|
352
|
+
for value: Variant in tries:
|
|
353
|
+
if _same(value, held):
|
|
354
|
+
continue
|
|
355
|
+
state.set(other, value)
|
|
356
|
+
var shows: bool = _lists(state, name)
|
|
357
|
+
state.set(other, held)
|
|
358
|
+
if shows:
|
|
359
|
+
return "%s is %s" % [other, tries[value]]
|
|
360
|
+
return ""
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
static func _lists(state: Object, name: String) -> bool:
|
|
364
|
+
for property: Dictionary in state.get_property_list():
|
|
365
|
+
if str(property.get("name", "")) == name:
|
|
366
|
+
return true
|
|
367
|
+
return false
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
## Each option [param state] lists, by name, described with the default [param fresh] holds, and
|
|
371
|
+
## the options every platform has behind Advanced Options.
|
|
372
|
+
static func _options_shown(state: Object, fresh: Object) -> Dictionary:
|
|
373
|
+
var listed: Dictionary = {}
|
|
374
|
+
for property: Dictionary in state.get_property_list():
|
|
375
|
+
var usage: int = GdhRead.as_int(property.get("usage", 0))
|
|
376
|
+
var name: String = str(property.get("name", ""))
|
|
377
|
+
if usage & PROPERTY_USAGE_CATEGORY or usage & PROPERTY_USAGE_GROUP or not name.contains("/"):
|
|
378
|
+
continue
|
|
379
|
+
listed[name] = {
|
|
380
|
+
"name": name,
|
|
381
|
+
"type": GdhRead.as_int(property.get("type", TYPE_NIL)),
|
|
382
|
+
"hint": GdhRead.as_int(property.get("hint", PROPERTY_HINT_NONE)),
|
|
383
|
+
"hintString": str(property.get("hint_string", "")),
|
|
384
|
+
"default": fresh.get(name),
|
|
385
|
+
"defaultText": var_to_str(fresh.get(name)),
|
|
386
|
+
"secret": usage & PROPERTY_USAGE_SECRET != 0,
|
|
387
|
+
}
|
|
388
|
+
for name: String in TEMPLATE_OPTIONS:
|
|
389
|
+
if not listed.has(name):
|
|
390
|
+
listed[name] = {
|
|
391
|
+
"name": name,
|
|
392
|
+
"type": TYPE_STRING,
|
|
393
|
+
"hint": PROPERTY_HINT_GLOBAL_FILE,
|
|
394
|
+
"hintString": "",
|
|
395
|
+
"default": "",
|
|
396
|
+
"defaultText": var_to_str(""),
|
|
397
|
+
"secret": false,
|
|
398
|
+
}
|
|
399
|
+
return listed
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
## Changes what [param params] gives of a preset in export_presets.cfg: its export path, its
|
|
403
|
+
## resource filter and the files it names, its include and exclude filters, its custom features and
|
|
404
|
+
## its platform's options, each option checked against [code]known_options[/code], the list
|
|
405
|
+
## [method export_options] answers for the preset's platform. Nothing is written when any of it is
|
|
406
|
+
## refused.
|
|
407
|
+
func set_export_preset(params: Dictionary) -> Dictionary:
|
|
408
|
+
var loaded: Variant = _preset_named(str(params.get("preset", "")).strip_edges())
|
|
409
|
+
if loaded is String:
|
|
410
|
+
return _log.failure(str(loaded))
|
|
411
|
+
var found: Dictionary = loaded
|
|
412
|
+
var config: ConfigFile = found["config"]
|
|
413
|
+
var section: String = found["section"]
|
|
414
|
+
var platform: String = str(config.get_value(section, "platform", ""))
|
|
415
|
+
var top: Dictionary = {}
|
|
416
|
+
if params.has("output_path"):
|
|
417
|
+
top["export_path"] = str(params["output_path"]).trim_prefix("res://")
|
|
418
|
+
for key: String in ["include_filter", "exclude_filter", "custom_features"]:
|
|
419
|
+
if params.has(key):
|
|
420
|
+
top[key] = str(params[key])
|
|
421
|
+
var filter: String = str(
|
|
422
|
+
params.get("export_filter", config.get_value(section, "export_filter", "all_resources"))
|
|
423
|
+
)
|
|
424
|
+
if params.has("export_filter"):
|
|
425
|
+
if filter not in FILTERS:
|
|
426
|
+
return _log.failure(GdhRead.offered("export_filter takes no %s" % filter, filter, FILTERS) + ".")
|
|
427
|
+
top["export_filter"] = filter
|
|
428
|
+
if params.has("files"):
|
|
429
|
+
var listed: Variant = _files_for(filter, params["files"])
|
|
430
|
+
if listed is String:
|
|
431
|
+
return _log.failure(str(listed))
|
|
432
|
+
top["export_files"] = listed
|
|
433
|
+
elif filter != "all_resources" and params.has("export_filter"):
|
|
434
|
+
var kept: Variant = config.get_value(section, "export_files", PackedStringArray())
|
|
435
|
+
var kept_files: PackedStringArray = kept if kept is PackedStringArray else PackedStringArray()
|
|
436
|
+
if kept is PackedStringArray and kept_files.is_empty():
|
|
437
|
+
return (
|
|
438
|
+
_log
|
|
439
|
+
. failure(
|
|
440
|
+
(
|
|
441
|
+
"export_filter %s exports the files the preset lists, and it lists none: give them in files."
|
|
442
|
+
% filter
|
|
443
|
+
)
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
var options: Variant = params.get("options", {})
|
|
447
|
+
if not options is Dictionary:
|
|
448
|
+
return _log.failure("options is an object of option names and values.")
|
|
449
|
+
var asked: Dictionary = options
|
|
450
|
+
var known: Variant = params.get("known_options", [])
|
|
451
|
+
var hidden: Dictionary = {}
|
|
452
|
+
if params.get("hidden_options") is Dictionary:
|
|
453
|
+
hidden = params["hidden_options"]
|
|
454
|
+
var written: Variant = _options_for(platform, asked, known, hidden)
|
|
455
|
+
if written is String:
|
|
456
|
+
return _log.failure(str(written))
|
|
457
|
+
var values: Dictionary = written
|
|
458
|
+
if top.is_empty() and values.is_empty():
|
|
459
|
+
return _log.failure(
|
|
460
|
+
(
|
|
461
|
+
"Nothing to change: give outputPath, exportFilter, files, includeFilter, excludeFilter, "
|
|
462
|
+
+ "customFeatures or options."
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
var changed: Dictionary = {}
|
|
466
|
+
var unchanged: Array[String] = []
|
|
467
|
+
for key: String in top:
|
|
468
|
+
var before: Variant = config.get_value(section, key) if config.has_section_key(section, key) else null
|
|
469
|
+
if _same(before, top[key]):
|
|
470
|
+
unchanged.append(key)
|
|
471
|
+
continue
|
|
472
|
+
changed[key] = {"before": before, "now": top[key]}
|
|
473
|
+
config.set_value(section, key, top[key])
|
|
474
|
+
var described: Dictionary = {}
|
|
475
|
+
if known is Array:
|
|
476
|
+
for option: Variant in known:
|
|
477
|
+
if option is Dictionary:
|
|
478
|
+
var one: Dictionary = option
|
|
479
|
+
described[str(one.get("name", ""))] = one
|
|
480
|
+
var credentials: ConfigFile = ConfigFile.new()
|
|
481
|
+
var _no_credentials: Error = credentials.load(CREDENTIALS_FILE)
|
|
482
|
+
var secrets: Array[String] = []
|
|
483
|
+
var missing: Array[String] = []
|
|
484
|
+
var missing_secrets: Array[String] = []
|
|
485
|
+
for name: String in values:
|
|
486
|
+
var option: Dictionary = described.get(name, {})
|
|
487
|
+
var secret: bool = GdhRead.as_bool(option.get("secret", false))
|
|
488
|
+
# The editor writes a secret where the project's version control does not reach.
|
|
489
|
+
var holder: ConfigFile = credentials if secret else config
|
|
490
|
+
var held: Variant = str_to_var(str(option.get("defaultText", "null")))
|
|
491
|
+
if holder.has_section_key(section + ".options", name):
|
|
492
|
+
held = holder.get_value(section + ".options", name)
|
|
493
|
+
if _same(held, values[name]):
|
|
494
|
+
unchanged.append(name)
|
|
495
|
+
continue
|
|
496
|
+
holder.set_value(section + ".options", name, values[name])
|
|
497
|
+
if secret:
|
|
498
|
+
secrets.append(name)
|
|
499
|
+
changed[name] = {"secret": true}
|
|
500
|
+
else:
|
|
501
|
+
changed[name] = {"before": held, "now": values[name]}
|
|
502
|
+
var gone: String = _missing_path(values[name], option)
|
|
503
|
+
if not gone.is_empty() and secret:
|
|
504
|
+
missing_secrets.append(name)
|
|
505
|
+
elif not gone.is_empty():
|
|
506
|
+
missing.append(gone)
|
|
507
|
+
var answer: Dictionary = {
|
|
508
|
+
"preset": config.get_value(section, "name"),
|
|
509
|
+
"platform": platform,
|
|
510
|
+
"changed": changed,
|
|
511
|
+
"file": PRESETS_FILE,
|
|
512
|
+
}
|
|
513
|
+
if not unchanged.is_empty():
|
|
514
|
+
answer["unchanged"] = unchanged
|
|
515
|
+
if changed.is_empty():
|
|
516
|
+
answer["saved"] = false
|
|
517
|
+
return answer
|
|
518
|
+
if not secrets.is_empty():
|
|
519
|
+
var _made: Error = DirAccess.make_dir_recursive_absolute(CREDENTIALS_FILE.get_base_dir())
|
|
520
|
+
var keeping: Error = credentials.save(CREDENTIALS_FILE)
|
|
521
|
+
if keeping != OK:
|
|
522
|
+
return _log.failure("Failed to save %s: %s" % [CREDENTIALS_FILE, error_string(keeping)])
|
|
523
|
+
answer["credentials"] = CREDENTIALS_FILE
|
|
524
|
+
answer["secretNote"] = (
|
|
525
|
+
(
|
|
526
|
+
"%s %s secret, so %s written to %s, where the editor keeps secrets out of the "
|
|
527
|
+
+ "export_presets.cfg a project commits, and %s not answered."
|
|
528
|
+
)
|
|
529
|
+
% [
|
|
530
|
+
_and_joined(secrets),
|
|
531
|
+
"is a" if secrets.size() == 1 else "are",
|
|
532
|
+
"it is" if secrets.size() == 1 else "they are",
|
|
533
|
+
CREDENTIALS_FILE,
|
|
534
|
+
"its value is" if secrets.size() == 1 else "their values are",
|
|
535
|
+
]
|
|
536
|
+
)
|
|
537
|
+
var saving: Error = config.save(PRESETS_FILE)
|
|
538
|
+
if saving != OK:
|
|
539
|
+
return _log.failure("Failed to save export_presets.cfg: " + error_string(saving))
|
|
540
|
+
# Written as given, since the file can come after the preset names it; said, since an export can
|
|
541
|
+
# fail on it until it does. A secret's path is a secret too, so only its option is named.
|
|
542
|
+
if not missing.is_empty():
|
|
543
|
+
answer["missing_files"] = missing
|
|
544
|
+
if not missing_secrets.is_empty():
|
|
545
|
+
answer["secretsNamingNoFile"] = missing_secrets
|
|
546
|
+
var absent: Array[String] = missing.duplicate()
|
|
547
|
+
for name: String in missing_secrets:
|
|
548
|
+
absent.append("the file %s names" % name)
|
|
549
|
+
if not absent.is_empty():
|
|
550
|
+
answer["note"] = (
|
|
551
|
+
(
|
|
552
|
+
"%s %s not exist, and an exporter can refuse the preset until %s, as the Windows "
|
|
553
|
+
+ "exporter refuses a missing icon as an invalid icon path."
|
|
554
|
+
)
|
|
555
|
+
% [
|
|
556
|
+
_and_joined(absent),
|
|
557
|
+
"does" if absent.size() == 1 else "do",
|
|
558
|
+
"it does" if absent.size() == 1 else "they do",
|
|
559
|
+
]
|
|
560
|
+
)
|
|
561
|
+
return answer
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
static func _and_joined(items: Array[String]) -> String:
|
|
565
|
+
if items.size() < 2:
|
|
566
|
+
return "".join(items)
|
|
567
|
+
return ", ".join(items.slice(0, items.size() - 1)) + " and " + items[items.size() - 1]
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
## Removes a preset from export_presets.cfg, numbering those after it down by one as the editor
|
|
571
|
+
## saves them, and gives its platform's runnable mark to the first other preset of that platform.
|
|
572
|
+
func remove_export_preset(params: Dictionary) -> Dictionary:
|
|
573
|
+
var preset_name: String = str(params.get("preset", "")).strip_edges()
|
|
574
|
+
var loaded: Variant = _preset_named(preset_name)
|
|
575
|
+
if loaded is String:
|
|
576
|
+
return _log.failure(str(loaded))
|
|
577
|
+
var found: Dictionary = loaded
|
|
578
|
+
var config: ConfigFile = found["config"]
|
|
579
|
+
var index: int = found["index"]
|
|
580
|
+
var section: String = found["section"]
|
|
581
|
+
var platform: String = str(config.get_value(section, "platform", ""))
|
|
582
|
+
var last: int = index
|
|
583
|
+
while config.has_section("preset." + str(last + 1)):
|
|
584
|
+
last += 1
|
|
585
|
+
_renumber(config, index, last)
|
|
586
|
+
var answer: Dictionary = {"removed": preset_name, "platform": platform, "file": PRESETS_FILE}
|
|
587
|
+
# The secrets are kept by the same numbers, so left alone they would pass to the next preset.
|
|
588
|
+
var credentials: ConfigFile = ConfigFile.new()
|
|
589
|
+
if FileAccess.file_exists(CREDENTIALS_FILE) and credentials.load(CREDENTIALS_FILE) == OK:
|
|
590
|
+
_renumber(credentials, index, last)
|
|
591
|
+
var keeping: Error = credentials.save(CREDENTIALS_FILE)
|
|
592
|
+
if keeping != OK:
|
|
593
|
+
return _log.failure("Failed to save %s: %s" % [CREDENTIALS_FILE, error_string(keeping)])
|
|
594
|
+
answer["credentials"] = CREDENTIALS_FILE
|
|
595
|
+
if str(config.get_value(RUNNABLE_SECTION, platform, "")) == preset_name:
|
|
596
|
+
config.set_value(RUNNABLE_SECTION, platform, null)
|
|
597
|
+
answer["runnableNow"] = null
|
|
598
|
+
var at: int = 0
|
|
599
|
+
while config.has_section("preset." + str(at)):
|
|
600
|
+
if str(config.get_value("preset." + str(at), "platform", "")) == platform:
|
|
601
|
+
var next: String = str(config.get_value("preset." + str(at), "name", ""))
|
|
602
|
+
config.set_value(RUNNABLE_SECTION, platform, next)
|
|
603
|
+
answer["runnableNow"] = next
|
|
604
|
+
break
|
|
605
|
+
at += 1
|
|
606
|
+
var saving: Error = config.save(PRESETS_FILE)
|
|
607
|
+
if saving != OK:
|
|
608
|
+
return _log.failure("Failed to save export_presets.cfg: " + error_string(saving))
|
|
609
|
+
answer["presets"] = names()
|
|
610
|
+
return answer
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
## [param config] with preset [param index] taken out and those after it, up to [param last],
|
|
614
|
+
## numbered down by one.
|
|
615
|
+
static func _renumber(config: ConfigFile, index: int, last: int) -> void:
|
|
616
|
+
for at: int in range(index, last + 1):
|
|
617
|
+
for part: String in ["", ".options"]:
|
|
618
|
+
var into: String = "preset.%d%s" % [at, part]
|
|
619
|
+
var from: String = "preset.%d%s" % [at + 1, part]
|
|
620
|
+
if config.has_section(into):
|
|
621
|
+
config.erase_section(into)
|
|
622
|
+
if at < last and config.has_section(from):
|
|
623
|
+
for key: String in config.get_section_keys(from):
|
|
624
|
+
config.set_value(into, key, config.get_value(from, key))
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
## export_presets.cfg loaded, with the section and index of the preset called [param preset_name],
|
|
628
|
+
## or why there is none.
|
|
629
|
+
static func _preset_named(preset_name: String) -> Variant:
|
|
630
|
+
if preset_name.is_empty():
|
|
631
|
+
return "preset is required: the name of the preset in export_presets.cfg"
|
|
632
|
+
if not FileAccess.file_exists(PRESETS_FILE):
|
|
633
|
+
return "This project has no export_presets.cfg; project_export add_preset adds a preset."
|
|
634
|
+
var config: ConfigFile = ConfigFile.new()
|
|
635
|
+
var loading: Error = config.load(PRESETS_FILE)
|
|
636
|
+
if loading != OK:
|
|
637
|
+
return "Failed to parse export_presets.cfg: " + error_string(loading)
|
|
638
|
+
var held: Array[String] = names()
|
|
639
|
+
var index: int = held.find(preset_name)
|
|
640
|
+
if index == -1:
|
|
641
|
+
if held.is_empty():
|
|
642
|
+
return "export_presets.cfg has no preset, so none called %s." % preset_name
|
|
643
|
+
return GdhRead.offered("There is no export preset %s" % preset_name, preset_name, held) + "."
|
|
644
|
+
return {"config": config, "section": "preset." + str(index), "index": index}
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
## The files a preset exporting by [param filter] lists, from [param given], or why they are not.
|
|
648
|
+
static func _files_for(filter: String, given: Variant) -> Variant:
|
|
649
|
+
if filter == "all_resources":
|
|
650
|
+
return (
|
|
651
|
+
"files are what a preset exports when export_filter is scenes, resources or exclude, and "
|
|
652
|
+
+ "this one exports all_resources: give exportFilter too."
|
|
653
|
+
)
|
|
654
|
+
if not given is Array:
|
|
655
|
+
return "files is a list of res:// paths."
|
|
656
|
+
var listed: PackedStringArray = []
|
|
657
|
+
var missing: PackedStringArray = []
|
|
658
|
+
var not_scenes: PackedStringArray = []
|
|
659
|
+
for entry: Variant in given:
|
|
660
|
+
var path: String = str(entry)
|
|
661
|
+
if not path.begins_with("res://"):
|
|
662
|
+
path = "res://" + path
|
|
663
|
+
if not FileAccess.file_exists(path):
|
|
664
|
+
var _missing: bool = missing.append(path)
|
|
665
|
+
elif filter == "scenes" and path.get_extension() not in ["tscn", "scn"]:
|
|
666
|
+
var _not_scene: bool = not_scenes.append(path)
|
|
667
|
+
elif not listed.has(path):
|
|
668
|
+
var _listed: bool = listed.append(path)
|
|
669
|
+
if not missing.is_empty():
|
|
670
|
+
return (
|
|
671
|
+
"Nothing was written: %s %s not in the project."
|
|
672
|
+
% [", ".join(missing), "is" if missing.size() == 1 else "are"]
|
|
673
|
+
)
|
|
674
|
+
if not not_scenes.is_empty():
|
|
675
|
+
return (
|
|
676
|
+
"Nothing was written: export_filter scenes lists scenes, and %s %s not one; resources lists any file."
|
|
677
|
+
% [", ".join(not_scenes), "is" if not_scenes.size() == 1 else "are"]
|
|
678
|
+
)
|
|
679
|
+
return listed
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
## The values [param asked] gives [param platform]'s options, each fitted to the option's type in
|
|
683
|
+
## [param known], or why one is refused. [param hidden] says, for an option the preset has and does
|
|
684
|
+
## not list as it would stand, what would list it.
|
|
685
|
+
static func _options_for(platform: String, asked: Dictionary, known: Variant, hidden: Dictionary) -> Variant:
|
|
686
|
+
var values: Dictionary = {}
|
|
687
|
+
if asked.is_empty():
|
|
688
|
+
return values
|
|
689
|
+
var by_name: Dictionary = {}
|
|
690
|
+
if known is Array:
|
|
691
|
+
for option: Variant in known:
|
|
692
|
+
if option is Dictionary:
|
|
693
|
+
var described: Dictionary = option
|
|
694
|
+
by_name[str(described.get("name", ""))] = described
|
|
695
|
+
for key: Variant in asked:
|
|
696
|
+
var name: String = str(key)
|
|
697
|
+
if not by_name.has(name) and hidden.has(name):
|
|
698
|
+
return (
|
|
699
|
+
(
|
|
700
|
+
"A %s preset lists %s only while %s, as the Export dialog shows it, and this one "
|
|
701
|
+
+ "would not: give %s in the same call, or set it first."
|
|
702
|
+
)
|
|
703
|
+
% [platform, name, hidden[name], str(hidden[name]).replace(" is ", " ")]
|
|
704
|
+
)
|
|
705
|
+
if not by_name.has(name):
|
|
706
|
+
return (
|
|
707
|
+
GdhRead.offered("A %s preset has no option %s" % [platform, name], name, by_name.keys()) + "."
|
|
708
|
+
)
|
|
709
|
+
var option: Dictionary = by_name[name]
|
|
710
|
+
var fitted: Variant = _fitted(name, asked[key], option)
|
|
711
|
+
if _is_refusal(fitted):
|
|
712
|
+
var refusal: Dictionary = fitted
|
|
713
|
+
return str(refusal["refused"])
|
|
714
|
+
values[name] = fitted
|
|
715
|
+
return values
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
static func _is_refusal(fitted: Variant) -> bool:
|
|
719
|
+
if not fitted is Dictionary:
|
|
720
|
+
return false
|
|
721
|
+
var held: Dictionary = fitted
|
|
722
|
+
return held.has("refused")
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
## Whether [param before] already is [param now], in its type too, so a write of it changes nothing.
|
|
726
|
+
static func _same(before: Variant, now: Variant) -> bool:
|
|
727
|
+
return typeof(before) == typeof(now) and before == now
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
## The file [param value] names for [param option] that is not there, or an empty string: a res://
|
|
731
|
+
## path, or a path on the machine for an option taking one, such as a custom template.
|
|
732
|
+
static func _missing_path(value: Variant, option: Dictionary) -> String:
|
|
733
|
+
if not value is String or str(value).is_empty():
|
|
734
|
+
return ""
|
|
735
|
+
var path: String = value
|
|
736
|
+
var hint: int = GdhRead.as_int(option.get("hint", PROPERTY_HINT_NONE))
|
|
737
|
+
var on_machine: bool = (
|
|
738
|
+
hint in [PROPERTY_HINT_GLOBAL_FILE, PROPERTY_HINT_GLOBAL_DIR] and path.is_absolute_path()
|
|
739
|
+
)
|
|
740
|
+
if not path.begins_with("res://") and not on_machine:
|
|
741
|
+
return ""
|
|
742
|
+
if FileAccess.file_exists(path) or DirAccess.dir_exists_absolute(path):
|
|
743
|
+
return ""
|
|
744
|
+
return path
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
## Each file the preset [param preset_name] names that is not there, as the option or list naming
|
|
748
|
+
## it and the path: an option's res:// path or custom template, or a file export_files lists. A
|
|
749
|
+
## secret's path is left out, as every answer leaves out a secret's value.
|
|
750
|
+
static func missing_named(preset_name: String) -> Array[String]:
|
|
751
|
+
var missing: Array[String] = []
|
|
752
|
+
var loaded: Variant = _preset_named(preset_name)
|
|
753
|
+
if not loaded is Dictionary:
|
|
754
|
+
return missing
|
|
755
|
+
var found: Dictionary = loaded
|
|
756
|
+
var config: ConfigFile = found["config"]
|
|
757
|
+
var section: String = found["section"]
|
|
758
|
+
# A secret can name a file too, as an Android preset's keystore does.
|
|
759
|
+
var credentials: ConfigFile = ConfigFile.new()
|
|
760
|
+
var _no_credentials: Error = credentials.load(CREDENTIALS_FILE)
|
|
761
|
+
for holder: ConfigFile in [config, credentials]:
|
|
762
|
+
if not holder.has_section(section + ".options"):
|
|
763
|
+
continue
|
|
764
|
+
for key: String in holder.get_section_keys(section + ".options"):
|
|
765
|
+
var option: Dictionary = {"hint": PROPERTY_HINT_GLOBAL_FILE} if key in TEMPLATE_OPTIONS else {}
|
|
766
|
+
var gone: String = _missing_path(holder.get_value(section + ".options", key), option)
|
|
767
|
+
if gone.is_empty():
|
|
768
|
+
continue
|
|
769
|
+
if holder == credentials:
|
|
770
|
+
missing.append("%s (a secret, so its path is left out)" % key)
|
|
771
|
+
else:
|
|
772
|
+
missing.append("%s: %s" % [key, gone])
|
|
773
|
+
var listed: Variant = config.get_value(section, "export_files", PackedStringArray())
|
|
774
|
+
if listed is PackedStringArray:
|
|
775
|
+
var files: PackedStringArray = listed
|
|
776
|
+
for path: String in files:
|
|
777
|
+
if not FileAccess.file_exists(path):
|
|
778
|
+
missing.append("export_files: " + path)
|
|
779
|
+
return missing
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
## [param value] as option [param name], described by [param option], holds it, or {refused} saying
|
|
783
|
+
## why it cannot.
|
|
784
|
+
static func _fitted(name: String, value: Variant, option: Dictionary) -> Variant:
|
|
785
|
+
var type: int = GdhRead.as_int(option.get("type", TYPE_NIL))
|
|
786
|
+
var hint: int = GdhRead.as_int(option.get("hint", PROPERTY_HINT_NONE))
|
|
787
|
+
var choices: PackedStringArray = str(option.get("hintString", "")).split(",", false)
|
|
788
|
+
var shown: String = JSON.stringify(value)
|
|
789
|
+
if type not in WRITABLE:
|
|
790
|
+
return {
|
|
791
|
+
"refused":
|
|
792
|
+
(
|
|
793
|
+
"%s is a %s, which this tool does not write; the editor's Export dialog does."
|
|
794
|
+
% [name, type_string(type)]
|
|
795
|
+
)
|
|
796
|
+
}
|
|
797
|
+
if type == TYPE_BOOL:
|
|
798
|
+
return value if value is bool else {"refused": "%s is true or false, not %s." % [name, shown]}
|
|
799
|
+
if type == TYPE_INT and hint == PROPERTY_HINT_ENUM:
|
|
800
|
+
var named: PackedStringArray = []
|
|
801
|
+
for at: int in choices.size():
|
|
802
|
+
var choice: String = choices[at].get_slice(":", 0)
|
|
803
|
+
var number: int = int(choices[at].get_slice(":", 1)) if choices[at].contains(":") else at
|
|
804
|
+
var _named: bool = named.append(choice)
|
|
805
|
+
if str(value) == choice or ((value is int or value is float) and value == number):
|
|
806
|
+
return number
|
|
807
|
+
return {"refused": "%s is one of %s, not %s." % [name, ", ".join(named), shown]}
|
|
808
|
+
if type == TYPE_INT:
|
|
809
|
+
if value is int:
|
|
810
|
+
return value
|
|
811
|
+
if value is float:
|
|
812
|
+
var whole: float = value
|
|
813
|
+
if whole == floorf(whole):
|
|
814
|
+
return int(whole)
|
|
815
|
+
return {"refused": "%s is a whole number, not %s." % [name, shown]}
|
|
816
|
+
if type == TYPE_FLOAT:
|
|
817
|
+
if value is int or value is float:
|
|
818
|
+
var amount: float = value
|
|
819
|
+
return amount
|
|
820
|
+
return {"refused": "%s is a number, not %s." % [name, shown]}
|
|
821
|
+
if type == TYPE_STRING:
|
|
822
|
+
if not value is String:
|
|
823
|
+
return {"refused": "%s is text, not %s." % [name, shown]}
|
|
824
|
+
if hint == PROPERTY_HINT_ENUM and not choices.is_empty() and str(value) not in choices:
|
|
825
|
+
return {"refused": "%s is one of %s, not %s." % [name, ", ".join(choices), shown]}
|
|
826
|
+
return value
|
|
827
|
+
if not value is Array:
|
|
828
|
+
return {"refused": "%s is a list of text, not %s." % [name, shown]}
|
|
829
|
+
var items: Array = value
|
|
830
|
+
return PackedStringArray(items)
|