diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index 8f293a3000d8..9a0432706c71 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -582,11 +582,17 @@ bool ProjectSettings::_load_resource_pack(const String &p_pack, bool p_replace_f return false; } - if (p_pack == "res://") { + String pack = p_pack.trim_suffix("/"); + + if (pack == "res://") { // Loading the resource directory as a pack source is reserved for internal use only. return false; } + if (pack.ends_with(".asyncpck")) { + PackedData::get_singleton()->add_pack_source(memnew(PackedSourceAsyncPCK)); + } + if (!p_main_pack && !using_datapack && !OS::get_singleton()->get_resource_dir().is_empty()) { // Add the project's resource file system to PackedData so directory access keeps working when // the game is running without a main pack, like in the editor or on Android. @@ -596,7 +602,7 @@ bool ProjectSettings::_load_resource_pack(const String &p_pack, bool p_replace_f using_datapack = true; } - bool ok = PackedData::get_singleton()->add_pack(p_pack, p_replace_files, p_offset) == OK; + bool ok = PackedData::get_singleton()->add_pack(pack, p_replace_files, p_offset) == OK; if (!ok) { return false; } @@ -755,7 +761,7 @@ Error ProjectSettings::_setup(const String &p_path, const String &p_main_pack, b } } -#ifdef ANDROID_ENABLED +#if defined(ANDROID_ENABLED) || defined(WEB_ENABLED) // Attempt to load sparse PCK assets. _load_resource_pack("res://assets.sparsepck", false, 0, true); #endif diff --git a/core/core_bind.cpp b/core/core_bind.cpp index 9e1bd6452681..7953cc14c593 100644 --- a/core/core_bind.cpp +++ b/core/core_bind.cpp @@ -743,6 +743,22 @@ void OS::remove_script_loggers(const ScriptLanguage *p_script) { } } +bool OS::async_pck_is_supported() const { + return ::OS::get_singleton()->async_pck_is_supported(); +} + +bool OS::async_pck_is_file_installable(const String &p_path) const { + return ::OS::get_singleton()->async_pck_is_file_installable(p_path); +} + +Error OS::async_pck_install_file(const String &p_path) const { + return ::OS::get_singleton()->async_pck_install_file(p_path); +} + +Dictionary OS::async_pck_install_file_get_status(const String &p_path) const { + return ::OS::get_singleton()->async_pck_install_file_get_status(p_path); +} + void OS::_bind_methods() { ClassDB::bind_method(D_METHOD("get_entropy", "size"), &OS::get_entropy); ClassDB::bind_method(D_METHOD("get_system_ca_certificates"), &OS::get_system_ca_certificates); @@ -851,6 +867,11 @@ void OS::_bind_methods() { ClassDB::bind_method(D_METHOD("add_logger", "logger"), &OS::add_logger); ClassDB::bind_method(D_METHOD("remove_logger", "logger"), &OS::remove_logger); + ClassDB::bind_method(D_METHOD("async_pck_is_supported"), &OS::async_pck_is_supported); + ClassDB::bind_method(D_METHOD("async_pck_is_file_installable", "path"), &OS::async_pck_is_file_installable); + ClassDB::bind_method(D_METHOD("async_pck_install_file", "path"), &OS::async_pck_install_file); + ClassDB::bind_method(D_METHOD("async_pck_install_file_get_status", "path"), &OS::async_pck_install_file_get_status); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "low_processor_usage_mode"), "set_low_processor_usage_mode", "is_in_low_processor_usage_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "low_processor_usage_mode_sleep_usec"), "set_low_processor_usage_mode_sleep_usec", "get_low_processor_usage_mode_sleep_usec"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "delta_smoothing"), "set_delta_smoothing", "is_delta_smoothing_enabled"); diff --git a/core/core_bind.h b/core/core_bind.h index 9848393e1dd2..6db5eb4f9d6a 100644 --- a/core/core_bind.h +++ b/core/core_bind.h @@ -313,6 +313,11 @@ class OS : public Object { void remove_logger(const Ref &p_logger); void remove_script_loggers(const ScriptLanguage *p_script); + bool async_pck_is_supported() const; + bool async_pck_is_file_installable(const String &p_path) const; + Error async_pck_install_file(const String &p_path) const; + Dictionary async_pck_install_file_get_status(const String &p_path) const; + static OS *get_singleton() { return singleton; } OS(); diff --git a/core/io/file_access_pack.cpp b/core/io/file_access_pack.cpp index 89967c15722a..eade24cae74c 100644 --- a/core/io/file_access_pack.cpp +++ b/core/io/file_access_pack.cpp @@ -46,54 +46,59 @@ Error PackedData::add_pack(const String &p_path, bool p_replace_files, uint64_t return ERR_FILE_UNRECOGNIZED; } -void PackedData::add_path(const String &p_pkg_path, const String &p_path, uint64_t p_ofs, uint64_t p_size, const uint8_t *p_md5, PackSource *p_src, bool p_replace_files, bool p_encrypted, bool p_bundle, bool p_delta) { +void PackedData::add_path(const String &p_pkg_path, const String &p_path, uint64_t p_ofs, uint64_t p_size, const uint8_t *p_md5, PackSource *p_src, bool p_replace_files, BitField p_properties) { String simplified_path = p_path.simplify_path().trim_prefix("res://"); PathMD5 pmd5(simplified_path.md5_buffer()); bool exists = files.has(pmd5); PackedFile pf; - pf.encrypted = p_encrypted; - pf.bundle = p_bundle; - pf.delta = p_delta; + pf.properties = p_properties; pf.pack = p_pkg_path; pf.offset = p_ofs; pf.size = p_size; - for (int i = 0; i < 16; i++) { - pf.md5[i] = p_md5[i]; - } + memcpy(pf.md5, p_md5, 16); pf.src = p_src; - if (p_delta) { + if (pf.properties.has_flag(PackedFile::PackedFileProperty::PACKED_FILE_PROPERTY_DELTA)) { delta_patches[pmd5].push_back(pf); } else if (!exists || p_replace_files) { files[pmd5] = pf; delta_patches[pmd5].clear(); } - if (!exists) { - // Search for directory. - PackedDir *cd = root; + if (exists) { + return; + } - if (simplified_path.contains_char('/')) { // In a subdirectory. - Vector ds = simplified_path.get_base_dir().split("/"); + // Search for directory. + PackedDir *cd = root; - for (int j = 0; j < ds.size(); j++) { - if (!cd->subdirs.has(ds[j])) { - PackedDir *pd = memnew(PackedDir); - pd->name = ds[j]; - pd->parent = cd; - cd->subdirs[pd->name] = pd; - cd = pd; - } else { - cd = cd->subdirs[ds[j]]; - } + if (simplified_path.contains_char('/')) { // In a subdirectory. + Vector ds = simplified_path.get_base_dir().split("/"); + + for (int j = 0; j < ds.size(); j++) { + if (!cd->subdirs.has(ds[j])) { + PackedDir *pd = memnew(PackedDir); + pd->name = ds[j]; + pd->parent = cd; + cd->subdirs[pd->name] = pd; + cd = pd; + } else { + cd = cd->subdirs[ds[j]]; } } - String filename = simplified_path.get_file(); - // Don't add as a file if the path points to a directory. - if (!filename.is_empty()) { - cd->files.insert(filename); + } + String filename = simplified_path.get_file(); + // Don't add as a file if the path points to a directory. + if (!filename.is_empty()) { + cd->files.insert(filename); + } + + if (p_properties.has_flag(PackedFile::PackedFileProperty::PACKED_FILE_PROPERTY_ASYNC)) { + Ref file = FileAccess::create_for_path(p_path); + if (!file->file_exists(p_path)) { + async_files[pmd5] = pf; } } } @@ -164,6 +169,26 @@ bool PackedData::has_delta_patches(const String &p_path) const { return !E->value.is_empty(); } +String PackedData::get_file_pack_path(const String &p_path) { + String simplified_path = p_path.simplify_path().trim_prefix("res://"); + PathMD5 pmd5(simplified_path.md5_buffer()); + HashMap::Iterator file_iterator = files.find(pmd5); + if (!file_iterator) { + return ""; + } + return file_iterator->value.pack; +} + +String PackedData::get_file_async_pack_path(const String &p_path) { + String simplified_path = p_path.simplify_path().trim_prefix("res://"); + PathMD5 pmd5(simplified_path.md5_buffer()); + HashMap::Iterator file_iterator = async_files.find(pmd5); + if (!file_iterator) { + return ""; + } + return file_iterator->value.pack; +} + HashSet PackedData::get_file_paths() const { HashSet file_paths; _get_file_paths(root, root->name, file_paths); @@ -298,6 +323,7 @@ bool PackedSourcePCK::try_open_pack(const String &p_path, bool p_replace_files, bool enc_directory = (pack_flags & PACK_DIR_ENCRYPTED); bool rel_filebase = (pack_flags & PACK_REL_FILEBASE); // Note: Always enabled for V3. bool sparse_bundle = (pack_flags & PACK_SPARSE_BUNDLE); + bool async = (pack_flags & PACK_ASYNC); uint64_t file_base = f->get_64(); if ((version == PACK_FORMAT_VERSION_V3) || (version == PACK_FORMAT_VERSION_V2 && rel_filebase)) { @@ -324,9 +350,7 @@ bool PackedSourcePCK::try_open_pack(const String &p_path, bool p_replace_files, Vector key; key.resize(32); - for (int i = 0; i < key.size(); i++) { - key.write[i] = script_encryption_key[i]; - } + memcpy(key.ptrw(), script_encryption_key, 32); Error err = fae->open_and_parse(f, key, FileAccessEncrypted::MODE_READ, false); ERR_FAIL_COND_V_MSG(err, false, "Can't open encrypted pack directory."); @@ -350,7 +374,20 @@ bool PackedSourcePCK::try_open_pack(const String &p_path, bool p_replace_files, if (flags & PACK_FILE_REMOVAL) { // The file was removed. PackedData::get_singleton()->remove_path(path); } else { - PackedData::get_singleton()->add_path(p_path, path, file_base + ofs, size, md5, this, p_replace_files, (flags & PACK_FILE_ENCRYPTED), sparse_bundle, (flags & PACK_FILE_DELTA)); + BitField properties = PackedData::PackedFile::PackedFileProperty::PACKED_FILE_PROPERTY_NONE; + if (flags & PACK_FILE_ENCRYPTED) { + properties.set_flag(PackedData::PackedFile::PackedFileProperty::PACKED_FILE_PROPERTY_ENCRYPTED); + } + if (sparse_bundle) { + properties.set_flag(PackedData::PackedFile::PackedFileProperty::PACKED_FILE_PROPERTY_BUNDLED); + } + if (flags & PACK_FILE_DELTA) { + properties.set_flag(PackedData::PackedFile::PackedFileProperty::PACKED_FILE_PROPERTY_DELTA); + } + if (async) { + properties.set_flag(PackedData::PackedFile::PackedFileProperty::PACKED_FILE_PROPERTY_ASYNC); + } + PackedData::get_singleton()->add_path(p_path, path, file_base + ofs, size, md5, this, p_replace_files, properties); } } @@ -373,6 +410,27 @@ Ref PackedSourcePCK::get_file(const String &p_path, PackedData::Pack ////////////////////////////////////////////////////////////////// +bool PackedSourceAsyncPCK::try_open_pack(const String &p_path, bool p_replace_files, uint64_t p_offset) { + String path = p_path.simplify_path(); + if (path.ends_with("/")) { + path = path.trim_suffix("/"); + } + if (!path.ends_with(".asyncpck")) { + return false; + } + path = path.path_join("assets").path_join("assets.sparsepck"); + return PackedSourcePCK::try_open_pack(path, p_replace_files, p_offset); +} + +Ref PackedSourceAsyncPCK::get_file(const String &p_path, PackedData::PackedFile *p_file) { + String pack = p_file->pack.get_base_dir(); + String path = p_path.simplify_path().trim_prefix("res://"); + String file_path = pack.path_join(path); + return memnew(FileAccessPack(file_path, *p_file)); +} + +////////////////////////////////////////////////////////////////// + bool PackedSourceDirectory::try_open_pack(const String &p_path, bool p_replace_files, uint64_t p_offset) { // Load with offset feature only supported for PCK files. ERR_FAIL_COND_V_MSG(p_offset != 0, false, "Invalid PCK data. Note that loading files with a non-zero offset isn't supported with directories."); @@ -400,7 +458,7 @@ void PackedSourceDirectory::add_directory(const String &p_path, bool p_replace_f for (const String &file_name : da->get_files()) { String file_path = p_path.path_join(file_name); uint8_t md5[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - PackedData::get_singleton()->add_path(p_path, file_path, 0, 0, md5, this, p_replace_files, false, false, false); + PackedData::get_singleton()->add_path(p_path, file_path, 0, 0, md5, this, p_replace_files); } for (const String &sub_dir_name : da->get_directories()) { @@ -510,28 +568,29 @@ void FileAccessPack::close() { FileAccessPack::FileAccessPack(const String &p_path, const PackedData::PackedFile &p_file) { path = p_path; pf = p_file; - if (pf.bundle) { + + if (pf.properties.has_flag(PackedData::PackedFile::PackedFileProperty::PACKED_FILE_PROPERTY_BUNDLED)) { String simplified_path = p_path.simplify_path(); f = FileAccess::open(simplified_path, FileAccess::READ | FileAccess::SKIP_PACK); ERR_FAIL_COND_MSG(f.is_null(), vformat(R"(Can't open pack-referenced file "%s" from sparse pack "%s".)", simplified_path, pf.pack)); off = 0; // For the sparse pack offset is always zero. + ERR_FAIL_COND_MSG(f.is_null(), vformat("Can't open pack-referenced file '%s' from '%s'.", simplified_path, String(pf.pack))); } else { f = FileAccess::open(pf.pack, FileAccess::READ); ERR_FAIL_COND_MSG(f.is_null(), vformat(R"(Can't open pack-referenced file "%s" from pack "%s".)", p_path, pf.pack)); f->seek(pf.offset); off = pf.offset; + ERR_FAIL_COND_MSG(f.is_null(), vformat("Can't open pack-referenced file '%s'.", String(pf.pack))); } - if (pf.encrypted) { + if (pf.properties.has_flag(PackedData::PackedFile::PackedFileProperty::PACKED_FILE_PROPERTY_ENCRYPTED)) { Ref fae; fae.instantiate(); ERR_FAIL_COND_MSG(fae.is_null(), vformat(R"(Can't open encrypted pack-referenced file "%s" from pack "%s".)", p_path, pf.pack)); Vector key; key.resize(32); - for (int i = 0; i < key.size(); i++) { - key.write[i] = script_encryption_key[i]; - } + memcpy(key.ptrw(), script_encryption_key, 32); Error err = fae->open_and_parse(f, key, FileAccessEncrypted::MODE_READ, false); ERR_FAIL_COND_MSG(err, vformat(R"(Can't open encrypted pack-referenced file "%s" from pack "%s".)", p_path, pf.pack)); diff --git a/core/io/file_access_pack.h b/core/io/file_access_pack.h index 121372b51efc..53810ab2a82c 100644 --- a/core/io/file_access_pack.h +++ b/core/io/file_access_pack.h @@ -49,6 +49,7 @@ enum PackFlags { PACK_DIR_ENCRYPTED = 1 << 0, PACK_REL_FILEBASE = 1 << 1, PACK_SPARSE_BUNDLE = 1 << 2, + PACK_ASYNC = 1 << 3, }; enum PackFileFlags { @@ -66,14 +67,21 @@ class PackedData { public: struct PackedFile { + enum PackedFileProperty { + PACKED_FILE_PROPERTY_NONE = 0, + PACKED_FILE_PROPERTY_ENCRYPTED = 1 << 0, + PACKED_FILE_PROPERTY_BUNDLED = 1 << 1, + PACKED_FILE_PROPERTY_DELTA = 1 << 2, + PACKED_FILE_PROPERTY_ASYNC = 1 << 3, + PACKED_FILE_PROPERTY_ALL = (PACKED_FILE_PROPERTY_ASYNC << 1) - 1, + }; + String pack; uint64_t offset; //if offset is ZERO, the file was ERASED uint64_t size; uint8_t md5[16]; PackSource *src = nullptr; - bool encrypted; - bool bundle; - bool delta; + BitField properties; }; private: @@ -106,6 +114,7 @@ class PackedData { HashMap files; HashMap, PathMD5> delta_patches; + HashMap async_files; Vector sources; @@ -119,11 +128,13 @@ class PackedData { public: void add_pack_source(PackSource *p_source); - void add_path(const String &p_pkg_path, const String &p_path, uint64_t p_ofs, uint64_t p_size, const uint8_t *p_md5, PackSource *p_src, bool p_replace_files, bool p_encrypted = false, bool p_bundle = false, bool p_delta = false); // for PackSource + void add_path(const String &p_pkg_path, const String &p_path, uint64_t p_ofs, uint64_t p_size, const uint8_t *p_md5, PackSource *p_src, bool p_replace_files, BitField p_properties = PackedData::PackedFile::PackedFileProperty::PACKED_FILE_PROPERTY_NONE); // for PackSource void remove_path(const String &p_path); uint8_t *get_file_hash(const String &p_path); Vector get_delta_patches(const String &p_path) const; bool has_delta_patches(const String &p_path) const; + String get_file_pack_path(const String &p_path); + String get_file_async_pack_path(const String &p_path); HashSet get_file_paths() const; void set_disabled(bool p_disabled) { disabled = p_disabled; } @@ -136,6 +147,8 @@ class PackedData { _FORCE_INLINE_ Ref try_open_path(const String &p_path); _FORCE_INLINE_ bool has_path(const String &p_path); + _FORCE_INLINE_ bool has_async_path(const String &p_path); + _FORCE_INLINE_ String get_async_path(const String &p_path); _FORCE_INLINE_ int64_t get_size(const String &p_path); @@ -159,6 +172,12 @@ class PackedSourcePCK : public PackSource { virtual Ref get_file(const String &p_path, PackedData::PackedFile *p_file) override; }; +class PackedSourceAsyncPCK : public PackedSourcePCK { +public: + virtual bool try_open_pack(const String &p_path, bool p_replace_files, uint64_t p_offset) override; + virtual Ref get_file(const String &p_path, PackedData::PackedFile *p_file) override; +}; + class PackedSourceDirectory : public PackSource { void add_directory(const String &p_path, bool p_replace_files); @@ -244,7 +263,39 @@ Ref PackedData::try_open_path(const String &p_path) { } bool PackedData::has_path(const String &p_path) { - return files.has(PathMD5(p_path.simplify_path().trim_prefix("res://").md5_buffer())); + String md5_path = p_path.simplify_path().trim_prefix("res://"); + PathMD5 md5_data(md5_path.md5_buffer()); + String md5_text = md5_path.md5_text(); + bool has_file = files.has(md5_data); + return has_file; +} + +bool PackedData::has_async_path(const String &p_path) { + return !PackedData::get_async_path(p_path).is_empty(); +} + +String PackedData::get_async_path(const String &p_path) { + const String PREFIX_RES = "res://"; + + const String md5_path = p_path.simplify_path().trim_prefix(PREFIX_RES); + PathMD5 md5_data(md5_path.md5_buffer()); + if (async_files.has(md5_data)) { + return PREFIX_RES + md5_path; + } + + String md5_remap_path = md5_path + ".remap"; + PathMD5 md5_remap_data(md5_remap_path.md5_buffer()); + if (async_files.has(md5_remap_data)) { + return PREFIX_RES + md5_remap_path; + } + + String md5_import_path = md5_path + ".import"; + PathMD5 md5_import_data(md5_import_path.md5_buffer()); + if (async_files.has(md5_import_data)) { + return PREFIX_RES + md5_import_path; + } + + return String(); } bool PackedData::has_directory(const String &p_path) { diff --git a/core/io/file_access_patched.cpp b/core/io/file_access_patched.cpp index 8c520b687694..243198e4fa26 100644 --- a/core/io/file_access_patched.cpp +++ b/core/io/file_access_patched.cpp @@ -44,7 +44,7 @@ Error FileAccessPatched::_apply_patch() const { for (int i = 0; i < delta_patches.size(); ++i) { const PackedData::PackedFile &delta_patch = delta_patches[i]; - ERR_FAIL_COND_V(delta_patch.bundle, FAILED); + ERR_FAIL_COND_V(delta_patch.properties.has_flag(PackedData::PackedFile::PackedFileProperty::PACKED_FILE_PROPERTY_BUNDLED), FAILED); Error err = OK; diff --git a/core/io/resource_loader.cpp b/core/io/resource_loader.cpp index 952a7e93cc59..fd2d71df0c64 100644 --- a/core/io/resource_loader.cpp +++ b/core/io/resource_loader.cpp @@ -301,8 +301,6 @@ Ref ResourceLoader::_load(const String &p_path, const String &p_origin } load_paths_stack.push_back(original_path); - print_verbose(vformat("Loading resource: %s", p_path)); - // Try all loaders and pick the first match for the type hint bool found = false; Ref res; diff --git a/core/object/object.h b/core/object/object.h index 60451ce43cd9..5e5c83910cb7 100644 --- a/core/object/object.h +++ b/core/object/object.h @@ -157,6 +157,10 @@ enum PropertyUsageFlags { // PropertyInfo(Variant::ARRAY, "fallbacks", PROPERTY_HINT_ARRAY_TYPE, MAKE_RESOURCE_TYPE_HINT("Font") #define MAKE_RESOURCE_TYPE_HINT(m_type) vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, m_type) +// Helper macro to use with PROPERTY_HINT_ARRAY_TYPE for arrays of file paths: +// PropertyInfo(Variant::PACKED_STRING_ARRAY, "async/initial_load_forced_files", PROPERTY_HINT_ARRAY_TYPE, MAKE_FILE_ARRAY_TYPE_HINT("*")) +#define MAKE_FILE_ARRAY_TYPE_HINT(m_type) vformat("%s/%s:%s", Variant::STRING, PROPERTY_HINT_FILE, m_type) + struct PropertyInfo { Variant::Type type = Variant::NIL; String name; diff --git a/core/os/os.cpp b/core/os/os.cpp index 1d6561a7dffd..82c1edd3b6f8 100644 --- a/core/os/os.cpp +++ b/core/os/os.cpp @@ -821,6 +821,39 @@ void OS::benchmark_dump() { #endif } +String OS::async_pck_get_async_pck_path(const String &p_path, Error *r_error) const { + Error err = OK; + String async_path; + String pck_path; + +#define RETURN_ERROR \ + if (r_error != nullptr) { \ + *r_error = err; \ + } \ + return pck_path + +#define _ERR_FAIL_COND(m_cond, m_err) \ + if (unlikely(m_cond)) { \ + err = m_err; \ + RETURN_ERROR; \ + } \ + (void)0 + + _ERR_FAIL_COND(p_path.is_empty(), ERR_INVALID_PARAMETER); + _ERR_FAIL_COND(FileAccess::exists(p_path), ERR_INVALID_PARAMETER); + + async_path = PackedData::get_singleton()->get_async_path(p_path); + _ERR_FAIL_COND(async_path.is_empty(), ERR_CANT_RESOLVE); + + pck_path = PackedData::get_singleton()->get_file_async_pack_path(async_path); + _ERR_FAIL_COND(pck_path.is_empty(), ERR_CANT_RESOLVE); + + RETURN_ERROR; + +#undef _ERR_COND +#undef RETURN_ERROR +} + OS::OS() { singleton = this; diff --git a/core/os/os.h b/core/os/os.h index 5275a30c9283..48bfb58dedfa 100644 --- a/core/os/os.h +++ b/core/os/os.h @@ -31,6 +31,7 @@ #pragma once #include "core/config/engine.h" +#include "core/io/file_access_pack.h" #include "core/io/logger.h" #include "core/io/remote_filesystem_client.h" #include "core/os/time_enums.h" @@ -127,6 +128,8 @@ class OS { virtual bool _check_internal_feature_support(const String &p_feature) = 0; + virtual String async_pck_get_async_pck_path(const String &p_path, Error *r_error) const; + public: typedef int64_t ProcessID; @@ -371,6 +374,23 @@ class OS { virtual PreferredTextureFormat get_preferred_texture_format() const; + virtual bool async_pck_is_supported() const { return false; } + virtual bool async_pck_is_file_installable(const String &p_path) const { + return PackedData::get_singleton() && !PackedData::get_singleton()->is_disabled() && PackedData::get_singleton()->has_async_path(p_path); + } + virtual Error async_pck_install_file(const String &p_path) const { + return FAILED; + } + virtual Dictionary async_pck_install_file_get_status(const String &p_path) const { + Dictionary status; + status["files"] = Dictionary(); + status["size"] = 0; + status["progress"] = 0; + status["progress_ratio"] = 0; + status["status"] = "STATUS_IDLE"; + return status; + } + // Load GDExtensions specific to this platform. // This is invoked by the GDExtensionManager after loading GDExtensions specified by the project. virtual void load_platform_gdextensions() const {} diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 88ec032fd203..8c6a51ee9b1a 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -1327,7 +1327,7 @@ void EditorNode::_fs_changed() { } if (err != OK) { export_error = vformat("Project export for preset \"%s\" failed.", preset_name); - } else if (platform->get_worst_message_type() >= EditorExportPlatform::EXPORT_MESSAGE_WARNING) { + } else if (platform->get_worst_message_type() >= EditorExportPlatformData::EXPORT_MESSAGE_WARNING) { export_error = vformat("Project export for preset \"%s\" completed with warnings.", preset_name); } } diff --git a/editor/editor_node.h b/editor/editor_node.h index 21634f59f98a..52deb3c11d8a 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -1005,6 +1005,7 @@ class EditorNode : public Node { Dictionary drag_files_and_dirs(const Vector &p_paths, Control *p_from); EditorQuickOpenDialog *get_quick_open_dialog() { return quick_open_dialog; } + ProjectExportDialog *get_project_export_dialog() { return project_export; } void add_tool_menu_item(const String &p_name, const Callable &p_callback); void add_tool_submenu_item(const String &p_name, PopupMenu *p_submenu); diff --git a/editor/export/editor_export.cpp b/editor/export/editor_export.cpp index fb5c2287d256..a066fb30d121 100644 --- a/editor/export/editor_export.cpp +++ b/editor/export/editor_export.cpp @@ -104,10 +104,12 @@ void EditorExport::_save() { for (const KeyValue &E : preset->values) { PropertyInfo *prop = preset->properties.getptr(E.key); - if (prop && prop->usage & PROPERTY_USAGE_SECRET) { - credentials->set_value(option_section, E.key, E.value); - } else { - config->set_value(option_section, E.key, E.value); + if (prop && !(prop->usage & PROPERTY_USAGE_NO_INSTANCE_STATE)) { + if (prop->usage & PROPERTY_USAGE_SECRET) { + credentials->set_value(option_section, E.key, E.value); + } else { + config->set_value(option_section, E.key, E.value); + } } } } diff --git a/editor/export/editor_export_platform.cpp b/editor/export/editor_export_platform.cpp index ee4eb922ddf3..3c74c4f71c6e 100644 --- a/editor/export/editor_export_platform.cpp +++ b/editor/export/editor_export_platform.cpp @@ -37,7 +37,6 @@ #include "core/extension/gdextension.h" #include "core/io/delta_encoding.h" #include "core/io/dir_access.h" -#include "core/io/file_access_encrypted.h" #include "core/io/file_access_pack.h" // PACK_HEADER_MAGIC, PACK_FORMAT_VERSION #include "core/io/image.h" #include "core/io/image_loader.h" @@ -59,38 +58,6 @@ #include "scene/resources/packed_scene.h" #include "scene/resources/texture.h" -class EditorExportSaveProxy { - HashSet saved_paths; - EditorExportPlatform::EditorExportSaveFunction save_func; - bool tracking_saves = false; - -public: - bool has_saved(const String &p_path) const { return saved_paths.has(p_path); } - - Error save_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta) { - if (tracking_saves) { - saved_paths.insert(p_path.simplify_path().trim_prefix("res://")); - } - - return save_func(p_preset, p_userdata, p_path, p_data, p_file, p_total, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, p_delta); - } - - EditorExportSaveProxy(EditorExportPlatform::EditorExportSaveFunction p_save_func, bool p_track_saves) : - save_func(p_save_func), tracking_saves(p_track_saves) {} -}; - -static int _get_pad(int p_alignment, int p_n) { - int rest = p_n % p_alignment; - int pad = 0; - if (rest > 0) { - pad = p_alignment - rest; - }; - - return pad; -} - -static constexpr int PCK_PADDING = 16; - Ref EditorExportPlatform::_load_icon_or_splash_image(const String &p_path, Error *r_error) const { Ref image; @@ -123,8 +90,8 @@ bool EditorExportPlatform::fill_log_messages(RichTextLabel *p_log, Error p_err) p_log->add_text(" "); p_log->add_text(get_name()); p_log->add_text(" - "); - if (p_err == OK && get_worst_message_type() < EditorExportPlatform::EXPORT_MESSAGE_ERROR) { - if (get_worst_message_type() >= EditorExportPlatform::EXPORT_MESSAGE_WARNING) { + if (p_err == OK && get_worst_message_type() < EditorExportPlatformData::EXPORT_MESSAGE_ERROR) { + if (get_worst_message_type() >= EditorExportPlatformData::EXPORT_MESSAGE_WARNING) { p_log->add_image(p_log->get_editor_theme_icon(SNAME("StatusWarning")), 16 * EDSCALE, 16 * EDSCALE, Color(1.0, 1.0, 1.0), INLINE_ALIGNMENT_CENTER); p_log->add_text(" "); p_log->add_text(TTR("Completed with warnings.")); @@ -155,14 +122,14 @@ bool EditorExportPlatform::fill_log_messages(RichTextLabel *p_log, Error p_err) Ref icon; switch (msg.msg_type) { - case EditorExportPlatform::EXPORT_MESSAGE_INFO: { + case EditorExportPlatformData::EXPORT_MESSAGE_INFO: { color = p_log->get_theme_color(SceneStringName(font_color), EditorStringName(Editor)) * Color(1, 1, 1, 0.6); } break; - case EditorExportPlatform::EXPORT_MESSAGE_WARNING: { + case EditorExportPlatformData::EXPORT_MESSAGE_WARNING: { icon = p_log->get_editor_theme_icon(SNAME("Warning")); color = p_log->get_theme_color(SNAME("warning_color"), EditorStringName(Editor)); } break; - case EditorExportPlatform::EXPORT_MESSAGE_ERROR: { + case EditorExportPlatformData::EXPORT_MESSAGE_ERROR: { icon = p_log->get_editor_theme_icon(SNAME("Error")); color = p_log->get_theme_color(SNAME("error_color"), EditorStringName(Editor)); } break; @@ -225,7 +192,7 @@ Error EditorExportPlatform::_load_patches(const Vector &p_patches) { for (const String &path : p_patches) { err = PackedData::get_singleton()->add_pack(path, true, 0); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Patch Creation"), vformat(TTR("Could not load patch pack with path \"%s\"."), path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Patch Creation"), vformat(TTR("Could not load patch pack with path \"%s\"."), path)); return err; } } @@ -237,64 +204,10 @@ void EditorExportPlatform::_unload_patches() { PackedData::get_singleton()->clear(); } -Error EditorExportPlatform::_encrypt_and_store_data(Ref p_fd, const String &p_path, const Vector &p_data, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool &r_encrypt) { - r_encrypt = false; - for (int i = 0; i < p_enc_in_filters.size(); ++i) { - if (p_path.matchn(p_enc_in_filters[i]) || p_path.trim_prefix("res://").matchn(p_enc_in_filters[i])) { - r_encrypt = true; - break; - } - } - - for (int i = 0; i < p_enc_ex_filters.size(); ++i) { - if (p_path.matchn(p_enc_ex_filters[i]) || p_path.trim_prefix("res://").matchn(p_enc_ex_filters[i])) { - r_encrypt = false; - break; - } - } - - Ref fae; - Ref ftmp = p_fd; - if (r_encrypt) { - Vector iv; - if (p_seed != 0) { - uint64_t seed = p_seed; - - const uint8_t *ptr = p_data.ptr(); - int64_t len = p_data.size(); - for (int64_t i = 0; i < len; i++) { - seed = ((seed << 5) + seed) ^ ptr[i]; - } - - RandomPCG rng = RandomPCG(seed); - iv.resize(16); - for (int i = 0; i < 16; i++) { - iv.write[i] = rng.rand() % 256; - } - } - - fae.instantiate(); - ERR_FAIL_COND_V(fae.is_null(), ERR_FILE_CANT_OPEN); - - Error err = fae->open_and_parse(ftmp, p_key, FileAccessEncrypted::MODE_WRITE_AES256, false, iv); - ERR_FAIL_COND_V(err != OK, ERR_FILE_CANT_OPEN); - ftmp = fae; - } - - // Store file content. - ftmp->store_buffer(p_data.ptr(), p_data.size()); - - if (fae.is_valid()) { - ftmp.unref(); - fae.unref(); - } - return OK; -} - Error EditorExportPlatform::_save_pack_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta) { ERR_FAIL_COND_V_MSG(p_total < 1, ERR_PARAMETER_RANGE_ERROR, "Must select at least one file to export."); - PackData *pd = (PackData *)p_userdata; + EditorExportPlatformData::PackData *pd = (EditorExportPlatformData::PackData *)p_userdata; const String simplified_path = simplify_path(p_path); @@ -305,12 +218,12 @@ Error EditorExportPlatform::_save_pack_file(const Ref &p_pre ftmp = pd->f; } - SavedData sd; + EditorExportPlatformData::SavedData sd; sd.path_utf8 = simplified_path.trim_prefix("res://").utf8(); sd.ofs = (pd->use_sparse_pck) ? 0 : pd->f->get_position(); sd.size = p_data.size(); sd.delta = p_delta; - Error err = _encrypt_and_store_data(ftmp, simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, sd.encrypted); + Error err = EditorExportPlatformUtils::encrypt_and_store_data(ftmp, simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, sd.encrypted); if (err != OK) { return err; } @@ -319,7 +232,7 @@ Error EditorExportPlatform::_save_pack_file(const Ref &p_pre } if (!pd->use_sparse_pck) { - int pad = _get_pad(PCK_PADDING, pd->f->get_position()); + int pad = EditorExportPlatformUtils::get_pad(EditorExportPlatformData::PCK_PADDING, pd->f->get_position()); for (int i = 0; i < pad; i++) { pd->f->store_8(0); } @@ -410,7 +323,7 @@ Error EditorExportPlatform::_save_zip_file(const Ref &p_pres const String path = simplify_path(p_path).replace_first("res://", ""); - ZipData *zd = (ZipData *)p_userdata; + EditorExportPlatformData::ZipData *zd = (EditorExportPlatformData::ZipData *)p_userdata; zipFile zip = (zipFile)zd->zip; @@ -499,125 +412,6 @@ Ref EditorExportPlatform::create_preset() { return preset; } -void EditorExportPlatform::_export_find_resources(EditorFileSystemDirectory *p_dir, HashSet &p_paths) { - for (int i = 0; i < p_dir->get_subdir_count(); i++) { - _export_find_resources(p_dir->get_subdir(i), p_paths); - } - - for (int i = 0; i < p_dir->get_file_count(); i++) { - if (p_dir->get_file_type(i) == "TextFile") { - continue; - } - p_paths.insert(p_dir->get_file_path(i)); - } -} - -void EditorExportPlatform::_export_find_customized_resources(const Ref &p_preset, EditorFileSystemDirectory *p_dir, EditorExportPreset::FileExportMode p_mode, HashSet &p_paths) { - for (int i = 0; i < p_dir->get_subdir_count(); i++) { - EditorFileSystemDirectory *subdir = p_dir->get_subdir(i); - _export_find_customized_resources(p_preset, subdir, p_preset->get_file_export_mode(subdir->get_path(), p_mode), p_paths); - } - - for (int i = 0; i < p_dir->get_file_count(); i++) { - if (p_dir->get_file_type(i) == "TextFile") { - continue; - } - String path = p_dir->get_file_path(i); - EditorExportPreset::FileExportMode file_mode = p_preset->get_file_export_mode(path, p_mode); - if (file_mode != EditorExportPreset::MODE_FILE_REMOVE) { - p_paths.insert(path); - } - } -} - -void EditorExportPlatform::_export_find_dependencies(const String &p_path, HashSet &p_paths) { - if (p_paths.has(p_path)) { - return; - } - - p_paths.insert(p_path); - - EditorFileSystemDirectory *dir; - int file_idx; - dir = EditorFileSystem::get_singleton()->find_file(p_path, &file_idx); - if (!dir) { - return; - } - - Vector deps = dir->get_file_deps(file_idx); - - for (int i = 0; i < deps.size(); i++) { - _export_find_dependencies(deps[i], p_paths); - } -} - -void EditorExportPlatform::_edit_files_with_filter(Ref &da, const Vector &p_filters, HashSet &r_list, bool exclude) { - da->list_dir_begin(); - String cur_dir = da->get_current_dir().replace_char('\\', '/'); - if (!cur_dir.ends_with("/")) { - cur_dir += "/"; - } - String cur_dir_no_prefix = cur_dir.replace("res://", ""); - - Vector dirs; - String f = da->get_next(); - while (!f.is_empty()) { - if (da->current_is_dir()) { - dirs.push_back(f); - } else { - String fullpath = cur_dir + f; - // Test also against path without res:// so that filters like `file.txt` can work. - String fullpath_no_prefix = cur_dir_no_prefix + f; - for (int i = 0; i < p_filters.size(); ++i) { - if (fullpath.matchn(p_filters[i]) || fullpath_no_prefix.matchn(p_filters[i])) { - if (!exclude) { - r_list.insert(fullpath); - } else { - r_list.erase(fullpath); - } - } - } - } - f = da->get_next(); - } - - da->list_dir_end(); - - for (int i = 0; i < dirs.size(); ++i) { - const String &dir = dirs[i]; - if (dir.begins_with(".")) { - continue; - } - - if (EditorFileSystem::_should_skip_directory(cur_dir + dir)) { - continue; - } - - da->change_dir(dir); - _edit_files_with_filter(da, p_filters, r_list, exclude); - da->change_dir(".."); - } -} - -void EditorExportPlatform::_edit_filter_list(HashSet &r_list, const String &p_filter, bool exclude) { - if (p_filter.is_empty()) { - return; - } - Vector split = p_filter.split(","); - Vector filters; - for (int i = 0; i < split.size(); i++) { - String f = split[i].strip_edges(); - if (f.is_empty()) { - continue; - } - filters.push_back(f); - } - - Ref da = DirAccess::create(DirAccess::ACCESS_RESOURCES); - ERR_FAIL_COND(da.is_null()); - _edit_files_with_filter(da, filters, r_list, exclude); -} - HashSet EditorExportPlatform::get_features(const Ref &p_preset, bool p_debug) const { Ref platform = p_preset->get_platform(); List feature_list; @@ -1000,93 +794,53 @@ String EditorExportPlatform::_get_script_encryption_key(const Refget_script_encryption_key().to_lower(); } -Dictionary EditorExportPlatform::get_internal_export_files(const Ref &p_preset, bool p_debug) { - Dictionary files; - - // Text server support data. - if (TS->has_feature(TextServer::FEATURE_USE_SUPPORT_DATA)) { - bool include_data = (bool)get_project_setting(p_preset, "internationalization/locale/include_text_server_data"); - if (!include_data) { - Vector translations = get_project_setting(p_preset, "internationalization/locale/translations"); - translations.push_back(get_project_setting(p_preset, "internationalization/locale/fallback")); - for (const String &t : translations) { - if (TS->is_locale_using_support_data(t)) { - include_data = true; - break; - } - } - } - if (include_data) { - String ts_name = TS->get_support_data_filename(); - String ts_target = "res://" + ts_name; - if (!ts_name.is_empty()) { - bool export_ok = false; - if (FileAccess::exists(ts_target)) { // Include user supplied data file. - const PackedByteArray &ts_data = FileAccess::get_file_as_bytes(ts_target); - if (!ts_data.is_empty()) { - add_message(EXPORT_MESSAGE_INFO, TTR("Export"), TTR("Using user provided text server data, text display in the exported project might be broken if export template was built with different ICU version!")); - files[ts_target] = ts_data; - export_ok = true; - } - } else { - String current_version = GODOT_VERSION_FULL_CONFIG; - String template_path = EditorPaths::get_singleton()->get_export_templates_dir().path_join(current_version); - if (p_debug && p_preset->has("custom_template/debug") && p_preset->get("custom_template/debug") != "") { - template_path = p_preset->get("custom_template/debug").operator String().get_base_dir(); - } else if (!p_debug && p_preset->has("custom_template/release") && p_preset->get("custom_template/release") != "") { - template_path = p_preset->get("custom_template/release").operator String().get_base_dir(); - } - String data_file_name = template_path.path_join(ts_name); - if (FileAccess::exists(data_file_name)) { - const PackedByteArray &ts_data = FileAccess::get_file_as_bytes(data_file_name); - if (!ts_data.is_empty()) { - print_line("Using text server data from export templates."); - files[ts_target] = ts_data; - export_ok = true; - } - } else { - const PackedByteArray &ts_data = TS->get_support_data(); - if (!ts_data.is_empty()) { - add_message(EXPORT_MESSAGE_INFO, TTR("Export"), TTR("Using editor embedded text server data, text display in the exported project might be broken if export template was built with different ICU version!")); - files[ts_target] = ts_data; - export_ok = true; - } - } - } - if (!export_ok) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Export"), TTR("Missing text server data, text display in the exported project might be broken!")); - } - } - } +Error EditorExportPlatform::_generate_sparse_pck_metadata(const Ref &p_preset, EditorExportPlatformData::PackData &p_pack_data, Vector &r_data, bool p_async) { + Error err; + Ref ftmp = FileAccess::create_temp(FileAccess::WRITE_READ, "export_index", "tmp", false, &err); + if (err != OK) { + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not create temporary file!")); + return err; } + int64_t pck_start_pos = ftmp->get_position(); + uint64_t file_base_ofs = 0; + uint64_t dir_base_ofs = 0; + EditorExportPlatform::_store_header(ftmp, p_preset->get_enc_pck() && p_preset->get_enc_directory(), true, p_async, file_base_ofs, dir_base_ofs); - return files; -} - -Vector EditorExportPlatform::get_forced_export_files(const Ref &p_preset) { - Vector files; - - files.push_back(ProjectSettings::get_singleton()->get_global_class_list_path()); + // Write directory. + uint64_t dir_offset = ftmp->get_position(); + ftmp->seek(dir_base_ofs); + ftmp->store_64(dir_offset - pck_start_pos); + ftmp->seek(dir_offset); - String icon = ResourceUID::ensure_path(get_project_setting(p_preset, "application/config/icon")); - String splash = ResourceUID::ensure_path(get_project_setting(p_preset, "application/boot_splash/image")); - if (!icon.is_empty() && FileAccess::exists(icon)) { - files.push_back(icon); - } - if (!splash.is_empty() && FileAccess::exists(splash) && icon != splash) { - files.push_back(splash); + Vector key; + if (p_preset->get_enc_pck() && p_preset->get_enc_directory()) { + key = EditorExportPlatformUtils::convert_string_encryption_key_to_bytes(_get_script_encryption_key(p_preset)); } - String resource_cache_file = ResourceUID::get_cache_file(); - if (FileAccess::exists(resource_cache_file)) { - files.push_back(resource_cache_file); + + if (!EditorExportPlatformUtils::encrypt_and_store_directory(ftmp, p_pack_data, key, p_preset->get_seed(), 0)) { + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Can't create encrypted file.")); + return ERR_CANT_CREATE; } - String extension_list_config_file = GDExtension::get_extension_list_config_file(); - if (FileAccess::exists(extension_list_config_file)) { - files.push_back(extension_list_config_file); + r_data.resize(ftmp->get_length()); + ftmp->seek(0); + ftmp->get_buffer(r_data.ptrw(), r_data.size()); + ftmp.unref(); + + return OK; +} + +Dictionary EditorExportPlatform::get_internal_export_files(const Ref &p_preset, bool p_debug) { + Dictionary internal_export_files; + HashMap files = EditorExportPlatformUtils::get_internal_export_files(this, p_preset, p_debug); + for (const KeyValue &key_value : files) { + internal_export_files[key_value.key] = key_value.value; } + return internal_export_files; +} - return files; +Vector EditorExportPlatform::get_forced_export_files(const Ref &p_preset) { + return EditorExportPlatformUtils::get_forced_export_files(p_preset); } Error EditorExportPlatform::_script_save_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta) { @@ -1145,57 +899,7 @@ Error EditorExportPlatform::export_project_files(const Ref & HashSet paths; Vector path_remaps; - if (p_preset->get_export_filter() == EditorExportPreset::EXPORT_ALL_RESOURCES) { - //find stuff - _export_find_resources(EditorFileSystem::get_singleton()->get_filesystem(), paths); - } else if (p_preset->get_export_filter() == EditorExportPreset::EXCLUDE_SELECTED_RESOURCES) { - _export_find_resources(EditorFileSystem::get_singleton()->get_filesystem(), paths); - Vector files = p_preset->get_files_to_export(); - for (int i = 0; i < files.size(); i++) { - paths.erase(files[i]); - } - } else if (p_preset->get_export_filter() == EditorExportPreset::EXPORT_CUSTOMIZED) { - _export_find_customized_resources(p_preset, EditorFileSystem::get_singleton()->get_filesystem(), p_preset->get_file_export_mode("res://"), paths); - } else { - bool scenes_only = p_preset->get_export_filter() == EditorExportPreset::EXPORT_SELECTED_SCENES; - - Vector files = p_preset->get_files_to_export(); - for (int i = 0; i < files.size(); i++) { - if (scenes_only && ResourceLoader::get_resource_type(files[i]) != "PackedScene") { - continue; - } - - _export_find_dependencies(files[i], paths); - } - - // Add autoload resources and their dependencies - List props; - ProjectSettings::get_singleton()->get_property_list(&props); - - for (const PropertyInfo &pi : props) { - if (!pi.name.begins_with("autoload/")) { - continue; - } - - String autoload_path = get_project_setting(p_preset, pi.name); - - if (autoload_path.begins_with("*")) { - autoload_path = autoload_path.substr(1); - } - - _export_find_dependencies(autoload_path, paths); - } - } - - //add native icons to non-resource include list - _edit_filter_list(paths, String("*.icns"), false); - _edit_filter_list(paths, String("*.ico"), false); - - _edit_filter_list(paths, p_preset->get_include_filter(), false); - _edit_filter_list(paths, p_preset->get_exclude_filter(), true); - - // Ignore import files, since these are automatically added to the jar later with the resources - _edit_filter_list(paths, String("*.import"), true); + EditorExportPlatformUtils::export_find_preset_resources(p_preset, paths); // Get encryption filters. bool enc_pck = p_preset->get_enc_pck(); @@ -1225,36 +929,10 @@ Error EditorExportPlatform::export_project_files(const Ref & } // Get encryption key. - String script_key = _get_script_encryption_key(p_preset); - key.resize(32); - if (script_key.length() == 64) { - for (int i = 0; i < 32; i++) { - int v = 0; - if (i * 2 < script_key.length()) { - char32_t ct = script_key[i * 2]; - if (is_digit(ct)) { - ct = ct - '0'; - } else if (ct >= 'a' && ct <= 'f') { - ct = 10 + ct - 'a'; - } - v |= ct << 4; - } - - if (i * 2 + 1 < script_key.length()) { - char32_t ct = script_key[i * 2 + 1]; - if (is_digit(ct)) { - ct = ct - '0'; - } else if (ct >= 'a' && ct <= 'f') { - ct = 10 + ct - 'a'; - } - v |= ct; - } - key.write[i] = v; - } - } + key = EditorExportPlatformUtils::convert_string_encryption_key_to_bytes(_get_script_encryption_key(p_preset)); } - EditorExportSaveProxy save_proxy(p_save_func, p_remove_func != nullptr); + EditorExportPlatformData::EditorExportSaveProxy save_proxy(p_save_func, p_remove_func != nullptr); Error err = OK; Vector> export_plugins = EditorExport::get_singleton()->get_export_plugins(); @@ -1643,11 +1321,11 @@ Error EditorExportPlatform::export_project_files(const Ref & } } - Vector forced_export = get_forced_export_files(p_preset); + Vector forced_export = EditorExportPlatformUtils::get_forced_export_files(p_preset); for (int i = 0; i < forced_export.size(); i++) { Vector array; if (GDExtension::get_extension_list_config_file() == forced_export[i]) { - array = _filter_extension_list_config_file(forced_export[i], paths); + array = EditorExportPlatformUtils::filter_extension_list_config_file(forced_export[i], paths); if (array.is_empty()) { continue; } @@ -1696,24 +1374,8 @@ Error EditorExportPlatform::export_project_files(const Ref & return OK; } -Vector EditorExportPlatform::_filter_extension_list_config_file(const String &p_config_path, const HashSet &p_paths) { - Ref f = FileAccess::open(p_config_path, FileAccess::READ); - if (f.is_null()) { - ERR_FAIL_V_MSG(Vector(), "Can't open file from path '" + String(p_config_path) + "'."); - } - Vector data; - while (!f->eof_reached()) { - String l = f->get_line().strip_edges(); - if (p_paths.has(l)) { - data.append_array(l.to_utf8_buffer()); - data.append('\n'); - } - } - return data; -} - Error EditorExportPlatform::_pack_add_shared_object(const Ref &p_preset, void *p_userdata, const SharedObject &p_so) { - PackData *pack_data = (PackData *)p_userdata; + EditorExportPlatformData::PackData *pack_data = (EditorExportPlatformData::PackData *)p_userdata; if (pack_data->so_files) { pack_data->so_files->push_back(p_so); } @@ -1722,16 +1384,15 @@ Error EditorExportPlatform::_pack_add_shared_object(const Ref &p_preset, void *p_userdata, const String &p_path) { - PackData *pd = (PackData *)p_userdata; - - SavedData sd; + EditorExportPlatformData::PackData *pd = (EditorExportPlatformData::PackData *)p_userdata; + EditorExportPlatformData::SavedData sd; sd.path_utf8 = p_path.utf8(); sd.ofs = pd->f->get_position(); sd.size = 0; sd.removal = true; // This padding will likely never be added, as we should already be aligned when removals are added. - int pad = _get_pad(PCK_PADDING, pd->f->get_position()); + int pad = EditorExportPlatformUtils::get_pad(EditorExportPlatformData::PCK_PADDING, pd->f->get_position()); for (int i = 0; i < pad; i++) { pd->f->store_8(0); } @@ -1744,7 +1405,7 @@ Error EditorExportPlatform::_remove_pack_file(const Ref &p_p } Error EditorExportPlatform::_zip_add_shared_object(const Ref &p_preset, void *p_userdata, const SharedObject &p_so) { - ZipData *zip_data = (ZipData *)p_userdata; + EditorExportPlatformData::ZipData *zip_data = (EditorExportPlatformData::ZipData *)p_userdata; if (zip_data->so_files) { zip_data->so_files->push_back(p_so); } @@ -1848,7 +1509,7 @@ void EditorExportPlatform::zip_folder_recursive(zipFile &p_zip, const String &p_ Ref fa = FileAccess::open(dir.path_join(f), FileAccess::READ); if (fa.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("ZIP Creation"), vformat(TTR("Could not open file to read from path \"%s\"."), dir.path_join(f))); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("ZIP Creation"), vformat(TTR("Could not open file to read from path \"%s\"."), dir.path_join(f))); return; } const int bufsize = 16384; @@ -1959,7 +1620,7 @@ Dictionary EditorExportPlatform::_save_zip_patch(const Ref & return ret; } -bool EditorExportPlatform::_store_header(Ref p_fd, bool p_enc, bool p_sparse, uint64_t &r_file_base_ofs, uint64_t &r_dir_base_ofs) { +bool EditorExportPlatform::_store_header(Ref p_fd, bool p_enc, bool p_sparse, bool p_async, uint64_t &r_file_base_ofs, uint64_t &r_dir_base_ofs) { p_fd->store_32(PACK_HEADER_MAGIC); p_fd->store_32(PACK_FORMAT_VERSION); p_fd->store_32(GODOT_VERSION_MAJOR); @@ -1973,6 +1634,9 @@ bool EditorExportPlatform::_store_header(Ref p_fd, bool p_enc, bool if (p_sparse) { pack_flags |= PACK_SPARSE_BUNDLE; } + if (p_async) { + pack_flags |= PACK_ASYNC; + } p_fd->store_32(pack_flags); // Flags. r_file_base_ofs = p_fd->get_position(); @@ -1988,79 +1652,6 @@ bool EditorExportPlatform::_store_header(Ref p_fd, bool p_enc, bool return true; } -bool EditorExportPlatform::_encrypt_and_store_directory(Ref p_fd, PackData &p_pack_data, const Vector &p_key, uint64_t p_seed, uint64_t p_file_base) { - Ref fae; - Ref fhead = p_fd; - - fhead->store_32(p_pack_data.file_ofs.size()); //amount of files - - if (!p_key.is_empty()) { - uint64_t seed = p_seed; - fae.instantiate(); - if (fae.is_null()) { - return false; - } - - Vector iv; - if (seed != 0) { - for (int i = 0; i < p_pack_data.file_ofs.size(); i++) { - for (int64_t j = 0; j < p_pack_data.file_ofs[i].path_utf8.length(); j++) { - seed = ((seed << 5) + seed) ^ p_pack_data.file_ofs[i].path_utf8.get_data()[j]; - } - for (int64_t j = 0; j < p_pack_data.file_ofs[i].md5.size(); j++) { - seed = ((seed << 5) + seed) ^ p_pack_data.file_ofs[i].md5[j]; - } - seed = ((seed << 5) + seed) ^ (p_pack_data.file_ofs[i].ofs - p_file_base); - seed = ((seed << 5) + seed) ^ p_pack_data.file_ofs[i].size; - } - - RandomPCG rng = RandomPCG(seed); - iv.resize(16); - for (int i = 0; i < 16; i++) { - iv.write[i] = rng.rand() % 256; - } - } - - Error err = fae->open_and_parse(fhead, p_key, FileAccessEncrypted::MODE_WRITE_AES256, false, iv); - if (err != OK) { - return false; - } - - fhead = fae; - } - for (int i = 0; i < p_pack_data.file_ofs.size(); i++) { - uint32_t string_len = p_pack_data.file_ofs[i].path_utf8.length(); - uint32_t pad = _get_pad(4, string_len); - - fhead->store_32(string_len + pad); - fhead->store_buffer((const uint8_t *)p_pack_data.file_ofs[i].path_utf8.get_data(), string_len); - for (uint32_t j = 0; j < pad; j++) { - fhead->store_8(0); - } - - fhead->store_64(p_pack_data.file_ofs[i].ofs - p_file_base); - fhead->store_64(p_pack_data.file_ofs[i].size); // pay attention here, this is where file is - fhead->store_buffer(p_pack_data.file_ofs[i].md5.ptr(), 16); //also save md5 for file - uint32_t flags = 0; - if (p_pack_data.file_ofs[i].encrypted) { - flags |= PACK_FILE_ENCRYPTED; - } - if (p_pack_data.file_ofs[i].removal) { - flags |= PACK_FILE_REMOVAL; - } - if (p_pack_data.file_ofs[i].delta) { - flags |= PACK_FILE_DELTA; - } - fhead->store_32(flags); - } - - if (fae.is_valid()) { - fhead.unref(); - fae.unref(); - } - return true; -} - Error EditorExportPlatform::save_pack(const Ref &p_preset, bool p_debug, const String &p_path, Vector *p_so_files, EditorExportSaveFunction p_save_func, EditorExportRemoveFunction p_remove_func, bool p_embed, int64_t *r_embedded_start, int64_t *r_embedded_size) { EditorProgress ep("savepack", TTR("Packing"), 102, true); @@ -2078,14 +1669,14 @@ Error EditorExportPlatform::save_pack(const Ref &p_preset, b // Regular output to separate PCK file. f = FileAccess::open(p_path, FileAccess::WRITE); if (f.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), vformat(TTR("Can't open file for writing at path \"%s\"."), p_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Save PCK"), vformat(TTR("Can't open file for writing at path \"%s\"."), p_path)); return ERR_CANT_CREATE; } } else { // Append to executable. f = FileAccess::open(p_path, FileAccess::READ_WRITE); if (f.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), vformat(TTR("Can't open file for reading-writing at path \"%s\"."), p_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Save PCK"), vformat(TTR("Can't open file for reading-writing at path \"%s\"."), p_path)); return ERR_FILE_CANT_OPEN; } @@ -2107,10 +1698,10 @@ Error EditorExportPlatform::save_pack(const Ref &p_preset, b uint64_t file_base_ofs = 0; uint64_t dir_base_ofs = 0; - _store_header(f, p_preset->get_enc_pck() && p_preset->get_enc_directory(), false, file_base_ofs, dir_base_ofs); + _store_header(f, p_preset->get_enc_pck() && p_preset->get_enc_directory(), false, false, file_base_ofs, dir_base_ofs); // Align for first file. - int file_padding = _get_pad(PCK_PADDING, f->get_position()); + int file_padding = EditorExportPlatformUtils::get_pad(EditorExportPlatformData::PCK_PADDING, f->get_position()); for (int i = 0; i < file_padding; i++) { f->store_8(0); } @@ -2121,7 +1712,7 @@ Error EditorExportPlatform::save_pack(const Ref &p_preset, b f->seek(file_base); // Write files. - PackData pd; + EditorExportPlatformData::PackData pd; pd.ep = &ep; pd.f = f; pd.so_files = p_so_files; @@ -2130,18 +1721,18 @@ Error EditorExportPlatform::save_pack(const Ref &p_preset, b Error err = export_project_files(p_preset, p_debug, p_save_func, p_remove_func, &pd, _pack_add_shared_object); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Failed to export project files.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Failed to export project files.")); return err; } if (pd.file_ofs.is_empty()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("No files or changes to export.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("No files or changes to export.")); return FAILED; } pd.file_ofs.sort(); // Do sort, so we can do binary search later (where ?). - int dir_padding = _get_pad(PCK_PADDING, f->get_position()); + int dir_padding = EditorExportPlatformUtils::get_pad(EditorExportPlatformData::PCK_PADDING, f->get_position()); for (int i = 0; i < dir_padding; i++) { f->store_8(0); } @@ -2154,37 +1745,11 @@ Error EditorExportPlatform::save_pack(const Ref &p_preset, b Vector key; if (p_preset->get_enc_pck() && p_preset->get_enc_directory()) { - String script_key = _get_script_encryption_key(p_preset); - key.resize(32); - if (script_key.length() == 64) { - for (int i = 0; i < 32; i++) { - int v = 0; - if (i * 2 < script_key.length()) { - char32_t ct = script_key[i * 2]; - if (is_digit(ct)) { - ct = ct - '0'; - } else if (ct >= 'a' && ct <= 'f') { - ct = 10 + ct - 'a'; - } - v |= ct << 4; - } - - if (i * 2 + 1 < script_key.length()) { - char32_t ct = script_key[i * 2 + 1]; - if (is_digit(ct)) { - ct = ct - '0'; - } else if (ct >= 'a' && ct <= 'f') { - ct = 10 + ct - 'a'; - } - v |= ct; - } - key.write[i] = v; - } - } + key = EditorExportPlatformUtils::convert_string_encryption_key_to_bytes(_get_script_encryption_key(p_preset)); } - if (!_encrypt_and_store_directory(f, pd, key, p_preset->get_seed(), file_base)) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Can't create encrypted file.")); + if (!EditorExportPlatformUtils::encrypt_and_store_directory(f, pd, key, p_preset->get_seed(), file_base)) { + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Can't create encrypted file.")); return ERR_CANT_CREATE; } @@ -2226,14 +1791,14 @@ Error EditorExportPlatform::save_zip(const Ref &p_preset, bo zlib_filefunc_def io = zipio_create_io(&io_fa); zipFile zip = zipOpen2(tmppath.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io); - ZipData zd; + EditorExportPlatformData::ZipData zd; zd.ep = &ep; zd.zip = zip; zd.so_files = p_so_files; Error err = export_project_files(p_preset, p_debug, p_save_func, nullptr, &zd, _zip_add_shared_object); if (err != OK && err != ERR_SKIP) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Save ZIP"), TTR("Failed to export project files.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Save ZIP"), TTR("Failed to export project files.")); } zipClose(zip, nullptr); @@ -2242,14 +1807,14 @@ Error EditorExportPlatform::save_zip(const Ref &p_preset, bo if (zd.file_count == 0) { da->remove(tmppath); - add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("No files or changes to export.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("No files or changes to export.")); return FAILED; } err = da->rename(tmppath, p_path); if (err != OK) { da->remove(tmppath); - add_message(EXPORT_MESSAGE_ERROR, TTR("Save ZIP"), vformat(TTR("Failed to move temporary file \"%s\" to \"%s\"."), tmppath, p_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Save ZIP"), vformat(TTR("Failed to move temporary file \"%s\" to \"%s\"."), tmppath, p_path)); } return OK; @@ -2298,11 +1863,11 @@ Vector EditorExportPlatform::gen_export_flags(BitFieldhas_setting("export/android/use_wifi_for_remote_debug") && EDITOR_GET("export/android/use_wifi_for_remote_debug")) { host = EDITOR_GET("export/android/wifi_remote_debug_host"); - } else if (p_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST)) { + } else if (p_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST)) { host = "localhost"; } - if (p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) { + if (p_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_DUMB_CLIENT)) { int port = EDITOR_GET("filesystem/file_server/port"); String passwd = EDITOR_GET("filesystem/file_server/password"); ret.push_back("--remote-fs"); @@ -2313,7 +1878,7 @@ Vector EditorExportPlatform::gen_export_flags(BitField EditorExportPlatform::gen_export_flags(BitField &p_preset, const StringName &p_name) { - if (p_preset.is_valid()) { - return p_preset->get_project_setting(p_name); - } else { - return GLOBAL_GET(p_name); - } + return EditorExportPlatformUtils::get_project_setting(p_preset, p_name); } void EditorExportPlatform::_bind_methods() { @@ -2591,14 +2152,14 @@ void EditorExportPlatform::_bind_methods() { ClassDB::bind_static_method("EditorExportPlatform", D_METHOD("get_forced_export_files", "preset"), &EditorExportPlatform::get_forced_export_files, DEFVAL(Ref())); - BIND_ENUM_CONSTANT(EXPORT_MESSAGE_NONE); - BIND_ENUM_CONSTANT(EXPORT_MESSAGE_INFO); - BIND_ENUM_CONSTANT(EXPORT_MESSAGE_WARNING); - BIND_ENUM_CONSTANT(EXPORT_MESSAGE_ERROR); + BIND_ENUM_CONSTANT(EditorExportPlatformData::EXPORT_MESSAGE_NONE); + BIND_ENUM_CONSTANT(EditorExportPlatformData::EXPORT_MESSAGE_INFO); + BIND_ENUM_CONSTANT(EditorExportPlatformData::EXPORT_MESSAGE_WARNING); + BIND_ENUM_CONSTANT(EditorExportPlatformData::EXPORT_MESSAGE_ERROR); - BIND_BITFIELD_FLAG(DEBUG_FLAG_DUMB_CLIENT); - BIND_BITFIELD_FLAG(DEBUG_FLAG_REMOTE_DEBUG); - BIND_BITFIELD_FLAG(DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST); - BIND_BITFIELD_FLAG(DEBUG_FLAG_VIEW_COLLISIONS); - BIND_BITFIELD_FLAG(DEBUG_FLAG_VIEW_NAVIGATION); + BIND_BITFIELD_FLAG(EditorExportPlatformData::DEBUG_FLAG_DUMB_CLIENT); + BIND_BITFIELD_FLAG(EditorExportPlatformData::DEBUG_FLAG_REMOTE_DEBUG); + BIND_BITFIELD_FLAG(EditorExportPlatformData::DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST); + BIND_BITFIELD_FLAG(EditorExportPlatformData::DEBUG_FLAG_VIEW_COLLISIONS); + BIND_BITFIELD_FLAG(EditorExportPlatformData::DEBUG_FLAG_VIEW_NAVIGATION); } diff --git a/editor/export/editor_export_platform.h b/editor/export/editor_export_platform.h index 3c4f3c1cc906..f2dde1d7b1bc 100644 --- a/editor/export/editor_export_platform.h +++ b/editor/export/editor_export_platform.h @@ -32,11 +32,11 @@ #include "core/io/zip_io.h" #include "core/os/os.h" +#include "editor/export/editor_export_platform_utils.h" #include "editor/export/editor_export_preset.h" class DirAccess; class EditorExportPlugin; -class EditorFileSystemDirectory; class Image; class Node; class RichTextLabel; @@ -49,80 +49,33 @@ const String ENV_SCRIPT_ENCRYPTION_KEY = "GODOT_SCRIPT_ENCRYPTION_KEY"; class EditorExportPlatform : public RefCounted { GDCLASS(EditorExportPlatform, RefCounted); + friend class EditorExportPlatformUtils; + protected: static void _bind_methods(); public: - typedef Error (*EditorExportSaveFunction)(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); - typedef Error (*EditorExportRemoveFunction)(const Ref &p_preset, void *p_userdata, const String &p_path); - typedef Error (*EditorExportSaveSharedObject)(const Ref &p_preset, void *p_userdata, const SharedObject &p_so); - - enum DebugFlags { - DEBUG_FLAG_DUMB_CLIENT = 1, - DEBUG_FLAG_REMOTE_DEBUG = 2, - DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST = 4, - DEBUG_FLAG_VIEW_COLLISIONS = 8, - DEBUG_FLAG_VIEW_NAVIGATION = 16, - }; - - enum ExportMessageType { - EXPORT_MESSAGE_NONE, - EXPORT_MESSAGE_INFO, - EXPORT_MESSAGE_WARNING, - EXPORT_MESSAGE_ERROR, - }; + typedef EditorExportPlatformData::EditorExportSaveFunction EditorExportSaveFunction; + typedef EditorExportPlatformData::EditorExportRemoveFunction EditorExportRemoveFunction; + typedef EditorExportPlatformData::EditorExportSaveSharedObject EditorExportSaveSharedObject; - struct ExportMessage { - ExportMessageType msg_type; - String category; - String text; - }; - - struct SavedData { - uint64_t ofs = 0; - uint64_t size = 0; - bool encrypted = false; - bool removal = false; - bool delta = false; - Vector md5; - CharString path_utf8; - - bool operator<(const SavedData &p_data) const { - return path_utf8 < p_data.path_utf8; - } - }; + typedef EditorExportPlatformData::DebugFlags DebugFlags; + typedef EditorExportPlatformData::ExportMessageType ExportMessageType; + typedef EditorExportPlatformData::ExportMessage ExportMessage; - struct PackData { - String path; - Ref f; - Vector file_ofs; - EditorProgress *ep = nullptr; - Vector *so_files = nullptr; - bool use_sparse_pck = false; - }; + friend bool EditorExportPlatformUtils::encrypt_and_store_directory(Ref p_fd, EditorExportPlatformData::PackData &p_pack_data, const Vector &p_key, uint64_t p_seed, uint64_t p_file_base); - static bool _store_header(Ref p_fd, bool p_enc, bool p_sparse, uint64_t &r_file_base_ofs, uint64_t &r_dir_base_ofs); - static bool _encrypt_and_store_directory(Ref p_fd, PackData &p_pack_data, const Vector &p_key, uint64_t p_seed, uint64_t p_file_base); - static Error _encrypt_and_store_data(Ref p_fd, const String &p_path, const Vector &p_data, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool &r_encrypt); + static bool _store_header(Ref p_fd, bool p_enc, bool p_sparse, bool p_async, uint64_t &r_file_base_ofs, uint64_t &r_dir_base_ofs); String _get_script_encryption_key(const Ref &p_preset) const; + Error _generate_sparse_pck_metadata(const Ref &p_preset, EditorExportPlatformData::PackData &p_pack_data, Vector &r_data, bool p_async = false); private: - struct ZipData { - void *zip = nullptr; - EditorProgress *ep = nullptr; - Vector *so_files = nullptr; - int file_count = 0; - }; - Vector messages; - void _export_find_resources(EditorFileSystemDirectory *p_dir, HashSet &p_paths); - void _export_find_customized_resources(const Ref &p_preset, EditorFileSystemDirectory *p_dir, EditorExportPreset::FileExportMode p_mode, HashSet &p_paths); - void _export_find_dependencies(const String &p_path, HashSet &p_paths); - static Error _save_pack_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); static Error _save_pack_patch_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); static Error _pack_add_shared_object(const Ref &p_preset, void *p_userdata, const SharedObject &p_so); + static bool _check_hash(const uint8_t *p_hash, const Vector &p_data); static Error _remove_pack_file(const Ref &p_preset, void *p_userdata, const String &p_path); @@ -138,11 +91,6 @@ class EditorExportPlatform : public RefCounted { static Error _script_save_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); static Error _script_add_shared_object(const Ref &p_preset, void *p_userdata, const SharedObject &p_so); - void _edit_files_with_filter(Ref &da, const Vector &p_filters, HashSet &r_list, bool exclude); - void _edit_filter_list(HashSet &r_list, const String &p_filter, bool exclude); - - static Vector _filter_extension_list_config_file(const String &p_config_path, const HashSet &p_paths); - struct FileExportCache { uint64_t source_modified_time = 0; String source_md5; @@ -245,13 +193,13 @@ class EditorExportPlatform : public RefCounted { msg.msg_type = p_type; messages.push_back(msg); switch (p_type) { - case EXPORT_MESSAGE_INFO: { + case EditorExportPlatformData::EXPORT_MESSAGE_INFO: { print_line(vformat("%s: %s", msg.category, msg.text)); } break; - case EXPORT_MESSAGE_WARNING: { + case EditorExportPlatformData::EXPORT_MESSAGE_WARNING: { WARN_PRINT(vformat("%s: %s", msg.category, msg.text)); } break; - case EXPORT_MESSAGE_ERROR: { + case EditorExportPlatformData::EXPORT_MESSAGE_ERROR: { ERR_PRINT(vformat("%s: %s", msg.category, msg.text)); } break; default: @@ -269,7 +217,7 @@ class EditorExportPlatform : public RefCounted { } virtual ExportMessageType _get_message_type(int p_index) const { - ERR_FAIL_INDEX_V(p_index, messages.size(), EXPORT_MESSAGE_NONE); + ERR_FAIL_INDEX_V(p_index, messages.size(), EditorExportPlatformData::EXPORT_MESSAGE_NONE); return messages[p_index].msg_type; } @@ -284,7 +232,7 @@ class EditorExportPlatform : public RefCounted { } virtual ExportMessageType get_worst_message_type() const { - ExportMessageType worst_type = EXPORT_MESSAGE_NONE; + ExportMessageType worst_type = EditorExportPlatformData::EXPORT_MESSAGE_NONE; for (int i = 0; i < messages.size(); i++) { worst_type = MAX(worst_type, messages[i].msg_type); } diff --git a/editor/export/editor_export_platform_apple_embedded.cpp b/editor/export/editor_export_platform_apple_embedded.cpp index c11a7d69ac73..b676b54c4237 100644 --- a/editor/export/editor_export_platform_apple_embedded.cpp +++ b/editor/export/editor_export_platform_apple_embedded.cpp @@ -36,6 +36,7 @@ #include "editor/editor_node.h" #include "editor/editor_string_names.h" #include "editor/export/editor_export.h" +#include "editor/export/editor_export_platform_data.h" #include "editor/export/lipo.h" #include "editor/export/macho.h" #include "editor/file_system/editor_paths.h" @@ -1632,7 +1633,7 @@ Error EditorExportPlatformAppleEmbedded::_export_project_helper(const Ref= Math::floor(total_files * 0.8))) { da->erase_contents_recursive(); } else { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Unexpected files found in the export destination directory \"%s.xcodeproj\", delete it manually or select another destination."), binary_dir)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Unexpected files found in the export destination directory \"%s.xcodeproj\", delete it manually or select another destination."), binary_dir)); return ERR_CANT_CREATE; } } @@ -1725,7 +1726,7 @@ Error EditorExportPlatformAppleEmbedded::_export_project_helper(const Ref= Math::floor(total_files * 0.8))) { da->erase_contents_recursive(); } else { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Unexpected files found in the export destination directory \"%s\", delete it manually or select another destination."), binary_dir)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Unexpected files found in the export destination directory \"%s\", delete it manually or select another destination."), binary_dir)); return ERR_CANT_CREATE; } } @@ -1734,7 +1735,7 @@ Error EditorExportPlatformAppleEmbedded::_export_project_helper(const Refdir_exists(binary_dir)) { Error err = da->make_dir(binary_dir); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Failed to create the directory: \"%s\""), binary_dir)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Failed to create the directory: \"%s\""), binary_dir)); return err; } } @@ -1800,7 +1801,7 @@ Error EditorExportPlatformAppleEmbedded::_export_project_helper(const Ref tmp_app_path = DirAccess::create_for_path(dest_dir); if (tmp_app_path.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not create and open the directory: \"%s\""), dest_dir)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not create and open the directory: \"%s\""), dest_dir)); return ERR_CANT_CREATE; } @@ -1809,7 +1810,7 @@ Error EditorExportPlatformAppleEmbedded::_export_project_helper(const Refmake_dir_recursive(dir_name); if (dir_err) { unzClose(src_pkg_zip); - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create a directory at path \"%s\"."), dir_name)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create a directory at path \"%s\"."), dir_name)); return ERR_CANT_CREATE; } } @@ -1896,7 +1897,7 @@ Error EditorExportPlatformAppleEmbedded::_export_project_helper(const Ref f = FileAccess::open(file, FileAccess::WRITE); if (f.is_null()) { unzClose(src_pkg_zip); - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write to a file at path \"%s\"."), file)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write to a file at path \"%s\"."), file)); return ERR_CANT_CREATE; }; f->store_buffer(data.ptr(), data.size()); @@ -1917,7 +1918,7 @@ Error EditorExportPlatformAppleEmbedded::_export_project_helper(const Refcopy(static_lib_path, dest_lib_file_path); if (lib_copy_err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not copy a file at path \"%s\" to \"%s\"."), static_lib_path, dest_lib_file_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not copy a file at path \"%s\" to \"%s\"."), static_lib_path, dest_lib_file_path)); return lib_copy_err; } } @@ -1999,7 +2000,7 @@ Error EditorExportPlatformAppleEmbedded::_export_project_helper(const Refmake_dir_recursive(iconset_dir); } if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create a directory at path \"%s\"."), iconset_dir)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create a directory at path \"%s\"."), iconset_dir)); return err; } @@ -2014,7 +2015,7 @@ Error EditorExportPlatformAppleEmbedded::_export_project_helper(const Ref launch_screen_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); if (launch_screen_da.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not access the filesystem.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not access the filesystem.")); return ERR_CANT_CREATE; } @@ -2022,7 +2023,7 @@ Error EditorExportPlatformAppleEmbedded::_export_project_helper(const Ref f = FileAccess::open(project_file_name, FileAccess::WRITE); if (f.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write to a file at path \"%s\"."), project_file_name)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write to a file at path \"%s\"."), project_file_name)); return ERR_CANT_CREATE; }; f->store_buffer(project_file_data.ptr(), project_file_data.size()); @@ -2053,7 +2054,7 @@ Error EditorExportPlatformAppleEmbedded::_export_project_helper(const Ref &p_pr String can_export_error; bool can_export_missing_templates; if (!can_export(p_preset, can_export_error, can_export_missing_templates)) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), can_export_error); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Run"), can_export_error); return ERR_UNCONFIGURED; } @@ -2652,11 +2653,11 @@ Error EditorExportPlatformAppleEmbedded::run(const Ref &p_pr String host = EDITOR_GET("network/debug/remote_host"); int remote_port = (int)EDITOR_GET("network/debug/remote_port"); - if (p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST)) { + if (p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST)) { host = "localhost"; } - if (p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) { + if (p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_DUMB_CLIENT)) { int port = EDITOR_GET("filesystem/file_server/port"); String passwd = EDITOR_GET("filesystem/file_server/password"); cmd_args_list.push_back("--remote-fs"); @@ -2667,7 +2668,7 @@ Error EditorExportPlatformAppleEmbedded::run(const Ref &p_pr } } - if (p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG)) { + if (p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_REMOTE_DEBUG)) { cmd_args_list.push_back("--remote-debug"); cmd_args_list.push_back(get_debug_protocol() + host + ":" + String::num_int64(remote_port)); @@ -2689,11 +2690,11 @@ Error EditorExportPlatformAppleEmbedded::run(const Ref &p_pr } } - if (p_debug_flags.has_flag(DEBUG_FLAG_VIEW_COLLISIONS)) { + if (p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_VIEW_COLLISIONS)) { cmd_args_list.push_back("--debug-collisions"); } - if (p_debug_flags.has_flag(DEBUG_FLAG_VIEW_NAVIGATION)) { + if (p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_VIEW_NAVIGATION)) { cmd_args_list.push_back("--debug-navigation"); } @@ -2727,12 +2728,12 @@ Error EditorExportPlatformAppleEmbedded::run(const Ref &p_pr int ec; err = OS::get_singleton()->execute(idepl, args, &log, &ec, true); if (err != OK) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Run"), TTR("Could not start ios-deploy executable.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Run"), TTR("Could not start ios-deploy executable.")); CLEANUP_AND_RETURN(err); } if (ec != 0) { print_line("ios-deploy:\n" + log); - add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Installation/running failed, see editor log for details.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Installation/running failed, see editor log for details.")); CLEANUP_AND_RETURN(ERR_UNCONFIGURED); } } @@ -2756,7 +2757,7 @@ Error EditorExportPlatformAppleEmbedded::run(const Ref &p_pr }); if (ec != 0) { print_line("device install:\n" + log); - add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Installation failed, see editor log for details.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Installation failed, see editor log for details.")); CLEANUP_AND_RETURN(ERR_UNCONFIGURED); } } @@ -2783,7 +2784,7 @@ Error EditorExportPlatformAppleEmbedded::run(const Ref &p_pr }); if (ec != 0) { print_line("devicectl launch:\n" + log); - add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Running failed, see editor log for details.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Running failed, see editor log for details.")); } } } diff --git a/editor/export/editor_export_platform_data.cpp b/editor/export/editor_export_platform_data.cpp new file mode 100644 index 000000000000..6dc4ecc3a917 --- /dev/null +++ b/editor/export/editor_export_platform_data.cpp @@ -0,0 +1,39 @@ +/**************************************************************************/ +/* editor_export_platform_data.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "editor_export_platform_data.h" + +Error EditorExportPlatformData::EditorExportSaveProxy::save_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta) { + if (tracking_saves) { + saved_paths.insert(p_path.simplify_path().trim_prefix("res://")); + } + + return save_func(p_preset, p_userdata, p_path, p_data, p_file, p_total, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, p_delta); +} diff --git a/editor/export/editor_export_platform_data.h b/editor/export/editor_export_platform_data.h new file mode 100644 index 000000000000..c11d4b715916 --- /dev/null +++ b/editor/export/editor_export_platform_data.h @@ -0,0 +1,111 @@ +/**************************************************************************/ +/* editor_export_platform_data.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "core/io/file_access.h" +#include "core/os/shared_object.h" +#include "editor/editor_node.h" + +class EditorFileSystemDirectory; + +class EditorExportPlatformData { +public: + static constexpr int PCK_PADDING = 16; + + enum ExportMessageType { + EXPORT_MESSAGE_NONE, + EXPORT_MESSAGE_INFO, + EXPORT_MESSAGE_WARNING, + EXPORT_MESSAGE_ERROR, + }; + + struct ExportMessage { + ExportMessageType msg_type; + String category; + String text; + }; + + struct SavedData { + uint64_t ofs = 0; + uint64_t size = 0; + bool encrypted = false; + bool removal = false; + bool delta = false; + Vector md5; + CharString path_utf8; + + bool operator<(const SavedData &p_data) const { + return path_utf8 < p_data.path_utf8; + } + }; + + struct PackData { + String path; + Ref f; + Vector file_ofs; + EditorProgress *ep = nullptr; + Vector *so_files = nullptr; + bool use_sparse_pck = false; + }; + + enum DebugFlags { + DEBUG_FLAG_DUMB_CLIENT = 1, + DEBUG_FLAG_REMOTE_DEBUG = 2, + DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST = 4, + DEBUG_FLAG_VIEW_COLLISIONS = 8, + DEBUG_FLAG_VIEW_NAVIGATION = 16, + }; + + struct ZipData { + void *zip = nullptr; + EditorProgress *ep = nullptr; + Vector *so_files = nullptr; + int file_count = 0; + }; + + typedef Error (*EditorExportSaveFunction)(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); + typedef Error (*EditorExportRemoveFunction)(const Ref &p_preset, void *p_userdata, const String &p_path); + typedef Error (*EditorExportSaveSharedObject)(const Ref &p_preset, void *p_userdata, const SharedObject &p_so); + + class EditorExportSaveProxy { + HashSet saved_paths; + EditorExportSaveFunction save_func; + bool tracking_saves = false; + + public: + bool has_saved(const String &p_path) const { return saved_paths.has(p_path); } + + Error save_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); + + EditorExportSaveProxy(EditorExportSaveFunction p_save_func, bool p_track_saves) : + save_func(p_save_func), tracking_saves(p_track_saves) {} + }; +}; diff --git a/editor/export/editor_export_platform_pc.cpp b/editor/export/editor_export_platform_pc.cpp index c513b7ebfb6b..f07373838297 100644 --- a/editor/export/editor_export_platform_pc.cpp +++ b/editor/export/editor_export_platform_pc.cpp @@ -152,7 +152,7 @@ Error EditorExportPlatformPC::export_project(const Ref &p_pr Error EditorExportPlatformPC::prepare_template(const Ref &p_preset, bool p_debug, const String &p_path, BitField p_flags) { if (!DirAccess::exists(p_path.get_base_dir())) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Template"), TTR("The given export path doesn't exist.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Prepare Template"), TTR("The given export path doesn't exist.")); return ERR_FILE_BAD_PATH; } @@ -168,7 +168,7 @@ Error EditorExportPlatformPC::prepare_template(const Ref &p_ } if (!template_path.is_empty() && !FileAccess::exists(template_path)) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Template"), vformat(TTR("Template file not found: \"%s\"."), template_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Prepare Template"), vformat(TTR("Template file not found: \"%s\"."), template_path)); return ERR_FILE_NOT_FOUND; } @@ -196,7 +196,7 @@ Error EditorExportPlatformPC::prepare_template(const Ref &p_ } } if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Template"), TTR("Failed to copy export template.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Prepare Template"), TTR("Failed to copy export template.")); return err; } @@ -218,7 +218,7 @@ Error EditorExportPlatformPC::export_project_data(const Ref Error err = save_pack(p_preset, p_debug, pck_path, &so_files, nullptr, nullptr, p_preset->get("binary_format/embed_pck"), &embedded_pos, &embedded_size); if (err == OK && p_preset->get("binary_format/embed_pck")) { if (embedded_size >= 0x100000000 && String(p_preset->get("binary_format/architecture")).contains("32")) { - add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("On 32-bit exports the embedded PCK cannot be bigger than 4 GiB.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("On 32-bit exports the embedded PCK cannot be bigger than 4 GiB.")); return ERR_INVALID_PARAMETER; } @@ -244,13 +244,13 @@ Error EditorExportPlatformPC::export_project_data(const Ref if (err == OK) { err = da->copy_dir(src_path, target_path, -1, true); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("GDExtension"), vformat(TTR("Failed to copy shared object \"%s\"."), src_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("GDExtension"), vformat(TTR("Failed to copy shared object \"%s\"."), src_path)); } } } else { err = da->copy(src_path, target_path); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("GDExtension"), vformat(TTR("Failed to copy shared object \"%s\"."), src_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("GDExtension"), vformat(TTR("Failed to copy shared object \"%s\"."), src_path)); } if (err == OK) { err = sign_shared_object(p_preset, p_debug, target_path); diff --git a/editor/export/editor_export_platform_utils.cpp b/editor/export/editor_export_platform_utils.cpp new file mode 100644 index 000000000000..c97652dadbb0 --- /dev/null +++ b/editor/export/editor_export_platform_utils.cpp @@ -0,0 +1,657 @@ +/**************************************************************************/ +/* editor_export_platform_utils.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "editor_export_platform_utils.h" + +#include "core/config/project_settings.h" +#include "core/crypto/crypto_core.h" +#include "core/extension/gdextension.h" +#include "core/io/file_access_encrypted.h" +#include "core/io/file_access_pack.h" +#include "core/math/random_pcg.h" +#include "core/version.h" +#include "editor/export/editor_export_platform.h" +#include "editor/file_system/editor_file_system.h" +#include "editor/file_system/editor_paths.h" + +/** + * EditorExportPlatformUtils::AsyncPckFileDependenciesState + */ +void EditorExportPlatformUtils::AsyncPckFileDependenciesState::add_to_file_dependencies(const String &p_file) { + String file = p_file.strip_edges().simplify_path(); + + if (file_dependencies.has(file)) { + return; + } + + List dependencies; + ResourceLoader::get_dependencies(file, &dependencies); + for (const String &dependency : dependencies) { + String dependency_path = EditorExportPlatformUtils::get_path_from_dependency(dependency); + if (!file_dependencies.has(file)) { + HashSet *dependency_list = &file_dependencies_lists.push_back({})->get(); + file_dependencies[file] = dependency_list; + } + file_dependencies[file]->insert(dependency_path); + add_to_file_dependencies(dependency_path); + } +} + +void EditorExportPlatformUtils::AsyncPckFileDependenciesState::add_to_file_dependencies(const HashSet &p_file_set) { + for (const String &file : p_file_set) { + if (file.ends_with("/")) { + continue; + } + add_to_file_dependencies(file); + } +} + +HashMap *> EditorExportPlatformUtils::AsyncPckFileDependenciesState::get_file_dependencies_of(const HashSet &p_file_set) { + HashMap *> dependencies; + for (const String &file : p_file_set) { + _get_file_dependencies_of(file, dependencies); + } + return dependencies; +} + +HashMap *> EditorExportPlatformUtils::AsyncPckFileDependenciesState::get_file_dependencies_of(const String &p_file) { + HashMap *> dependencies; + _get_file_dependencies_of(p_file, dependencies); + return dependencies; +} + +void EditorExportPlatformUtils::AsyncPckFileDependenciesState::_get_file_dependencies_of(const String &p_file, HashMap *> &p_dependencies) { + if (p_dependencies.has(p_file)) { + return; + } + if (!file_dependencies.has(p_file)) { + p_dependencies[p_file] = {}; + return; + } + ERR_FAIL_NULL(file_dependencies[p_file]); + + p_dependencies[p_file] = file_dependencies[p_file]; + for (const String &file_dependency : *file_dependencies[p_file]) { + _get_file_dependencies_of(file_dependency, p_dependencies); + } +} + +/** + * EditorExportPlatformUtils + */ +String EditorExportPlatformUtils::get_path_from_dependency(const String &p_dependency) { + String path = p_dependency; + if (path.contains("::")) { + return path.get_slice("::", 2); + } + if (path.begins_with("uid://")) { + return ResourceUID::get_singleton()->uid_to_path(path); + } + return path.simplify_path(); +} + +int EditorExportPlatformUtils::get_pad(int p_alignment, int p_n) { + int rest = p_n % p_alignment; + int pad = 0; + if (rest > 0) { + pad = p_alignment - rest; + }; + + return pad; +} + +Variant EditorExportPlatformUtils::get_project_setting(const Ref &p_preset, const StringName &p_name) { + if (p_preset.is_valid()) { + return p_preset->get_project_setting(p_name); + } else { + return GLOBAL_GET(p_name); + } +} + +bool EditorExportPlatformUtils::encrypt_and_store_directory(Ref p_fd, EditorExportPlatformData::PackData &p_pack_data, const Vector &p_key, uint64_t p_seed, uint64_t p_file_base) { + Ref fae; + Ref fhead = p_fd; + + // Amount of files. + fhead->store_32(p_pack_data.file_ofs.size()); + + if (!p_key.is_empty()) { + uint64_t seed = p_seed; + fae.instantiate(); + if (fae.is_null()) { + return false; + } + + Vector iv; + if (seed != 0) { + for (int i = 0; i < p_pack_data.file_ofs.size(); i++) { + for (int64_t j = 0; j < p_pack_data.file_ofs[i].path_utf8.length(); j++) { + seed = ((seed << 5) + seed) ^ p_pack_data.file_ofs[i].path_utf8.get_data()[j]; + } + for (int64_t j = 0; j < p_pack_data.file_ofs[i].md5.size(); j++) { + seed = ((seed << 5) + seed) ^ p_pack_data.file_ofs[i].md5[j]; + } + seed = ((seed << 5) + seed) ^ (p_pack_data.file_ofs[i].ofs - p_file_base); + seed = ((seed << 5) + seed) ^ p_pack_data.file_ofs[i].size; + } + + RandomPCG rng = RandomPCG(seed); + iv.resize(16); + for (int i = 0; i < 16; i++) { + iv.write[i] = rng.rand() % 256; + } + } + + Error err = fae->open_and_parse(fhead, p_key, FileAccessEncrypted::MODE_WRITE_AES256, false, iv); + if (err != OK) { + return false; + } + + fhead = fae; + } + for (int i = 0; i < p_pack_data.file_ofs.size(); i++) { + uint32_t string_len = p_pack_data.file_ofs[i].path_utf8.length(); + uint32_t pad = EditorExportPlatformUtils::get_pad(4, string_len); + + fhead->store_32(string_len + pad); + fhead->store_buffer((const uint8_t *)p_pack_data.file_ofs[i].path_utf8.get_data(), string_len); + for (uint32_t j = 0; j < pad; j++) { + fhead->store_8(0); + } + + fhead->store_64(p_pack_data.file_ofs[i].ofs - p_file_base); + fhead->store_64(p_pack_data.file_ofs[i].size); // Pay attention here, this is where file is. + fhead->store_buffer(p_pack_data.file_ofs[i].md5.ptr(), 16); // Also save md5 for file. + uint32_t flags = 0; + if (p_pack_data.file_ofs[i].encrypted) { + flags |= PACK_FILE_ENCRYPTED; + } + if (p_pack_data.file_ofs[i].removal) { + flags |= PACK_FILE_REMOVAL; + } + if (p_pack_data.file_ofs[i].delta) { + flags |= PACK_FILE_DELTA; + } + fhead->store_32(flags); + } + + if (fae.is_valid()) { + fhead.unref(); + fae.unref(); + } + return true; +} + +Error EditorExportPlatformUtils::encrypt_and_store_data(Ref p_fd, const String &p_path, const Vector &p_data, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool &r_encrypt) { + r_encrypt = false; + for (const String &enc_in_filter : p_enc_in_filters) { + if (p_path.matchn(enc_in_filter) || p_path.trim_prefix("res://").matchn(enc_in_filter)) { + r_encrypt = true; + break; + } + } + + for (const String &enc_ex_filter : p_enc_ex_filters) { + if (p_path.matchn(enc_ex_filter) || p_path.trim_prefix("res://").matchn(enc_ex_filter)) { + r_encrypt = false; + break; + } + } + + Ref fae; + Ref ftmp = p_fd; + if (r_encrypt) { + Vector iv; + if (p_seed != 0) { + uint64_t seed = p_seed; + + const uint8_t *ptr = p_data.ptr(); + int64_t len = p_data.size(); + for (int64_t i = 0; i < len; i++) { + seed = ((seed << 5) + seed) ^ ptr[i]; + } + + RandomPCG rng = RandomPCG(seed); + iv.resize(16); + for (int i = 0; i < 16; i++) { + iv.write[i] = rng.rand() % 256; + } + } + + fae.instantiate(); + ERR_FAIL_COND_V(fae.is_null(), ERR_FILE_CANT_OPEN); + + Error err = fae->open_and_parse(ftmp, p_key, FileAccessEncrypted::MODE_WRITE_AES256, false, iv); + ERR_FAIL_COND_V(err != OK, ERR_FILE_CANT_OPEN); + ftmp = fae; + } + + // Store file content. + ftmp->store_buffer(p_data.ptr(), p_data.size()); + + if (fae.is_valid()) { + ftmp.unref(); + fae.unref(); + } + return OK; +} + +Error EditorExportPlatformUtils::store_temp_file(const String &p_simplified_path, const PackedByteArray &p_data, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const PackedByteArray &p_key, uint64_t p_seed, bool p_delta, PackedByteArray &r_enc_data, EditorExportPlatformData::SavedData &r_sd) { + Error err = OK; + Ref ftmp = FileAccess::create_temp(FileAccess::WRITE_READ, "export", "tmp", false, &err); + if (err != OK) { + return err; + } + r_sd.path_utf8 = p_simplified_path.trim_prefix("res://").utf8(); + r_sd.ofs = 0; + r_sd.size = p_data.size(); + r_sd.delta = p_delta; + err = EditorExportPlatformUtils::encrypt_and_store_data(ftmp, p_simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, r_sd.encrypted); + if (err != OK) { + return err; + } + + r_enc_data.resize(ftmp->get_length()); + ftmp->seek(0); + ftmp->get_buffer(r_enc_data.ptrw(), r_enc_data.size()); + ftmp.unref(); + + // Store MD5 of original file. + { + unsigned char hash[16]; + CryptoCore::md5(p_data.ptr(), p_data.size(), hash); + r_sd.md5.resize(16); + for (int i = 0; i < 16; i++) { + r_sd.md5.write[i] = hash[i]; + } + } + return OK; +} + +// Utility method used to create a directory. +Error EditorExportPlatformUtils::create_directory(const String &p_dir) { + if (DirAccess::exists(p_dir)) { + return OK; + } + Ref filesystem_da = DirAccess::create(DirAccess::ACCESS_RESOURCES); + ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); + Error err = filesystem_da->make_dir_recursive(p_dir); + ERR_FAIL_COND_V_MSG(err, ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); + return OK; +} + +// Writes p_data into a file at p_path, creating directories if necessary. +// Note: this will overwrite the file at p_path if it already exists. +Error EditorExportPlatformUtils::store_file_at_path(const String &p_path, const PackedByteArray &p_data) { + String dir = p_path.get_base_dir(); + Error err = EditorExportPlatformUtils::create_directory(dir); + if (err != OK) { + return err; + } + Ref fa = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); + fa->store_buffer(p_data.ptr(), p_data.size()); + return OK; +} + +// Writes string p_data into a file at p_path, creating directories if necessary. +// Note: this will overwrite the file at p_path if it already exists. +Error EditorExportPlatformUtils::store_string_at_path(const String &p_path, const String &p_data) { + String dir = p_path.get_base_dir(); + Error err = EditorExportPlatformUtils::create_directory(dir); + if (err != OK) { + if (OS::get_singleton()->is_stdout_verbose()) { + print_error("Unable to write data into " + p_path); + } + return err; + } + Ref fa = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); + fa->store_string(p_data); + return OK; +} + +PackedByteArray EditorExportPlatformUtils::convert_string_encryption_key_to_bytes(const String &p_encryption_key) { + PackedByteArray key; + key.resize_initialized(32); + ERR_FAIL_COND_V(p_encryption_key.length() != 64, key); + + for (int i = 0; i < 32; i++) { + int v = 0; + if (i * 2 < p_encryption_key.length()) { + char32_t ct = p_encryption_key[i * 2]; + if (is_digit(ct)) { + ct = ct - '0'; + } else if (ct >= 'a' && ct <= 'f') { + ct = 10 + ct - 'a'; + } + v |= ct << 4; + } + + if (i * 2 + 1 < p_encryption_key.length()) { + char32_t ct = p_encryption_key[i * 2 + 1]; + if (is_digit(ct)) { + ct = ct - '0'; + } else if (ct >= 'a' && ct <= 'f') { + ct = 10 + ct - 'a'; + } + v |= ct; + } + key.write[i] = v; + } + + return key; +} + +void EditorExportPlatformUtils::export_find_resources(EditorFileSystemDirectory *p_dir, HashSet &p_paths) { + for (int i = 0; i < p_dir->get_subdir_count(); i++) { + EditorExportPlatformUtils::export_find_resources(p_dir->get_subdir(i), p_paths); + } + + for (int i = 0; i < p_dir->get_file_count(); i++) { + if (p_dir->get_file_type(i) == "TextFile") { + continue; + } + p_paths.insert(p_dir->get_file_path(i)); + } +} + +void EditorExportPlatformUtils::export_find_customized_resources(const Ref &p_preset, EditorFileSystemDirectory *p_dir, EditorExportPreset::FileExportMode p_mode, HashSet &p_paths) { + for (int i = 0; i < p_dir->get_subdir_count(); i++) { + EditorFileSystemDirectory *subdir = p_dir->get_subdir(i); + EditorExportPlatformUtils::export_find_customized_resources(p_preset, subdir, p_preset->get_file_export_mode(subdir->get_path(), p_mode), p_paths); + } + + for (int i = 0; i < p_dir->get_file_count(); i++) { + if (p_dir->get_file_type(i) == "TextFile") { + continue; + } + String path = p_dir->get_file_path(i); + EditorExportPreset::FileExportMode file_mode = p_preset->get_file_export_mode(path, p_mode); + if (file_mode != EditorExportPreset::MODE_FILE_REMOVE) { + p_paths.insert(path); + } + } +} + +void EditorExportPlatformUtils::export_find_dependencies(const String &p_path, HashSet &p_paths) { + if (p_paths.has(p_path)) { + return; + } + + p_paths.insert(p_path); + + EditorFileSystemDirectory *dir; + int file_idx; + dir = EditorFileSystem::get_singleton()->find_file(p_path, &file_idx); + if (!dir) { + return; + } + + PackedStringArray deps = dir->get_file_deps(file_idx); + + for (const String &dep : deps) { + EditorExportPlatformUtils::export_find_dependencies(dep, p_paths); + } +} + +void EditorExportPlatformUtils::export_find_preset_resources(const Ref &p_preset, HashSet &p_paths) { + EditorExportPreset::ExportFilter export_filter = p_preset->get_export_filter(); + switch (export_filter) { + case EditorExportPreset::EXPORT_ALL_RESOURCES: + case EditorExportPreset::EXCLUDE_SELECTED_RESOURCES: { + EditorExportPlatformUtils::export_find_resources(EditorFileSystem::get_singleton()->get_filesystem(), p_paths); + + if (export_filter == EditorExportPreset::EXCLUDE_SELECTED_RESOURCES) { + PackedStringArray excluded_resources = p_preset->get_files_to_export(); + for (const String &excluded_resource : excluded_resources) { + p_paths.erase(excluded_resource); + } + } + } break; + + case EditorExportPreset::EXPORT_CUSTOMIZED: { + EditorExportPlatformUtils::export_find_customized_resources(p_preset, EditorFileSystem::get_singleton()->get_filesystem(), p_preset->get_file_export_mode("res://"), p_paths); + } break; + + case EditorExportPreset::EXPORT_SELECTED_SCENES: + case EditorExportPreset::EXPORT_SELECTED_RESOURCES: { + bool scenes_only = export_filter == EditorExportPreset::EXPORT_SELECTED_SCENES; + + PackedStringArray files = p_preset->get_files_to_export(); + for (const String &file : files) { + if (scenes_only && ResourceLoader::get_resource_type(file) != "PackedScene") { + continue; + } + + EditorExportPlatformUtils::export_find_dependencies(file, p_paths); + } + + // Add autoload resources and their dependencies + List props; + ProjectSettings::get_singleton()->get_property_list(&props); + + for (const PropertyInfo &pi : props) { + if (!pi.name.begins_with("autoload/")) { + continue; + } + + String autoload_path = EditorExportPlatformUtils::get_project_setting(p_preset, pi.name); + + if (autoload_path.begins_with("*")) { + autoload_path = autoload_path.substr(1); + } + + EditorExportPlatformUtils::export_find_dependencies(autoload_path, p_paths); + } + } break; + } + + //add native icons to non-resource include list + EditorExportPlatformUtils::edit_filter_list(p_paths, String("*.icns"), false); + EditorExportPlatformUtils::edit_filter_list(p_paths, String("*.ico"), false); + + EditorExportPlatformUtils::edit_filter_list(p_paths, p_preset->get_include_filter(), false); + EditorExportPlatformUtils::edit_filter_list(p_paths, p_preset->get_exclude_filter(), true); + + // Ignore import files, since these are automatically added to the jar later with the resources + EditorExportPlatformUtils::edit_filter_list(p_paths, String("*.import"), true); +} + +void EditorExportPlatformUtils::edit_files_with_filter(Ref &da, const Vector &p_filters, HashSet &r_list, bool exclude) { + da->list_dir_begin(); + String cur_dir = da->get_current_dir().replace_char('\\', '/'); + if (!cur_dir.ends_with("/")) { + cur_dir += "/"; + } + String cur_dir_no_prefix = cur_dir.replace("res://", ""); + + Vector dirs; + String f = da->get_next(); + while (!f.is_empty()) { + if (da->current_is_dir()) { + dirs.push_back(f); + } else { + String fullpath = cur_dir + f; + // Test also against path without res:// so that filters like `file.txt` can work. + String fullpath_no_prefix = cur_dir_no_prefix + f; + for (const String &filter : p_filters) { + if (fullpath.matchn(filter) || fullpath_no_prefix.matchn(filter)) { + if (!exclude) { + r_list.insert(fullpath); + } else { + r_list.erase(fullpath); + } + } + } + } + f = da->get_next(); + } + + da->list_dir_end(); + + for (const String &dir : dirs) { + if (dir.begins_with(".")) { + continue; + } + + if (EditorFileSystem::_should_skip_directory(cur_dir + dir)) { + continue; + } + + da->change_dir(dir); + EditorExportPlatformUtils::edit_files_with_filter(da, p_filters, r_list, exclude); + da->change_dir(".."); + } +} + +void EditorExportPlatformUtils::edit_filter_list(HashSet &r_list, const String &p_filter, bool exclude) { + if (p_filter.is_empty()) { + return; + } + Vector split = p_filter.split(","); + Vector filters; + for (int i = 0; i < split.size(); i++) { + String f = split[i].strip_edges(); + if (f.is_empty()) { + continue; + } + filters.push_back(f); + } + + Ref da = DirAccess::create(DirAccess::ACCESS_RESOURCES); + ERR_FAIL_COND(da.is_null()); + EditorExportPlatformUtils::edit_files_with_filter(da, filters, r_list, exclude); +} + +Vector EditorExportPlatformUtils::filter_extension_list_config_file(const String &p_config_path, const HashSet &p_paths) { + Ref f = FileAccess::open(p_config_path, FileAccess::READ); + if (f.is_null()) { + ERR_FAIL_V_MSG(Vector(), "Can't open file from path '" + String(p_config_path) + "'."); + } + Vector data; + while (!f->eof_reached()) { + String l = f->get_line().strip_edges(); + if (p_paths.has(l)) { + data.append_array(l.to_utf8_buffer()); + data.append('\n'); + } + } + return data; +} + +Vector EditorExportPlatformUtils::get_forced_export_files(const Ref &p_preset) { + Vector files; + + files.push_back(ProjectSettings::get_singleton()->get_global_class_list_path()); + + String icon = ResourceUID::ensure_path(get_project_setting(p_preset, "application/config/icon")); + String splash = ResourceUID::ensure_path(get_project_setting(p_preset, "application/boot_splash/image")); + if (!icon.is_empty() && FileAccess::exists(icon)) { + files.push_back(icon); + } + if (!splash.is_empty() && FileAccess::exists(splash) && icon != splash) { + files.push_back(splash); + } + String resource_cache_file = ResourceUID::get_cache_file(); + if (FileAccess::exists(resource_cache_file)) { + files.push_back(resource_cache_file); + } + + String extension_list_config_file = GDExtension::get_extension_list_config_file(); + if (FileAccess::exists(extension_list_config_file)) { + files.push_back(extension_list_config_file); + } + + return files; +} + +HashMap EditorExportPlatformUtils::get_internal_export_files(const Ref &p_editor_export_platform, const Ref &p_preset, bool p_debug) { + HashMap files; + + // Text server support data. + if (TS->has_feature(TextServer::FEATURE_USE_SUPPORT_DATA)) { + bool include_data = (bool)get_project_setting(p_preset, "internationalization/locale/include_text_server_data"); + if (!include_data) { + Vector translations = get_project_setting(p_preset, "internationalization/locale/translations"); + translations.push_back(get_project_setting(p_preset, "internationalization/locale/fallback")); + for (const String &t : translations) { + if (TS->is_locale_using_support_data(t)) { + include_data = true; + break; + } + } + } + if (include_data) { + String ts_name = TS->get_support_data_filename(); + String ts_target = "res://" + ts_name; + if (!ts_name.is_empty()) { + bool export_ok = false; + if (FileAccess::exists(ts_target)) { // Include user supplied data file. + const PackedByteArray &ts_data = FileAccess::get_file_as_bytes(ts_target); + if (!ts_data.is_empty()) { + p_editor_export_platform->add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Export"), TTR("Using user provided text server data, text display in the exported project might be broken if export template was built with different ICU version!")); + files[ts_target] = ts_data; + export_ok = true; + } + } else { + String current_version = GODOT_VERSION_FULL_CONFIG; + String template_path = EditorPaths::get_singleton()->get_export_templates_dir().path_join(current_version); + if (p_debug && p_preset->has("custom_template/debug") && p_preset->get("custom_template/debug") != "") { + template_path = p_preset->get("custom_template/debug").operator String().get_base_dir(); + } else if (!p_debug && p_preset->has("custom_template/release") && p_preset->get("custom_template/release") != "") { + template_path = p_preset->get("custom_template/release").operator String().get_base_dir(); + } + String data_file_name = template_path.path_join(ts_name); + if (FileAccess::exists(data_file_name)) { + const PackedByteArray &ts_data = FileAccess::get_file_as_bytes(data_file_name); + if (!ts_data.is_empty()) { + print_line("Using text server data from export templates."); + files[ts_target] = ts_data; + export_ok = true; + } + } else { + const PackedByteArray &ts_data = TS->get_support_data(); + if (!ts_data.is_empty()) { + p_editor_export_platform->add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Export"), TTR("Using editor embedded text server data, text display in the exported project might be broken if export template was built with different ICU version!")); + files[ts_target] = ts_data; + export_ok = true; + } + } + } + if (!export_ok) { + p_editor_export_platform->add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Export"), TTR("Missing text server data, text display in the exported project might be broken!")); + } + } + } + } + + return files; +} diff --git a/editor/export/editor_export_platform_utils.h b/editor/export/editor_export_platform_utils.h new file mode 100644 index 000000000000..44eaa1dd24cb --- /dev/null +++ b/editor/export/editor_export_platform_utils.h @@ -0,0 +1,101 @@ +/**************************************************************************/ +/* editor_export_platform_utils.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "core/io/dir_access.h" +#include "editor/export/editor_export_platform_data.h" +#include "editor/export/editor_export_preset.h" + +class EditorExportPlatformUtils { +public: + struct AsyncPckFileDependenciesState { + private: + void _get_file_dependencies_of(const String &p_file, HashMap *> &p_dependencies); + + public: + List> file_dependencies_lists; + HashMap *> file_dependencies; + + void add_to_file_dependencies(const String &p_file); + void add_to_file_dependencies(const HashSet &p_file_set); + HashMap *> get_file_dependencies_of(const HashSet &p_file_set); + HashMap *> get_file_dependencies_of(const String &p_file); + + void clear() { file_dependencies.clear(); } + }; + + // Get the + static String get_path_from_dependency(const String &p_dependency); + + static int get_pad(int p_alignment, int p_n); + + static Variant get_project_setting(const Ref &p_preset, const StringName &p_name); + + static bool encrypt_and_store_directory(Ref p_fd, EditorExportPlatformData::PackData &p_pack_data, const Vector &p_key, uint64_t p_seed, uint64_t p_file_base); + static Error encrypt_and_store_data(Ref p_fd, const String &p_path, const Vector &p_data, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool &r_encrypt); + + static Error store_temp_file(const String &p_simplified_path, const PackedByteArray &p_data, const PackedStringArray &p_encoded_included_filters, const PackedStringArray &p_encoded_excluded_filters, const Vector &p_key, uint64_t p_seed, bool p_delta, PackedByteArray &r_encoded_data, EditorExportPlatformData::SavedData &r_saved_data); + + // Utility method used to create a directory. + static Error create_directory(const String &p_dir); + + // Writes p_data into a file at p_path, creating directories if necessary. + // Note: this will overwrite the file at p_path if it already exists. + static Error store_file_at_path(const String &p_path, const PackedByteArray &p_data); + + // Writes string p_data into a file at p_path, creating directories if necessary. + // Note: this will overwrite the file at p_path if it already exists. + static Error store_string_at_path(const String &p_path, const String &p_data); + + // Converts script encryption key to bytes. + static PackedByteArray convert_string_encryption_key_to_bytes(const String &p_encryption_key); + + // Finds resource files from a file system directory. + static void export_find_resources(EditorFileSystemDirectory *p_dir, HashSet &p_paths); + + // + static void export_find_customized_resources(const Ref &p_preset, EditorFileSystemDirectory *p_dir, EditorExportPreset::FileExportMode p_mode, HashSet &p_paths); + + // + static void export_find_dependencies(const String &p_path, HashSet &p_paths); + + static void export_find_preset_resources(const Ref &p_preset, HashSet &p_paths); + + static void edit_files_with_filter(Ref &da, const Vector &p_filters, HashSet &r_list, bool exclude); + + static void edit_filter_list(HashSet &r_list, const String &p_filter, bool exclude); + + static Vector filter_extension_list_config_file(const String &p_config_path, const HashSet &p_paths); + + static Vector get_forced_export_files(const Ref &p_preset); + + static HashMap get_internal_export_files(const Ref &p_editor_export_platform, const Ref &p_preset, bool p_debug); +}; diff --git a/editor/export/gdextension_export_plugin.h b/editor/export/gdextension_export_plugin.h index 56fc389a3633..2ca4a584e1b1 100644 --- a/editor/export/gdextension_export_plugin.h +++ b/editor/export/gdextension_export_plugin.h @@ -161,11 +161,11 @@ void GDExtensionExportPlugin::_export_file(const String &p_path, const String &p for (const KeyValue &E : libs_found) { if (E.value.count == 0) { if (get_export_platform().is_valid()) { - get_export_platform()->add_message(EditorExportPlatform::EXPORT_MESSAGE_WARNING, TTR("GDExtension"), vformat(TTR("No \"%s\" library found for GDExtension: \"%s\". Possible feature flags for your platform: %s"), E.key, p_path, String(", ").join(features_vector))); + get_export_platform()->add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("GDExtension"), vformat(TTR("No \"%s\" library found for GDExtension: \"%s\". Possible feature flags for your platform: %s"), E.key, p_path, String(", ").join(features_vector))); } } else if (E.value.count > 1) { if (get_export_platform().is_valid()) { - get_export_platform()->add_message(EditorExportPlatform::EXPORT_MESSAGE_WARNING, TTR("GDExtension"), vformat(TTR("Multiple \"%s\" libraries found for GDExtension: \"%s\": \"%s\"."), E.key, p_path, String(", ").join(E.value.libs))); + get_export_platform()->add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("GDExtension"), vformat(TTR("Multiple \"%s\" libraries found for GDExtension: \"%s\": \"%s\"."), E.key, p_path, String(", ").join(E.value.libs))); } } } diff --git a/editor/export/project_export.cpp b/editor/export/project_export.cpp index 8b248ba58b8c..26467415832c 100644 --- a/editor/export/project_export.cpp +++ b/editor/export/project_export.cpp @@ -498,6 +498,16 @@ void ProjectExportDialog::_tab_changed(int) { _update_feature_list(); } +void ProjectExportDialog::_on_result_dialog_custom_action(const StringName &p_action) { + if (p_action == SNAME("copy_to_clipboard")) { + const String log_text = result_dialog_log->get_parsed_text(); + DisplayServer::get_singleton()->clipboard_set(log_text); + return; + } + + ERR_FAIL(); +} + void ProjectExportDialog::_update_parameters(const String &p_edited_property) { _update_current_preset(); } @@ -2020,8 +2030,12 @@ ProjectExportDialog::ProjectExportDialog() { result_dialog->set_title(TTR("Project Export")); result_dialog_log = memnew(RichTextLabel); result_dialog_log->set_custom_minimum_size(Size2(300, 80) * EDSCALE); + result_dialog_log->set_selection_enabled(true); result_dialog->add_child(result_dialog_log); + result_dialog->add_button(TTRC("Copy to clipboard"), false, "copy_to_clipboard"); + result_dialog->connect("custom_action", callable_mp(this, &ProjectExportDialog::_on_result_dialog_custom_action)); + main_vb->add_child(result_dialog); result_dialog->hide(); diff --git a/editor/export/project_export.h b/editor/export/project_export.h index 3fbc16ed728e..2df79a058ea8 100644 --- a/editor/export/project_export.h +++ b/editor/export/project_export.h @@ -224,6 +224,8 @@ class ProjectExportDialog : public ConfirmationDialog { void _tab_changed(int); + void _on_result_dialog_custom_action(const StringName &p_action); + protected: void _notification(int p_what); static void _bind_methods(); diff --git a/editor/icons/AsyncPCKInstaller.svg b/editor/icons/AsyncPCKInstaller.svg new file mode 100644 index 000000000000..4c1f79971604 --- /dev/null +++ b/editor/icons/AsyncPCKInstaller.svg @@ -0,0 +1 @@ + diff --git a/editor/run/editor_run_native.cpp b/editor/run/editor_run_native.cpp index f4311cf3e9a9..9a7200bbe029 100644 --- a/editor/run/editor_run_native.cpp +++ b/editor/run/editor_run_native.cpp @@ -148,23 +148,23 @@ Error EditorRunNative::start_run_native(int p_id) { bool debug_navigation = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_debug_navigation", false); if (deploy_debug_remote) { - flags.set_flag(EditorExportPlatform::DEBUG_FLAG_REMOTE_DEBUG); + flags.set_flag(EditorExportPlatformData::DEBUG_FLAG_REMOTE_DEBUG); } if (deploy_dumb) { - flags.set_flag(EditorExportPlatform::DEBUG_FLAG_DUMB_CLIENT); + flags.set_flag(EditorExportPlatformData::DEBUG_FLAG_DUMB_CLIENT); } if (debug_collisions) { - flags.set_flag(EditorExportPlatform::DEBUG_FLAG_VIEW_COLLISIONS); + flags.set_flag(EditorExportPlatformData::DEBUG_FLAG_VIEW_COLLISIONS); } if (debug_navigation) { - flags.set_flag(EditorExportPlatform::DEBUG_FLAG_VIEW_NAVIGATION); + flags.set_flag(EditorExportPlatformData::DEBUG_FLAG_VIEW_NAVIGATION); } eep->clear_messages(); Error err = eep->run(preset, idx, flags); result_dialog_log->clear(); if (eep->fill_log_messages(result_dialog_log, err)) { - if (eep->get_worst_message_type() >= EditorExportPlatform::EXPORT_MESSAGE_ERROR) { + if (eep->get_worst_message_type() >= EditorExportPlatformData::EXPORT_MESSAGE_ERROR) { result_dialog->popup_centered_ratio(0.5); } } diff --git a/misc/scripts/__init__.py b/misc/scripts/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/misc/scripts/copyright_headers.py b/misc/scripts/copyright_headers.py index 3d53c708ac5b..39182985bcf5 100755 --- a/misc/scripts/copyright_headers.py +++ b/misc/scripts/copyright_headers.py @@ -1,9 +1,15 @@ #!/usr/bin/env python3 +import io import os import sys +import typing -header = """\ +if typing.TYPE_CHECKING: + pass + + +HEADER = """\ /**************************************************************************/ /* $filename */ /**************************************************************************/ @@ -35,31 +41,44 @@ /**************************************************************************/ """ -if len(sys.argv) < 2: - print("Invalid usage of copyright_headers.py, it should be called with a path to one or multiple files.") - sys.exit(1) - -for f in sys.argv[1:]: - fname = f - - # Handle replacing $filename with actual filename and keep alignment - fsingle = os.path.basename(fname.strip()) - rep_fl = "$filename" - rep_fi = fsingle - len_fl = len(rep_fl) - len_fi = len(rep_fi) - # Pad with spaces to keep alignment - if len_fi < len_fl: - for x in range(len_fl - len_fi): - rep_fi += " " - elif len_fl < len_fi: - for x in range(len_fi - len_fl): - rep_fl += " " - if header.find(rep_fl) != -1: - text = header.replace(rep_fl, rep_fi) + +def process_file(filename): # type: (str) -> None + filename = filename.strip() + basename = os.path.basename(filename) + + new_buffer = None # type: typing.Union[io.StringIO, None] + with open(filename, "r", encoding="utf-8") as file: + new_buffer = process_file_buffer(basename, file) + with open(filename, "w", encoding="utf-8") as file: + CHUNK_SIZE = 1024 + while True: + chunk = new_buffer.read(CHUNK_SIZE) + if not chunk: + break + file.write(chunk) + + +def process_file_buffer(filename, text_buffer): # type: (str, io.TextIOWrapper) -> io.StringIO + output = io.StringIO() + + # Handle replacing $filename with actual filename and keep alignment. + token = "$filename" + padded_filename = filename + padded_token = token + filename_length = len(filename) + token_length = len(token) + + # Pad with spaces to keep alignment. + if filename_length < token_length: + padded_filename += " " * (token_length - filename_length) + elif token_length < filename_length: + padded_token += " " * (filename_length - token_length) + + if HEADER.find(token) != -1: + output.write(HEADER.replace(padded_token, padded_filename)) else: - text = header.replace("$filename", fsingle) - text += "\n" + output.write(HEADER.replace(token, filename)) + output.write("\n") # We now have the proper header, so we want to ignore the one in the original file # and potentially empty lines and badly formatted lines, while keeping comments that @@ -68,28 +87,35 @@ # In a second pass, we skip all consecutive comment lines starting with "/*", # then we can append the rest (step 2). - with open(fname.strip(), "r", encoding="utf-8") as fileread: - line = fileread.readline() - header_done = False + line = text_buffer.readline() + header_done = False - while line.strip() == "" and line != "": # Skip empty lines at the top - line = fileread.readline() + while line.strip() == "" and line != "": # Skip empty lines at the top + line = text_buffer.readline() - if line.find("/**********") == -1: # Godot header starts this way - # Maybe starting with a non-Godot comment, abort header magic + if line.find("/**********") == -1: # Godot header starts this way + # Maybe starting with a non-Godot comment, abort header magic + header_done = True + + while not header_done: # Handle header now + if line.find("/*") != 0: # No more starting with a comment header_done = True + if line.strip() != "": + output.write(line) + line = text_buffer.readline() + + while line != "": # Dump everything until EOF + output.write(line) + line = text_buffer.readline() + + output.seek(0) + return output - while not header_done: # Handle header now - if line.find("/*") != 0: # No more starting with a comment - header_done = True - if line.strip() != "": - text += line - line = fileread.readline() - while line != "": # Dump everything until EOF - text += line - line = fileread.readline() +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Invalid usage of copyright_headers.py, it should be called with a path to one or multiple files.") + sys.exit(1) - # Write - with open(fname.strip(), "w", encoding="utf-8", newline="\n") as filewrite: - filewrite.write(text) + for f in sys.argv[1:]: + process_file(f) diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index c75a70d3beaa..5f47d87c39a7 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -3097,22 +3097,43 @@ String ResourceFormatLoaderGDScript::get_resource_type(const String &p_path) con return ""; } -void ResourceFormatLoaderGDScript::get_dependencies(const String &p_path, List *p_dependencies, bool p_add_types) { +void ResourceFormatLoaderGDScript::get_dependencies(const String &p_path, List *r_dependencies, bool p_add_types) { Ref file = FileAccess::open(p_path, FileAccess::READ); ERR_FAIL_COND_MSG(file.is_null(), "Cannot open file '" + p_path + "'."); - String source = file->get_as_utf8_string(); - if (source.is_empty()) { + Error err; + GDScriptParser parser; + + if (p_path.ends_with(".gd")) { + String source = file->get_as_utf8_string(); + if (source.is_empty()) { + return; + } + err = parser.parse(source, p_path, false); + } else { + // Path ends with ".gdc". + PackedByteArray source; + uint64_t source_size = FileAccess::get_size(p_path); + source.resize(source_size); + uint64_t actual_size = file->get_buffer(source.ptrw(), FileAccess::get_size(p_path)); + if (source_size != actual_size) { + source.resize(actual_size); + } + + err = parser.parse_binary(source, p_path); + } + + if (err != OK) { return; } - GDScriptParser parser; - if (OK != parser.parse(source, p_path, false)) { + GDScriptAnalyzer analyzer(&parser); + if (OK != analyzer.analyze()) { return; } - for (const String &E : parser.get_dependencies()) { - p_dependencies->push_back(E); + for (const String &dependency : parser.get_dependencies()) { + r_dependencies->push_back(dependency); } } diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index 3af83ba8ad17..25ea2bc5b3ae 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -681,7 +681,7 @@ class ResourceFormatLoaderGDScript : public ResourceFormatLoader { virtual void get_recognized_extensions(List *p_extensions) const override; virtual bool handles_type(const String &p_type) const override; virtual String get_resource_type(const String &p_path) const override; - virtual void get_dependencies(const String &p_path, List *p_dependencies, bool p_add_types = false) override; + virtual void get_dependencies(const String &p_path, List *r_dependencies, bool p_add_types = false) override; virtual void get_classes_used(const String &p_path, HashSet *r_classes) override; }; diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 28d4dd5d1163..febc138fbb33 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -618,6 +618,11 @@ Error GDScriptAnalyzer::resolve_class_inheritance(GDScriptParser::ClassNode *p_c class_type.native_type = result.native_type; p_class->set_datatype(class_type); + // Add base class to the list of dependencies. + if (result.kind == GDScriptParser::DataType::CLASS) { + parser->add_dependency(result.script_path); + } + // Apply annotations. for (GDScriptParser::AnnotationNode *&E : p_class->annotations) { resolve_annotation(E); @@ -954,6 +959,11 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type } p_type->set_datatype(result); + + if (result.kind == GDScriptParser::DataType::CLASS || result.kind == GDScriptParser::DataType::SCRIPT) { + parser->add_dependency(result.script_path); + } + return result; } @@ -4562,6 +4572,7 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident if (ScriptServer::is_global_class(name)) { p_identifier->set_datatype(make_global_class_meta_type(name, p_identifier)); + parser->add_dependency(p_identifier->get_datatype().script_path); return; } @@ -4604,6 +4615,7 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident } result.is_constant = true; p_identifier->set_datatype(result); + parser->add_dependency(autoload.path); return; } } @@ -4723,7 +4735,6 @@ void GDScriptAnalyzer::reduce_preload(GDScriptParser::PreloadNode *p_preload) { push_error("Preloaded path must be a constant string.", p_preload->path); } else { p_preload->resolved_path = p_preload->path->reduced_value; - // TODO: Save this as script dependency. if (p_preload->resolved_path.is_relative_path()) { p_preload->resolved_path = parser->script_path.get_base_dir().path_join(p_preload->resolved_path); } @@ -4759,6 +4770,8 @@ void GDScriptAnalyzer::reduce_preload(GDScriptParser::PreloadNode *p_preload) { push_error(vformat(R"(Could not preload resource file "%s".)", p_preload->resolved_path), p_preload->path); } } + + parser->add_dependency(p_preload->resolved_path); } } diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 2427481a151e..f40f074d0925 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -1457,6 +1457,8 @@ class GDScriptParser { void reset_extents(Node *p_node, GDScriptTokenizer::Token p_token); void reset_extents(Node *p_node, Node *p_from); + HashSet dependencies; + template T *alloc_node() { T *node = memnew(T); @@ -1631,9 +1633,11 @@ class GDScriptParser { bool annotation_exists(const String &p_annotation_name) const; const List &get_errors() const { return errors; } - const List get_dependencies() const { - // TODO: Keep track of deps. - return List(); + const HashSet &get_dependencies() const { + return dependencies; + } + void add_dependency(const String &p_dependency) { + dependencies.insert(p_dependency); } #ifdef DEBUG_ENABLED diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 050bc113d372..c1395271fe19 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -44,6 +44,7 @@ #include "editor/editor_log.h" #include "editor/editor_node.h" #include "editor/editor_string_names.h" +#include "editor/export/editor_export_platform_utils.h" #include "editor/export/export_template_manager.h" #include "editor/file_system/editor_paths.h" #include "editor/import/resource_importer_texture_settings.h" @@ -809,8 +810,8 @@ Error EditorExportPlatformAndroid::save_apk_file(const Ref & const String simplified_path = simplify_path(p_path); Vector enc_data; - EditorExportPlatform::SavedData sd; - Error err = _store_temp_file(simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, p_delta, enc_data, sd); + EditorExportPlatformData::SavedData sd; + Error err = EditorExportPlatformUtils::store_temp_file(simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, p_delta, enc_data, sd); if (err != OK) { return err; } @@ -850,7 +851,7 @@ Error EditorExportPlatformAndroid::copy_gradle_so(const Ref String dst_path = export_data->libs_directory.path_join(type).path_join(abi).path_join(filename); Vector data = FileAccess::get_file_as_bytes(p_so.path); print_verbose("Copying .so file from " + p_so.path + " to " + dst_path); - Error err = store_file_at_path(dst_path, data); + Error err = EditorExportPlatformUtils::store_file_at_path(dst_path, data); ERR_FAIL_COND_V_MSG(err, err, "Failed to copy .so file from " + p_so.path + " to " + dst_path); export_data->libs.push_back(dst_path); } @@ -1048,7 +1049,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref &p_preset) const { @@ -1161,7 +1162,7 @@ void EditorExportPlatformAndroid::_fix_themes_xml(const Ref // Reconstruct the XML content from the modified lines. String xml_content = String("\n").join(new_lines); - store_string_at_path(themes_xml_path, xml_content); + EditorExportPlatformUtils::store_string_at_path(themes_xml_path, xml_content); print_verbose("Successfully modified " + themes_xml_path + ": " + "\n" + xml_content); } @@ -1932,7 +1933,7 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref data; _process_launcher_icons(LAUNCHER_ICONS[i].export_path, p_main_image, LAUNCHER_ICONS[i].dimensions, data); - store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ICONS[i].export_path), data); + EditorExportPlatformUtils::store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ICONS[i].export_path), data); } if (p_foreground.is_valid() && !p_foreground->is_empty()) { @@ -1940,7 +1941,7 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref data; _process_launcher_icons(LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].export_path, p_foreground, LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].dimensions, data); - store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].export_path), data); + EditorExportPlatformUtils::store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].export_path), data); } if (p_background.is_valid() && !p_background->is_empty()) { @@ -1948,7 +1949,7 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref data; _process_launcher_icons(LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].export_path, p_background, LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].dimensions, data); - store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].export_path), data); + EditorExportPlatformUtils::store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].export_path), data); } if (p_monochrome.is_valid() && !p_monochrome->is_empty()) { @@ -1956,13 +1957,13 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref data; _process_launcher_icons(LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].export_path, p_monochrome, LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].dimensions, data); - store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].export_path), data); + EditorExportPlatformUtils::store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].export_path), data); monochrome_tag = " \n"; } } // Finalize the icon.xml by formatting the template with the optional monochrome tag. - store_string_at_path(gradle_build_dir.path_join(ICON_XML_PATH), vformat(ICON_XML_TEMPLATE, monochrome_tag)); + EditorExportPlatformUtils::store_string_at_path(gradle_build_dir.path_join(ICON_XML_PATH), vformat(ICON_XML_TEMPLATE, monochrome_tag)); } Vector EditorExportPlatformAndroid::get_enabled_abis(const Ref &p_preset) { @@ -2339,7 +2340,7 @@ Error EditorExportPlatformAndroid::run(const Ref &p_preset, String can_export_error; bool can_export_missing_templates; if (!can_export(p_preset, can_export_error, can_export_missing_templates)) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), can_export_error); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Run"), can_export_error); return ERR_UNCONFIGURED; } @@ -2355,11 +2356,11 @@ Error EditorExportPlatformAndroid::run(const Ref &p_preset, } const bool use_wifi_for_remote_debug = EDITOR_GET("export/android/use_wifi_for_remote_debug"); - const bool use_remote = p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT); + const bool use_remote = p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_DUMB_CLIENT); const bool use_reverse = !use_wifi_for_remote_debug; if (use_reverse) { - p_debug_flags.set_flag(DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST); + p_debug_flags.set_flag(EditorExportPlatformData::DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST); } String tmp_export_path = EditorPaths::get_singleton()->get_temp_dir().path_join("tmpexport." + uitos(OS::get_singleton()->get_unix_time()) + ".apk"); @@ -2430,7 +2431,7 @@ Error EditorExportPlatformAndroid::run(const Ref &p_preset, err = OS::get_singleton()->execute(adb, args, &output, &rv, true); print_verbose(output); if (err || rv != 0) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Could not install to device: %s"), output)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Could not install to device: %s"), output)); CLEANUP_AND_RETURN(ERR_CANT_CREATE); } @@ -2449,7 +2450,7 @@ Error EditorExportPlatformAndroid::run(const Ref &p_preset, OS::get_singleton()->execute(adb, args, &output, &rv, true); print_verbose(output); - if (p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG)) { + if (p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_REMOTE_DEBUG)) { int dbg_port = EDITOR_GET("network/debug/remote_port"); args.clear(); args.push_back("-s"); @@ -2464,7 +2465,7 @@ Error EditorExportPlatformAndroid::run(const Ref &p_preset, print_line("Reverse result: " + itos(rv)); } - if (p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) { + if (p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_DUMB_CLIENT)) { int fs_port = EDITOR_GET("filesystem/file_server/port"); args.clear(); @@ -2515,7 +2516,7 @@ Error EditorExportPlatformAndroid::run(const Ref &p_preset, Dictionary data = OS::get_singleton()->execute_with_pipe(scrcpy, args, false); if (!data.has("pid") || data["pid"].operator int() <= 0) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Could not start scrcpy executable. Configure scrcpy path in the Editor Settings (Export > Android > scrcpy > Path).")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Could not start scrcpy executable. Configure scrcpy path in the Editor Settings (Export > Android > scrcpy > Path).")); CLEANUP_AND_RETURN(ERR_CANT_CREATE); } bool connected = false; @@ -2545,7 +2546,7 @@ Error EditorExportPlatformAndroid::run(const Ref &p_preset, print_verbose(err_output); if (!connected) { OS::get_singleton()->kill(data["pid"].operator int()); - add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Could not execute on device, scrcpy failed with the following error:\n" + err_output)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Could not execute on device, scrcpy failed with the following error:\n" + err_output)); CLEANUP_AND_RETURN(ERR_CANT_CREATE); } } else { @@ -2582,7 +2583,7 @@ Error EditorExportPlatformAndroid::run(const Ref &p_preset, print_verbose(output); if (err || rv != 0 || output.begins_with("Error: Activity not started")) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Could not execute on device.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Could not execute on device.")); CLEANUP_AND_RETURN(ERR_CANT_CREATE); } } @@ -3225,7 +3226,7 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) { int export_format = int(p_preset->get("gradle_build/export_format")); if (export_format == EXPORT_FORMAT_AAB) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("AAB signing is not supported")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("AAB signing is not supported")); return FAILED; } @@ -3258,9 +3259,9 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref &p_pre if (!FileAccess::exists(keystore)) { if (p_debug) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not find debug keystore, unable to export.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not find debug keystore, unable to export.")); } else { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not find release keystore, unable to export.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not find release keystore, unable to export.")); } return ERR_FILE_CANT_OPEN; } @@ -3275,7 +3276,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref &p_pre #ifdef ANDROID_ENABLED err = OS_Android::get_singleton()->sign_apk(apk_path, apk_path, keystore, user, password); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to sign apk.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to sign apk.")); return err; } #else @@ -3287,11 +3288,11 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref &p_pre String apksigner = get_apksigner_path(target_sdk_version.to_int(), true); print_verbose("Starting signing of the APK binary using " + apksigner); if (apksigner == "") { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("All 'apksigner' tools located in Android SDK 'build-tools' directory failed to execute. Please check that you have the correct version installed for your target sdk version. The resulting APK is unsigned.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("All 'apksigner' tools located in Android SDK 'build-tools' directory failed to execute. Please check that you have the correct version installed for your target sdk version. The resulting APK is unsigned.")); return OK; } if (!FileAccess::exists(apksigner)) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting APK is unsigned.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting APK is unsigned.")); return OK; } @@ -3319,14 +3320,14 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref &p_pre int retval; err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true); if (err != OK) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable.")); return err; } // By design, apksigner does not output credentials in its output unless --verbose is used print_line(output); if (retval) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' returned with error #%d"), retval)); - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' returned with error #%d"), retval)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output)); return ERR_CANT_CREATE; } #endif @@ -3338,7 +3339,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref &p_pre #ifdef ANDROID_ENABLED err = OS_Android::get_singleton()->verify_apk(apk_path); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to verify signed apk.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to verify signed apk.")); return err; } #else @@ -3353,13 +3354,13 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref &p_pre output.clear(); err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true); if (err != OK) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable.")); return err; } print_verbose(output); if (retval) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' verification of APK failed.")); - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' verification of APK failed.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output)); return ERR_CANT_CREATE; } #endif @@ -3525,74 +3526,12 @@ Error EditorExportPlatformAndroid::export_project(const Ref return export_project_helper(p_preset, p_debug, p_path, export_format, should_sign, p_flags); } -Error EditorExportPlatformAndroid::_generate_sparse_pck_metadata(const Ref &p_preset, PackData &p_pack_data, Vector &r_data) { - Error err; - Ref ftmp = FileAccess::create_temp(FileAccess::WRITE_READ, "export_index", "tmp", false, &err); - if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not create temporary file!")); - return err; - } - int64_t pck_start_pos = ftmp->get_position(); - uint64_t file_base_ofs = 0; - uint64_t dir_base_ofs = 0; - EditorExportPlatform::_store_header(ftmp, p_preset->get_enc_pck() && p_preset->get_enc_directory(), true, file_base_ofs, dir_base_ofs); - - // Write directory. - uint64_t dir_offset = ftmp->get_position(); - ftmp->seek(dir_base_ofs); - ftmp->store_64(dir_offset - pck_start_pos); - ftmp->seek(dir_offset); - - Vector key; - if (p_preset->get_enc_pck() && p_preset->get_enc_directory()) { - String script_key = _get_script_encryption_key(p_preset); - key.resize(32); - if (script_key.length() == 64) { - for (int i = 0; i < 32; i++) { - int v = 0; - if (i * 2 < script_key.length()) { - char32_t ct = script_key[i * 2]; - if (is_digit(ct)) { - ct = ct - '0'; - } else if (ct >= 'a' && ct <= 'f') { - ct = 10 + ct - 'a'; - } - v |= ct << 4; - } - - if (i * 2 + 1 < script_key.length()) { - char32_t ct = script_key[i * 2 + 1]; - if (is_digit(ct)) { - ct = ct - '0'; - } else if (ct >= 'a' && ct <= 'f') { - ct = 10 + ct - 'a'; - } - v |= ct; - } - key.write[i] = v; - } - } - } - - if (!EditorExportPlatform::_encrypt_and_store_directory(ftmp, p_pack_data, key, p_preset->get_seed(), 0)) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Can't create encrypted file.")); - return ERR_CANT_CREATE; - } - - r_data.resize(ftmp->get_length()); - ftmp->seek(0); - ftmp->get_buffer(r_data.ptrw(), r_data.size()); - ftmp.unref(); - - return OK; -} - Error EditorExportPlatformAndroid::export_project_helper(const Ref &p_preset, bool p_debug, const String &p_path, int export_format, bool should_sign, BitField p_flags) { ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); const String base_dir = p_path.get_base_dir(); if (!DirAccess::exists(base_dir)) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir)); return ERR_FILE_BAD_PATH; } @@ -3603,7 +3542,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Refget("gradle_build/use_gradle_build")); String gradle_build_directory = use_gradle_build ? ExportTemplateManager::get_android_build_directory(p_preset) : ""; - bool p_give_internet = p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT) || p_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG); + bool p_give_internet = p_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_DUMB_CLIENT) || p_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_REMOTE_DEBUG); bool apk_expansion = p_preset->get("apk_expansion/enable"); Vector enabled_abis = get_enabled_abis(p_preset); @@ -3632,25 +3571,25 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref EXPORT_FORMAT_AAB || export_format < EXPORT_FORMAT_APK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unsupported export format!")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unsupported export format!")); return ERR_UNCONFIGURED; } String err_string; if (!has_valid_username_and_password(p_preset, err_string)) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR(err_string)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), TTR(err_string)); return ERR_UNCONFIGURED; } @@ -3662,14 +3601,14 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref f = FileAccess::open(gradle_base_directory.path_join(".build_version"), FileAccess::READ); if (f.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Trying to build from a gradle built template, but no version info for it exists. Please reinstall from the 'Project' menu.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Trying to build from a gradle built template, but no version info for it exists. Please reinstall from the 'Project' menu.")); return ERR_UNCONFIGURED; } String current_version = ExportTemplateManager::get_android_template_identifier(p_preset); String installed_version = f->get_line().strip_edges(); print_verbose("- build version: " + installed_version); if (installed_version != current_version) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR(MISMATCHED_VERSIONS_MESSAGE), installed_version, current_version)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR(MISMATCHED_VERSIONS_MESSAGE), installed_version, current_version)); return ERR_UNCONFIGURED; } } @@ -3677,14 +3616,14 @@ Error EditorExportPlatformAndroid::export_project_helper(const Refget("package/name")); err = _create_project_name_strings_files(p_preset, project_name, gradle_build_directory, get_project_setting(p_preset, "application/config/name_localized")); //project name localization. if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res/*.xml files with project name.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res/*.xml files with project name.")); } // Copies the project icon files into the appropriate Gradle project directory. _copy_icons_to_gradle_project(p_preset, main_image, foreground, background, monochrome); @@ -3713,7 +3652,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref enc_data; err = _generate_sparse_pck_metadata(p_preset, user_data.pd, enc_data); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not generate sparse pck metadata!")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not generate sparse pck metadata!")); return err; } - err = store_file_at_path(user_data.assets_directory + "/assets.sparsepck", enc_data); + err = EditorExportPlatformUtils::store_file_at_path(user_data.assets_directory + "/assets.sparsepck", enc_data); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not write PCK directory!")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not write PCK directory!")); return err; } } if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project.")); return err; } if (user_data.libs.size() > 0) { @@ -3745,13 +3684,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Refget_resource_dir().path_join(debug_keystore).simplify_path(); } if (!FileAccess::exists(debug_keystore)) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find debug keystore, unable to export.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find debug keystore, unable to export.")); return ERR_FILE_CANT_OPEN; } #ifdef ANDROID_ENABLED @@ -3920,7 +3859,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Refget_resource_dir().path_join(release_keystore).simplify_path(); } if (!FileAccess::exists(release_keystore)) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find release keystore, unable to export.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find release keystore, unable to export.")); return ERR_FILE_CANT_OPEN; } @@ -3969,7 +3908,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Refexecute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline, true, false, &build_project_output); if (result != 0) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error:") + "\n\n" + build_project_output); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error:") + "\n\n" + build_project_output); return ERR_CANT_CREATE; } else { print_verbose(build_project_output); @@ -3979,7 +3918,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Refexecute_and_show_output(TTR("Moving output"), build_command, copy_args, true, false, ©_binary_output); if (copy_result != 0) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to copy and rename export file:") + "\n\n" + copy_binary_output); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to copy and rename export file:") + "\n\n" + copy_binary_output); return ERR_CANT_CREATE; } else { print_verbose(copy_binary_output); @@ -4004,7 +3943,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref enc_data; err = _generate_sparse_pck_metadata(p_preset, ed.pd, enc_data); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not generate sparse pck metadata!")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not generate sparse pck metadata!")); return err; } @@ -4239,7 +4178,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref &p_preset) const; - Error _generate_sparse_pck_metadata(const Ref &p_preset, PackData &p_pack_data, Vector &r_data); - protected: void _notification(int p_what); diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index 420c36e3959d..cf71adf7e7a7 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -123,48 +123,6 @@ String _get_app_category_label(int category_index) { } } -// Utility method used to create a directory. -Error create_directory(const String &p_dir) { - if (!DirAccess::exists(p_dir)) { - Ref filesystem_da = DirAccess::create(DirAccess::ACCESS_RESOURCES); - ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); - Error err = filesystem_da->make_dir_recursive(p_dir); - ERR_FAIL_COND_V_MSG(err, ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); - } - return OK; -} - -// Writes p_data into a file at p_path, creating directories if necessary. -// Note: this will overwrite the file at p_path if it already exists. -Error store_file_at_path(const String &p_path, const Vector &p_data) { - String dir = p_path.get_base_dir(); - Error err = create_directory(dir); - if (err != OK) { - return err; - } - Ref fa = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); - fa->store_buffer(p_data.ptr(), p_data.size()); - return OK; -} - -// Writes string p_data into a file at p_path, creating directories if necessary. -// Note: this will overwrite the file at p_path if it already exists. -Error store_string_at_path(const String &p_path, const String &p_data) { - String dir = p_path.get_base_dir(); - Error err = create_directory(dir); - if (err != OK) { - if (OS::get_singleton()->is_stdout_verbose()) { - print_error("Unable to write data into " + p_path); - } - return err; - } - Ref fa = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); - fa->store_string(p_data); - return OK; -} - // Implementation of EditorExportSaveFunction. // This method will only be called as an input to export_project_files. // It is used by the export_project_files method to save all the asset files into the gradle project. @@ -176,15 +134,15 @@ Error rename_and_store_file_in_gradle_project(const Ref &p_p const String simplified_path = EditorExportPlatform::simplify_path(p_path); Vector enc_data; - EditorExportPlatform::SavedData sd; - Error err = _store_temp_file(simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, p_delta, enc_data, sd); + EditorExportPlatformData::SavedData sd; + Error err = EditorExportPlatformUtils::store_temp_file(simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, p_delta, enc_data, sd); if (err != OK) { return err; } const String dst_path = export_data->assets_directory + String("/") + simplified_path.trim_prefix("res://"); print_verbose("Saving project files from " + simplified_path + " into " + dst_path); - err = store_file_at_path(dst_path, enc_data); + err = EditorExportPlatformUtils::store_file_at_path(dst_path, enc_data); export_data->pd.file_ofs.push_back(sd); return err; @@ -210,7 +168,7 @@ Error _create_project_name_strings_files(const Ref &p_preset print_verbose("Creating strings resources for supported locales for project " + p_project_name); // Stores the string into the default values directory. String processed_default_xml_string = vformat(GODOT_PROJECT_NAME_XML_STRING, _android_xml_escape(p_project_name)); - store_string_at_path(p_gradle_build_dir.path_join("res/values/godot_project_name_string.xml"), processed_default_xml_string); + EditorExportPlatformUtils::store_string_at_path(p_gradle_build_dir.path_join("res/values/godot_project_name_string.xml"), processed_default_xml_string); // Searches the Gradle project res/ directory to find all supported locales Ref da = DirAccess::open(p_gradle_build_dir.path_join("res")); @@ -249,10 +207,10 @@ Error _create_project_name_strings_files(const Ref &p_preset if (locale_project_name != p_project_name) { String processed_xml_string = vformat(GODOT_PROJECT_NAME_XML_STRING, _android_xml_escape(locale_project_name)); print_verbose("Storing project name for locale " + locale + " under " + locale_directory); - store_string_at_path(locale_directory, processed_xml_string); + EditorExportPlatformUtils::store_string_at_path(locale_directory, processed_xml_string); } else { // TODO: Once the legacy build system is deprecated we don't need to have xml files for this else branch - store_string_at_path(locale_directory, processed_default_xml_string); + EditorExportPlatformUtils::store_string_at_path(locale_directory, processed_default_xml_string); } } da->list_dir_end(); @@ -397,35 +355,3 @@ String _get_application_tag(const Ref &p_export_platform, manifest_application_text += " \n"; return manifest_application_text; } - -Error _store_temp_file(const String &p_simplified_path, const Vector &p_data, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta, Vector &r_enc_data, EditorExportPlatform::SavedData &r_sd) { - Error err = OK; - Ref ftmp = FileAccess::create_temp(FileAccess::WRITE_READ, "export", "tmp", false, &err); - if (err != OK) { - return err; - } - r_sd.path_utf8 = p_simplified_path.trim_prefix("res://").utf8(); - r_sd.ofs = 0; - r_sd.size = p_data.size(); - r_sd.delta = p_delta; - err = EditorExportPlatform::_encrypt_and_store_data(ftmp, p_simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, r_sd.encrypted); - if (err != OK) { - return err; - } - - r_enc_data.resize(ftmp->get_length()); - ftmp->seek(0); - ftmp->get_buffer(r_enc_data.ptrw(), r_enc_data.size()); - ftmp.unref(); - - // Store MD5 of original file. - { - unsigned char hash[16]; - CryptoCore::md5(p_data.ptr(), p_data.size(), hash); - r_sd.md5.resize(16); - for (int i = 0; i < 16; i++) { - r_sd.md5.write[i] = hash[i]; - } - } - return OK; -} diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index c72168879276..8b6362958103 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -37,6 +37,7 @@ #include "core/os/os.h" #include "editor/export/editor_export.h" #include "editor/export/editor_export_platform.h" +#include "editor/export/editor_export_platform_utils.h" #include "servers/display/display_server.h" const String GODOT_PROJECT_NAME_XML_STRING = R"( @@ -65,7 +66,7 @@ static const int XR_MODE_REGULAR = 0; static const int XR_MODE_OPENXR = 1; struct CustomExportData { - EditorExportPlatform::PackData pd; + EditorExportPlatformData::PackData pd; String assets_directory; String libs_directory; bool debug; @@ -85,19 +86,6 @@ int _get_app_category_value(int category_index); String _get_app_category_label(int category_index); -Error _store_temp_file(const String &p_simplified_path, const Vector &p_data, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta, Vector &r_enc_data, EditorExportPlatform::SavedData &r_sd); - -// Utility method used to create a directory. -Error create_directory(const String &p_dir); - -// Writes p_data into a file at p_path, creating directories if necessary. -// Note: this will overwrite the file at p_path if it already exists. -Error store_file_at_path(const String &p_path, const Vector &p_data); - -// Writes string p_data into a file at p_path, creating directories if necessary. -// Note: this will overwrite the file at p_path if it already exists. -Error store_string_at_path(const String &p_path, const String &p_data); - // Implementation of EditorExportSaveFunction. // This method will only be called as an input to export_project_files. // It is used by the export_project_files method to save all the asset files into the gradle project. diff --git a/platform/ios/export/export_plugin.cpp b/platform/ios/export/export_plugin.cpp index 2af484eae886..7d911d374849 100644 --- a/platform/ios/export/export_plugin.cpp +++ b/platform/ios/export/export_plugin.cpp @@ -236,7 +236,7 @@ Error EditorExportPlatformIOS::_export_icons(const Ref &p_pr Ref da = DirAccess::open(p_iconset_dir); if (da.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat(TTR("Could not open a directory at path \"%s\"."), p_iconset_dir)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat(TTR("Could not open a directory at path \"%s\"."), p_iconset_dir)); return ERR_CANT_OPEN; } @@ -287,7 +287,7 @@ Error EditorExportPlatformIOS::_export_icons(const Ref &p_pr Error err = OK; Ref img = _load_icon_or_splash_image(icon_path, &err); if (err != OK || img.is_null() || img->is_empty()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Invalid icon (%s): '%s'.", info.preset_key, icon_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Invalid icon (%s): '%s'.", info.preset_key, icon_path)); return ERR_UNCONFIGURED; } else if (info.force_opaque && img->detect_alpha() != Image::ALPHA_NONE) { img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int())); @@ -300,7 +300,7 @@ Error EditorExportPlatformIOS::_export_icons(const Ref &p_pr err = img->save_png(p_iconset_dir + exp_name); } if (err) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Failed to export icon (%s): '%s'.", info.preset_key, icon_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Failed to export icon (%s): '%s'.", info.preset_key, icon_path)); return err; } } else { @@ -308,11 +308,11 @@ Error EditorExportPlatformIOS::_export_icons(const Ref &p_pr Error err = OK; Ref img = _load_icon_or_splash_image(icon_path, &err); if (err != OK || img.is_null() || img->is_empty()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Invalid icon (%s): '%s'.", info.preset_key, icon_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Invalid icon (%s): '%s'.", info.preset_key, icon_path)); return ERR_UNCONFIGURED; } else if (info.force_opaque && img->detect_alpha() != Image::ALPHA_NONE) { if (resize_waning) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Export Icons"), vformat("Icon (%s) must be opaque.", info.preset_key)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Export Icons"), vformat("Icon (%s) must be opaque.", info.preset_key)); } img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int())); Ref new_img = Image::create_empty(side_size, side_size, false, Image::FORMAT_RGBA8); @@ -321,7 +321,7 @@ Error EditorExportPlatformIOS::_export_icons(const Ref &p_pr err = new_img->save_png(p_iconset_dir + exp_name); } else if (img->get_width() != side_size || img->get_height() != side_size) { if (resize_waning) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Export Icons"), vformat("Icon (%s): '%s' has incorrect size %s and was automatically resized to %s.", info.preset_key, icon_path, img->get_size(), Vector2i(side_size, side_size))); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Export Icons"), vformat("Icon (%s): '%s' has incorrect size %s and was automatically resized to %s.", info.preset_key, icon_path, img->get_size(), Vector2i(side_size, side_size))); } img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int())); err = img->save_png(p_iconset_dir + exp_name); @@ -332,7 +332,7 @@ Error EditorExportPlatformIOS::_export_icons(const Ref &p_pr } if (err) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Failed to export icon (%s): '%s'.", info.preset_key, icon_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Failed to export icon (%s): '%s'.", info.preset_key, icon_path)); return err; } } @@ -367,7 +367,7 @@ Error EditorExportPlatformIOS::_export_icons(const Ref &p_pr Ref json_file = FileAccess::open(p_iconset_dir + "Contents.json", FileAccess::WRITE); if (json_file.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat(TTR("Could not write to a file at path \"%s\"."), p_iconset_dir + "Contents.json")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat(TTR("Could not write to a file at path \"%s\"."), p_iconset_dir + "Contents.json")); return ERR_CANT_CREATE; } @@ -376,7 +376,7 @@ Error EditorExportPlatformIOS::_export_icons(const Ref &p_pr Ref sizes_file = FileAccess::open(p_iconset_dir + "sizes", FileAccess::WRITE); if (sizes_file.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat(TTR("Could not write to a file at path \"%s\"."), p_iconset_dir + "sizes")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat(TTR("Could not write to a file at path \"%s\"."), p_iconset_dir + "sizes")); return ERR_CANT_CREATE; } diff --git a/platform/linuxbsd/export/export_plugin.cpp b/platform/linuxbsd/export/export_plugin.cpp index 6c470d9be4c2..d0f6f8c75cc6 100644 --- a/platform/linuxbsd/export/export_plugin.cpp +++ b/platform/linuxbsd/export/export_plugin.cpp @@ -46,7 +46,7 @@ Error EditorExportPlatformLinuxBSD::_export_debug_script(const Ref &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) { Ref f = FileAccess::open(p_path, FileAccess::WRITE); if (f.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), vformat(TTR("Could not open file \"%s\"."), p_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), vformat(TTR("Could not open file \"%s\"."), p_path)); return ERR_CANT_CREATE; } @@ -68,7 +68,7 @@ Error EditorExportPlatformLinuxBSD::export_project(const Ref if (!template_path.is_empty()) { String exe_arch = _get_exe_arch(template_path); if (arch != exe_arch) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Mismatching custom export template executable architecture: found \"%s\", expected \"%s\"."), exe_arch, arch)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Mismatching custom export template executable architecture: found \"%s\", expected \"%s\"."), exe_arch, arch)); return ERR_CANT_CREATE; } } @@ -96,7 +96,7 @@ Error EditorExportPlatformLinuxBSD::export_project(const Ref Ref tmp_app_dir = DirAccess::create_for_path(tmp_dir_path); if (export_as_zip) { if (tmp_app_dir.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not create and open the directory: \"%s\""), tmp_dir_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not create and open the directory: \"%s\""), tmp_dir_path)); return ERR_CANT_CREATE; } if (DirAccess::exists(tmp_dir_path)) { @@ -122,7 +122,7 @@ Error EditorExportPlatformLinuxBSD::export_project(const Ref err = _export_debug_script(p_preset, pkg_name, path.get_file(), scr_path); FileAccess::set_unix_permissions(scr_path, 0755); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Console Export"), TTR("Could not create console wrapper.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Debug Console Export"), TTR("Could not create console wrapper.")); } } @@ -297,7 +297,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int Ref f = FileAccess::open(p_path, FileAccess::READ_WRITE); if (f.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), vformat(TTR("Failed to open executable file \"%s\"."), p_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), vformat(TTR("Failed to open executable file \"%s\"."), p_path)); return ERR_CANT_OPEN; } @@ -305,7 +305,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int { uint32_t magic = f->get_32(); if (magic != 0x464c457f) { // 0x7F + "ELF" - add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Executable file header corrupted.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Executable file header corrupted.")); return ERR_FILE_CORRUPT; } } @@ -315,7 +315,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int int bits = f->get_8() * 32; if (bits == 32 && p_embedded_size >= 0x100000000) { - add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("32-bit executables cannot have embedded data >= 4 GiB.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("32-bit executables cannot have embedded data >= 4 GiB.")); } // Get info about the section header table. @@ -393,7 +393,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int memfree(strings); if (!found) { - add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Executable \"pck\" section not found.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Executable \"pck\" section not found.")); return ERR_FILE_CORRUPT; } return OK; @@ -530,7 +530,7 @@ Error EditorExportPlatformLinuxBSD::run(const Ref &p_preset, } } - const bool use_remote = p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT); + const bool use_remote = p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_DUMB_CLIENT); int dbg_port = EDITOR_GET("network/debug/remote_port"); print_line("Creating temporary directory..."); diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp index 56e8136787a0..a84e4eed7ef8 100644 --- a/platform/macos/export/export_plugin.cpp +++ b/platform/macos/export/export_plugin.cpp @@ -961,13 +961,13 @@ Error EditorExportPlatformMacOS::_export_liquid_glass_icon(const Refexecute(actool, args, &str, &exitcode, true); if (err != OK) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), TTR("Could not start 'actool' executable.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), TTR("Could not start 'actool' executable.")); return err; } PList info_plist; if (!info_plist.load_string(str, err_str)) { print_verbose(str); - add_message(EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), TTR("Could not read 'actool' version.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), TTR("Could not read 'actool' version.")); return err; } if (info_plist.get_root()->data_type == PList::PLNodeType::PL_NODE_TYPE_DICT && info_plist.get_root()->data_dict.has("com.apple.actool.version")) { @@ -975,7 +975,7 @@ Error EditorExportPlatformMacOS::_export_liquid_glass_icon(const Refdata_type == PList::PLNodeType::PL_NODE_TYPE_DICT && dict->data_dict.has("short-bundle-version")) { float version = String::utf8(dict->data_dict["short-bundle-version"]->data_string.get_data()).to_float(); if (version < 26.0) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), vformat(TTR("At least version 26.0 of 'actool' is required (version %f found)."), version)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), vformat(TTR("At least version 26.0 of 'actool' is required (version %f found)."), version)); return ERR_UNAVAILABLE; } } @@ -1010,7 +1010,7 @@ Error EditorExportPlatformMacOS::_export_liquid_glass_icon(const Refexecute(actool, args, &str, &exitcode, true); if (err != OK || str.contains("error:") || !FileAccess::exists(p_app_path + "/Contents/Resources/Assets.car") || !FileAccess::exists(plist)) { print_verbose(str); - add_message(EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), TTR("Could not export liquid glass icon:") + "\n" + str); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), TTR("Could not export liquid glass icon:") + "\n" + str); return err; } @@ -1032,7 +1032,7 @@ Error EditorExportPlatformMacOS::_notarize(const Ref &p_pres String rcodesign = EDITOR_GET("export/macos/rcodesign").operator String(); if (rcodesign.is_empty()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).")); return Error::FAILED; } @@ -1041,11 +1041,11 @@ Error EditorExportPlatformMacOS::_notarize(const Ref &p_pres args.push_back("notary-submit"); if (p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) == "") { - add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect issuer ID name not specified.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect issuer ID name not specified.")); return Error::FAILED; } if (p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY) == "") { - add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified.")); return Error::FAILED; } @@ -1067,25 +1067,25 @@ Error EditorExportPlatformMacOS::_notarize(const Ref &p_pres Error err = OS::get_singleton()->execute(rcodesign, args, &str, &exitcode, true); if (err != OK) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start rcodesign executable.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start rcodesign executable.")); return err; } int rq_offset = str.find("created submission ID:"); if (exitcode != 0 || rq_offset == -1) { print_line("rcodesign (" + p_path + "):\n" + str); - add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed, see editor log for details.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed, see editor log for details.")); return Error::FAILED; } else { print_verbose("rcodesign (" + p_path + "):\n" + str); int next_nl = str.find_char('\n', rq_offset); String request_uuid = (next_nl == -1) ? str.substr(rq_offset + 23) : str.substr(rq_offset + 23, next_nl - rq_offset - 23); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid)); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour.")); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check the progress manually by opening a Terminal and running the following command:")); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"rcodesign notary-log --api-issuer --api-key \""); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):")); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"rcodesign staple \""); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check the progress manually by opening a Terminal and running the following command:")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"rcodesign notary-log --api-issuer --api-key \""); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"rcodesign staple \""); } } break; #ifdef MACOS_ENABLED @@ -1093,7 +1093,7 @@ Error EditorExportPlatformMacOS::_notarize(const Ref &p_pres print_verbose("using notarytool notarization..."); if (!FileAccess::exists("/usr/bin/xcrun") && !FileAccess::exists("/bin/xcrun")) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Xcode command line tools are not installed.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Xcode command line tools are not installed.")); return Error::FAILED; } @@ -1105,17 +1105,17 @@ Error EditorExportPlatformMacOS::_notarize(const Ref &p_pres args.push_back(p_path); if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) == "" && p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) == "") { - add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Neither Apple ID name nor App Store Connect issuer ID name not specified.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Neither Apple ID name nor App Store Connect issuer ID name not specified.")); return Error::FAILED; } if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) != "" && p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) != "") { - add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time.")); return Error::FAILED; } if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) != "") { if (p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS) == "") { - add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Apple ID password not specified.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Apple ID password not specified.")); return Error::FAILED; } args.push_back("--apple-id"); @@ -1125,7 +1125,7 @@ Error EditorExportPlatformMacOS::_notarize(const Ref &p_pres args.push_back(p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS)); } else { if (p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID) == "") { - add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified.")); return Error::FAILED; } args.push_back("--issuer"); @@ -1151,27 +1151,27 @@ Error EditorExportPlatformMacOS::_notarize(const Ref &p_pres int exitcode = 0; Error err = OS::get_singleton()->execute("xcrun", args, &str, &exitcode, true); if (err != OK) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start xcrun executable.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start xcrun executable.")); return err; } int rq_offset = str.find("id:"); if (exitcode != 0 || rq_offset == -1) { print_line("notarytool (" + p_path + "):\n" + str); - add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed, see editor log for details.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed, see editor log for details.")); return Error::FAILED; } else { print_verbose("notarytool (" + p_path + "):\n" + str); int next_nl = str.find_char('\n', rq_offset); String request_uuid = (next_nl == -1) ? str.substr(rq_offset + 4) : str.substr(rq_offset + 4, next_nl - rq_offset - 4); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid)); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour.")); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("See instructions on finding your team ID: https://developer.apple.com/help/glossary/team-id")); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check the progress manually by opening a Terminal and running the following command:")); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun notarytool log --issuer --key-id --key \" or"); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun notarytool log --team-id --apple-id --password \""); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):")); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun stapler staple \""); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("See instructions on finding your team ID: https://developer.apple.com/help/glossary/team-id")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check the progress manually by opening a Terminal and running the following command:")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun notarytool log --issuer --key-id --key \" or"); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun notarytool log --team-id --apple-id --password \""); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun stapler staple \""); } } break; #endif @@ -1189,7 +1189,7 @@ void EditorExportPlatformMacOS::_code_sign(const Ref &p_pres String error_msg; Error err = CodeSign::codesign(false, true, p_path, p_ent_path, error_msg); if (err != OK) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Built-in CodeSign failed with error \"%s\"."), error_msg)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Built-in CodeSign failed with error \"%s\"."), error_msg)); return; } } break; @@ -1198,7 +1198,7 @@ void EditorExportPlatformMacOS::_code_sign(const Ref &p_pres String rcodesign = EDITOR_GET("export/macos/rcodesign").operator String(); if (rcodesign.is_empty()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xrcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xrcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).")); return; } @@ -1236,13 +1236,13 @@ void EditorExportPlatformMacOS::_code_sign(const Ref &p_pres Error err = OS::get_singleton()->execute(rcodesign, args, &str, &exitcode, true); if (err != OK) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start rcodesign executable.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start rcodesign executable.")); return; } if (exitcode != 0) { print_line("rcodesign (" + p_path + "):\n" + str); - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details.")); return; } else { print_verbose("rcodesign (" + p_path + "):\n" + str); @@ -1253,7 +1253,7 @@ void EditorExportPlatformMacOS::_code_sign(const Ref &p_pres print_verbose("using xcode codesign..."); if (!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xcode command line tools are not installed.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xcode command line tools are not installed.")); return; } @@ -1302,13 +1302,13 @@ void EditorExportPlatformMacOS::_code_sign(const Ref &p_pres Error err = OS::get_singleton()->execute("codesign", args, &str, &exitcode, true); if (err != OK) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start codesign executable, make sure Xcode command line tools are installed.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start codesign executable, make sure Xcode command line tools are installed.")); return; } if (exitcode != 0) { print_line("codesign (" + p_path + "):\n" + str); - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details.")); return; } else { print_verbose("codesign (" + p_path + "):\n" + str); @@ -1335,7 +1335,7 @@ void EditorExportPlatformMacOS::_code_sign_directory(const Ref dir_access{ DirAccess::open(p_path, &dir_access_error) }; if (dir_access_error != OK) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Cannot sign directory %s."), p_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Cannot sign directory %s."), p_path)); return; } @@ -1367,7 +1367,7 @@ void EditorExportPlatformMacOS::_code_sign_directory(const Refcurrent_is_dir()) { _code_sign_directory(p_preset, current_file_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code); } else if (p_should_error_on_non_code) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Cannot sign file %s."), current_file)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Cannot sign file %s."), current_file)); } current_file = dir_access->get_next(); @@ -1390,7 +1390,7 @@ Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref &dir_access Error err{ OK }; if (dir_access->dir_exists(p_src_path)) { #ifndef UNIX_ENABLED - add_message(EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Relative symlinks are not supported, exported \"%s\" might be broken!"), p_src_path.get_file())); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Relative symlinks are not supported, exported \"%s\" might be broken!"), p_src_path.get_file())); #endif print_verbose("export framework: " + p_src_path + " -> " + p_in_app_path); @@ -1414,7 +1414,7 @@ Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref &dir_access err = dir_access->copy_dir(p_src_path, p_in_app_path, -1, true); } if (err == OK && plist_missing) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Export"), vformat(TTR("\"%s\": Info.plist missing or invalid, new Info.plist generated."), p_src_path.get_file())); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Export"), vformat(TTR("\"%s\": Info.plist missing or invalid, new Info.plist generated."), p_src_path.get_file())); // Generate Info.plist String lib_name = p_src_path.get_basename().get_file(); String lib_id = p_preset->get("application/bundle_identifier"); @@ -1530,13 +1530,13 @@ Error EditorExportPlatformMacOS::_create_pkg(const Ref &p_pr String str; Error err = OS::get_singleton()->execute("xcrun", args, &str, nullptr, true); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("PKG Creation"), TTR("Could not start productbuild executable.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("PKG Creation"), TTR("Could not start productbuild executable.")); return err; } print_verbose("productbuild returned: " + str); if (str.contains("productbuild: error:")) { - add_message(EXPORT_MESSAGE_ERROR, TTR("PKG Creation"), TTR("`productbuild` failed.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("PKG Creation"), TTR("`productbuild` failed.")); return FAILED; } @@ -1562,16 +1562,16 @@ Error EditorExportPlatformMacOS::_create_dmg(const String &p_dmg_path, const Str String str; Error err = OS::get_singleton()->execute("hdiutil", args, &str, nullptr, true); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("Could not start hdiutil executable.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("Could not start hdiutil executable.")); return err; } print_verbose("hdiutil returned: " + str); if (str.contains("create failed")) { if (str.contains("File exists")) { - add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("`hdiutil create` failed - file exists.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("`hdiutil create` failed - file exists.")); } else { - add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("`hdiutil create` failed.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("`hdiutil create` failed.")); } return FAILED; } @@ -1593,7 +1593,7 @@ bool EditorExportPlatformMacOS::is_executable(const String &p_path) const { Error EditorExportPlatformMacOS::_export_debug_script(const Ref &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) { Ref f = FileAccess::open(p_path, FileAccess::WRITE); if (f.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), vformat(TTR("Could not open file \"%s\"."), p_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), vformat(TTR("Could not open file \"%s\"."), p_path)); return ERR_CANT_CREATE; } @@ -1623,7 +1623,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p const String base_dir = p_path.get_base_dir(); if (!DirAccess::exists(base_dir)) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir)); return ERR_FILE_BAD_PATH; } @@ -1640,7 +1640,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p String err; src_pkg_name = find_export_template("macos.zip", &err); if (src_pkg_name.is_empty()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), TTR("Export template not found.") + "\n" + err); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), TTR("Export template not found.") + "\n" + err); return ERR_FILE_NOT_FOUND; } } @@ -1654,7 +1654,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io); if (!src_pkg_zip) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not find template app to export: \"%s\"."), src_pkg_name)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not find template app to export: \"%s\"."), src_pkg_name)); return ERR_FILE_NOT_FOUND; } @@ -1683,7 +1683,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p export_format = "pkg"; #endif } else { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid export format.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid export format.")); return ERR_CANT_CREATE; } @@ -1708,7 +1708,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p Ref tmp_app_dir = DirAccess::create_for_path(tmp_base_path_name); if (tmp_app_dir.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory: \"%s\"."), tmp_base_path_name)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory: \"%s\"."), tmp_base_path_name)); err = ERR_CANT_CREATE; } @@ -1730,7 +1730,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p print_verbose("Creating " + tmp_app_path_name + "/Contents/MacOS"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/MacOS"); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/MacOS")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/MacOS")); } } @@ -1738,7 +1738,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p print_verbose("Creating " + tmp_app_path_name + "/Contents/Frameworks"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Frameworks"); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Frameworks")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Frameworks")); } } @@ -1746,7 +1746,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p print_line("Creating " + tmp_app_path_name + "/Contents/Helpers"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Helpers"); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Helpers")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Helpers")); } } @@ -1754,7 +1754,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p print_verbose("Creating " + tmp_app_path_name + "/Contents/Resources"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Resources"); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Resources")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Resources")); } } @@ -1919,21 +1919,21 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p if (((info.external_fa >> 16L) & 0120000) == 0120000) { #ifndef UNIX_ENABLED - add_message(EXPORT_MESSAGE_INFO, TTR("Export"), TTR("Relative symlinks are not supported on this OS, the exported project might be broken!")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Export"), TTR("Relative symlinks are not supported on this OS, the exported project might be broken!")); #endif // Handle symlinks in the archive. file = tmp_app_path_name.path_join(file); if (err == OK) { err = tmp_app_dir->make_dir_recursive(file.get_base_dir()); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), file.get_base_dir())); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), file.get_base_dir())); } } if (err == OK) { String lnk_data = String::utf8((const char *)data.ptr(), data.size()); err = tmp_app_dir->create_link(lnk_data, file); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not created symlink \"%s\" -> \"%s\"."), lnk_data, file)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not created symlink \"%s\" -> \"%s\"."), lnk_data, file)); } print_verbose(vformat("ADDING SYMLINK %s => %s\n", file, lnk_data)); } @@ -2017,7 +2017,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p if (err == OK) { err = tmp_app_dir->make_dir_recursive(file.get_base_dir()); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), file.get_base_dir())); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), file.get_base_dir())); } } if (err == OK) { @@ -2030,12 +2030,12 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p FileAccess::set_unix_permissions(file, 0755); #ifndef UNIX_ENABLED if (export_format == "app") { - add_message(EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Unable to set Unix permissions for executable \"%s\". Use \"chmod +x\" to set it after transferring the exported .app to macOS or Linux."), "Contents/MacOS/" + file.get_file())); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Unable to set Unix permissions for executable \"%s\". Use \"chmod +x\" to set it after transferring the exported .app to macOS or Linux."), "Contents/MacOS/" + file.get_file())); } #endif } } else { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not open \"%s\"."), file)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not open \"%s\"."), file)); err = ERR_CANT_CREATE; } } @@ -2048,7 +2048,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p unzClose(src_pkg_zip); if (!found_binary) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Requested template binary \"%s\" not found. It might be missing from your template archive."), binary_to_use)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Requested template binary \"%s\" not found. It might be missing from your template archive."), binary_to_use)); err = ERR_FILE_NOT_FOUND; } @@ -2060,11 +2060,11 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p FileAccess::set_unix_permissions(scr_path, 0755); #ifndef UNIX_ENABLED if (export_format == "app") { - add_message(EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Unable to set Unix permissions for executable \"%s\". Use \"chmod +x\" to set it after transferring the exported .app to macOS or Linux."), scr_path.get_file())); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Unable to set Unix permissions for executable \"%s\". Use \"chmod +x\" to set it after transferring the exported .app to macOS or Linux."), scr_path.get_file())); } #endif if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not create console wrapper.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not create console wrapper.")); } } } @@ -2100,12 +2100,12 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p bool lib_validation = p_preset->get("codesign/entitlements/disable_library_validation"); if (!shared_objects.is_empty() && sign_enabled && ad_hoc && !lib_validation) { - add_message(EXPORT_MESSAGE_INFO, TTR("Entitlements Modified"), TTR("Ad-hoc signed applications require the 'Disable Library Validation' entitlement to load dynamic libraries.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Entitlements Modified"), TTR("Ad-hoc signed applications require the 'Disable Library Validation' entitlement to load dynamic libraries.")); lib_validation = true; } if (!shared_objects.is_empty() && sign_enabled && codesign_tool == 2) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("'rcodesign' doesn't support signing applications with embedded dynamic libraries.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("'rcodesign' doesn't support signing applications with embedded dynamic libraries.")); } bool sandbox = p_preset->get("codesign/entitlements/app_sandbox/enabled"); @@ -2263,7 +2263,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p ent_f->store_line(""); ent_f->store_line(""); } else { - add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not create entitlements file.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not create entitlements file.")); err = ERR_CANT_CREATE; } @@ -2281,7 +2281,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p ent_f->store_line(""); ent_f->store_line(""); } else { - add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not create helper entitlements file.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not create helper entitlements file.")); err = ERR_CANT_CREATE; } } @@ -2298,7 +2298,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p FileAccess::set_unix_permissions(tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), 0755); #ifndef UNIX_ENABLED if (export_format == "app") { - add_message(EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Unable to set Unix permissions for executable \"%s\". Use \"chmod +x\" to set it after transferring the exported .app to macOS or Linux."), "Contents/Helpers/" + hlp_path.get_file())); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Unable to set Unix permissions for executable \"%s\". Use \"chmod +x\" to set it after transferring the exported .app to macOS or Linux."), "Contents/Helpers/" + hlp_path.get_file())); } #endif } @@ -2413,7 +2413,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p if (err == OK && noto_enabled) { if (export_format == "pkg") { - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("Notarization requires the app to be archived first, select the DMG or ZIP export format instead.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("Notarization requires the app to be archived first, select the DMG or ZIP export format instead.")); } else { if (ep.step(TTR("Sending archive for notarization"), 4)) { return ERR_SKIP; @@ -2744,7 +2744,7 @@ Error EditorExportPlatformMacOS::run(const Ref &p_preset, in } } - const bool use_remote = p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT); + const bool use_remote = p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_DUMB_CLIENT); int dbg_port = EDITOR_GET("network/debug/remote_port"); print_line("Creating temporary directory..."); diff --git a/platform/web/detect.py b/platform/web/detect.py index 49dc2c50f529..97a6f43a63b1 100644 --- a/platform/web/detect.py +++ b/platform/web/detect.py @@ -11,6 +11,7 @@ create_engine_file, create_template_zip, get_template_zip_path, + package_js_module_generator, run_closure_compiler, ) from SCons.Util import WhereIs @@ -228,6 +229,13 @@ def configure(env: "SConsEnvironment"): # Add method for creating the final zip file env.AddMethod(create_template_zip, "CreateTemplateZip") + # Add method for bundling JS modules. + env.Append( + BUILDERS={ + "PackageJSModule": env.Builder(generator=package_js_module_generator), + } + ) + # Use TempFileMunge since some AR invocations are too long for cmd.exe. # Use POSIX-style paths, required with TempFileMunge. env["ARCOM_POSIX"] = env["ARCOM"].replace("$TARGET", "$TARGET.posix").replace("$SOURCES", "$SOURCES.posix") diff --git a/platform/web/emscripten_helpers.py b/platform/web/emscripten_helpers.py index 6f3c0517079e..9b399ed38089 100644 --- a/platform/web/emscripten_helpers.py +++ b/platform/web/emscripten_helpers.py @@ -1,11 +1,22 @@ import json import os +import typing + +if typing.TYPE_CHECKING: + T = typing.TypeVar("T") from SCons.Util import WhereIs +from misc.scripts.copyright_headers import process_file_buffer as process_file_buffer_copyright_buffer from platform_methods import get_build_version +def ensure_list(value): # type: (typing.Union[T, typing.List[T]]) -> typing.List[T] + if not isinstance(value, list): + return [value] + return value + + def run_closure_compiler(target, source, env, for_signature): closure_bin = os.path.join( os.path.dirname(WhereIs("emcc")), @@ -30,20 +41,63 @@ def create_engine_file(env, target, source, externs, threads_enabled): return env.Substfile(target=target, source=[env.File(s) for s in source], SUBST_DICT=subst_dict) +def package_js_module_generator(target, source, env, for_signature): + if for_signature: + return source + + target = target[0] + source = source[0] + + def get_wrapper(filename): + def wrapper(target, source, env): + return package_js_module_action_ensure_copyright_buffer(target, source, env, filename) + + return wrapper + + source_filename = os.path.basename(source.get_abspath()) + wrapper = get_wrapper(source_filename) + return env.Action(wrapper, "Ensuring copyright buffer.") + + +def package_js_module_action_ensure_copyright_buffer(target, source, env, filename): + target = target[0] + source = source[0] + + with open(source, mode="r", encoding="utf-8") as source_file: + with open(target, mode="w", encoding="utf-8") as target_file: + new_contents = process_file_buffer_copyright_buffer(filename, source_file) + CHUNK_SIZE = 1024 + while True: + chunk = new_contents.read(CHUNK_SIZE) + if not chunk: + break + target_file.write(chunk) + + def create_template_zip(env, js, wasm, side): binary_name = "godot.editor" if env.editor_build else "godot" zip_dir = env.Dir(env.GetTemplateZipPath()) + in_files = [ js, wasm, "#platform/web/js/libs/audio.worklet.js", "#platform/web/js/libs/audio.position.worklet.js", + env.PackageJSModule( + target="#bin/obj/platform/web/js/modules/utils/concurrency.js", + source="#platform/web/js/modules/utils/concurrency.js", + ), + env.PackageJSModule( + target="#bin/obj/platform/web/js/modules/utils/wait.js", source="#platform/web/js/modules/utils/wait.js" + ), ] out_files = [ zip_dir.File(binary_name + ".js"), zip_dir.File(binary_name + ".wasm"), zip_dir.File(binary_name + ".audio.worklet.js"), zip_dir.File(binary_name + ".audio.position.worklet.js"), + zip_dir.File(binary_name + ".utils.concurrency.js"), + zip_dir.File(binary_name + ".utils.wait.js"), ] # Dynamic linking (extensions) specific. if env["dlink_enabled"]: diff --git a/platform/web/eslint.config.cjs b/platform/web/eslint.config.cjs index 7bc1280396f0..ce457fdcd068 100644 --- a/platform/web/eslint.config.cjs +++ b/platform/web/eslint.config.cjs @@ -140,6 +140,16 @@ module.exports = [ }, }, + // module files (browser) + { + files: ['js/modules/**/*.js', 'platform/web/js/modules/**/*.js'], + languageOptions: { + globals: { + ...globals.browser, + }, + }, + }, + // libraries and modules (browser) { files: [ diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp index effd549b44ca..040ef54a6ca0 100644 --- a/platform/web/export/export_plugin.cpp +++ b/platform/web/export/export_plugin.cpp @@ -30,6 +30,14 @@ #include "export_plugin.h" +#include "core/crypto/hashing_context.h" +#include "core/extension/gdextension.h" +#include "core/io/config_file.h" +#include "core/io/json.h" +#include "core/io/resource_loader.h" +#include "core/os/memory.h" +#include "core/string/string_builder.h" +#include "editor/export/editor_export_platform.h" #include "logo_svg.gen.h" #include "run_icon_svg.gen.h" @@ -37,6 +45,8 @@ #include "core/io/dir_access.h" #include "editor/editor_string_names.h" #include "editor/export/editor_export.h" +#include "editor/export/editor_export_platform_utils.h" +#include "editor/file_system/editor_file_system.h" #include "editor/import/resource_importer_texture_settings.h" #include "editor/settings/editor_settings.h" #include "editor/themes/editor_scale.h" @@ -45,18 +55,341 @@ #include "modules/modules_enabled.gen.h" // For mono. #include "modules/svg/image_loader_svg.h" +#include + +/** + * EditorExportPlatformWeb::ExportData::ResourceData + */ +uint32_t EditorExportPlatformWeb::ExportData::ResourceData::get_size() const { + uint32_t size = 0; + if (native_file.exists) { + size += native_file.size; + } + if (remap_file.exists) { + size += remap_file.size; + } + if (remapped_file.exists) { + size += remapped_file.size; + } + return size; +} + +Dictionary EditorExportPlatformWeb::ExportData::ResourceData::get_as_resource_dictionary() const { + Dictionary data; + Dictionary resources; + + if (native_file.exists) { + resources[native_file.resource_path] = native_file.get_as_dictionary(); + } + if (remap_file.exists) { + resources[remap_file.resource_path] = remap_file.get_as_dictionary(); + } + if (remapped_file.exists) { + resources[remapped_file.resource_path] = remapped_file.get_as_dictionary(); + } + data["files"] = resources; + data["totalSize"] = get_size(); + return data; +} + +String EditorExportPlatformWeb::ExportData::ResourceData::get_resource_path() const { + if (remap_file.exists) { + return remap_file.resource_path; + } + return native_file.resource_path; +} + +void EditorExportPlatformWeb::ExportData::ResourceData::flatten_dependencies(LocalVector *p_deps) const { + ERR_FAIL_NULL(p_deps); + + for (const ResourceData *dependency : dependencies) { + if (p_deps->has(dependency)) { + continue; + } + p_deps->push_back(dependency); + dependency->flatten_dependencies(p_deps); + } +} + +HashSet EditorExportPlatformWeb::ExportData::get_features_set() const { + List features_list; + + preset->get_platform()->get_platform_features(&features_list); + preset->get_platform()->get_preset_features(preset, &features_list); + + String custom = preset->get_custom_features(); + Vector custom_list = custom.split(","); + for (int i = 0; i < custom_list.size(); i++) { + String f = custom_list[i].strip_edges(); + if (!f.is_empty()) { + features_list.push_back(f); + } + } + + HashSet features_set; + for (const String &feature : features_list) { + features_set.insert(feature); + } + return features_set; +} + +EditorExportPlatformWeb::ExportData::ResourceData *EditorExportPlatformWeb::ExportData::add_dependency(const String &p_path, const HashSet &p_features_set, Ref p_uid_cache, /* bool p_encrypt, */ Error *r_error) { + ResourceData *data = nullptr; + List::Element *data_iterator = nullptr; + + String remap_path; + bool has_suffix_import = false; + +#define SET_ERR(m_err) \ + if (r_error != nullptr) { \ + *r_error = (m_err); \ + } \ + ((void)0) + +#define _HANDLE_ERR_COND_V_MSG(m_cond, m_err, m_msg) \ + if (unlikely((m_cond))) { \ + SET_ERR(m_err); \ + if (data != nullptr && data_iterator != nullptr) { \ + dependencies.erase(data_iterator); \ + } \ + ERR_FAIL_V_MSG(nullptr, (m_msg)); \ + return nullptr; \ + } \ + ((void)0) + + _HANDLE_ERR_COND_V_MSG(p_path.is_empty(), ERR_INVALID_PARAMETER, "p_path.is_empty()"); + + if (dependencies_map.has(p_path)) { + SET_ERR(OK); + return dependencies_map[p_path]; + } + + data_iterator = dependencies.push_back({}); + data = &data_iterator->get(); + data->path = p_path; + update_file(&data->native_file, p_path); + + if (FileAccess::exists(data->native_file.absolute_path + SUFFIX_IMPORT)) { + has_suffix_import = true; + remap_path = data->native_file.resource_path + SUFFIX_IMPORT; + } else if (FileAccess::exists(data->native_file.absolute_path + SUFFIX_REMAP)) { + remap_path = data->native_file.resource_path + SUFFIX_REMAP; + } + + _HANDLE_ERR_COND_V_MSG(!data->native_file.exists && remap_path.is_empty(), ERR_FILE_NOT_FOUND, vformat(R"*("%s" doesn't exist, and there is no remap/import file.)*", data->native_file.absolute_path)); + + if (!remap_path.is_empty()) { + update_file(&data->remap_file, remap_path); + _HANDLE_ERR_COND_V_MSG(!data->remap_file.exists, ERR_FILE_NOT_FOUND, vformat(R"*("%s" doesn't exist)*", data->remap_file.absolute_path)); + + Error err; + Ref remap_file_access = FileAccess::open(data->remap_file.absolute_path, FileAccess::READ, &err); + _HANDLE_ERR_COND_V_MSG(err != OK, err, vformat(R"*(Error while opening "%s": %s)*", data->remap_file.absolute_path, error_names[err])); + + Ref remap_file; + remap_file.instantiate(); + // if (p_is_encrypted) { + // Ref remap_file_access_encrypted; + // remap_file_access_encrypted.instantiate(); + // Error err = remap_file_access_encrypted->open_and_parse(remap_file_access, p_key, FileAccessEncrypted::MODE_READ, false); + // ERR_FAIL_COND_V(err != OK, err); + // remap_file_access = remap_file_access_encrypted; + // } + remap_file->parse(remap_file_access->get_as_text()); + + const String PREFIX_PATH = "path."; + const String PATH_UID = "uid"; + + String remapped_path; + String uid_path; + Vector remap_section_keys = remap_file->get_section_keys("remap"); + for (const String &remap_section_key : remap_section_keys) { + bool found = false; + if (remap_section_key == PATH_UID) { + p_uid_cache->seek(0); + uid_path = ResourceUID::get_path_from_cache(p_uid_cache, remap_file->get_value("remap", remap_section_key)); + continue; + } + if (remap_section_key.begins_with(PREFIX_PATH)) { + String type = remap_section_key.trim_prefix(PREFIX_PATH); + if (p_features_set.has(type)) { + found = true; + } + } + if (remap_section_key == "path") { + found = true; + } + if (!found) { + continue; + } + remapped_path = remap_file->get_value("remap", remap_section_key); + break; + } + if (remapped_path.is_empty() && !uid_path.is_empty()) { + remapped_path = uid_path; + } + _HANDLE_ERR_COND_V_MSG(remapped_path.is_empty(), ERR_PARSE_ERROR, vformat(R"*(Could not find any remap path in %s file "%s")*", has_suffix_import ? "import" : "remap", data->remap_file.absolute_path)); + + update_file(&data->remapped_file, remapped_path); + } + + dependencies_map.insert(p_path, data); + + File *resource_file = nullptr; + if (data->native_file.exists && !data->remap_file.exists) { + resource_file = &data->native_file; + } else if (data->remapped_file.exists) { + resource_file = &data->remapped_file; + } + + if (resource_file != nullptr) { + List remapped_dependencies; + ResourceLoader::get_dependencies(resource_file->absolute_path, &remapped_dependencies); + for (const String &remapped_dependency : remapped_dependencies) { + Error error; + String remapped_dependency_path = EditorExportPlatformUtils::get_path_from_dependency(remapped_dependency); + ResourceData *dependency = add_dependency(remapped_dependency_path, p_features_set, p_uid_cache, &error); + _HANDLE_ERR_COND_V_MSG(error != OK, error, vformat(R"*(Error while processing remapped dependencies of "%s": couldn't add dependency of "%s")*", resource_file->absolute_path, remapped_dependency_path)); + data->dependencies.push_back(dependency); + } + } + + SET_ERR(OK); + return data; + +#undef _HANDLE_ERR_COND_V_MSG +#undef SET_ERR +} + +void EditorExportPlatformWeb::ExportData::update_file(File *p_file, const String &p_resource_path) { + ERR_FAIL_NULL(p_file); + ERR_FAIL_COND(p_resource_path.is_empty()); + + p_file->resource_path = p_resource_path; + p_file->absolute_path = res_to_global(p_resource_path); + p_file->exists = FileAccess::exists(p_file->absolute_path); + if (!p_file->exists) { + return; + } + + p_file->size = FileAccess::get_size(p_file->absolute_path); + if (p_file->size == 0) { + return; + } + + Ref context_md5; + context_md5.instantiate(); + context_md5->start(HashingContext::HASH_MD5); + + Ref context_sha256; + context_sha256.instantiate(); + context_sha256->start(HashingContext::HASH_SHA256); + + const uint64_t CHUNK_SIZE = 1024; + Error error; + Ref file = FileAccess::open(p_file->absolute_path, FileAccess::READ, &error); + if (error != OK) { + ERR_FAIL_MSG(vformat(R"*(Error while opening "%s": %s)*", p_file->absolute_path, error_names[error])); + } + + while (file->get_position() < file->get_length()) { + uint64_t remaining = file->get_length() - file->get_position(); + PackedByteArray chunk = file->get_buffer(MIN(remaining, CHUNK_SIZE)); + context_md5->update(chunk); + context_sha256->update(chunk); + } + + PackedByteArray hash_md5 = context_md5->finish(); + PackedByteArray hash_sha256 = context_sha256->finish(); + + p_file->md5 = String::hex_encode_buffer(hash_md5.ptr(), hash_md5.size()); + p_file->sha256 = String::hex_encode_buffer(hash_sha256.ptr(), hash_sha256.size()); +} + +Dictionary EditorExportPlatformWeb::ExportData::get_deps_json_dictionary(const ResourceData *p_dependency) { + Dictionary deps; + Dictionary resources; + + // Resources. + deps["resources"] = resources; + resources[p_dependency->path] = p_dependency->get_as_resource_dictionary(); + + // Dependencies. + Dictionary deps_dependencies; + deps["dependencies"] = deps_dependencies; + + // Recursive dependency lambda. + std::function _l_add_deps_dependencies; + _l_add_deps_dependencies = [&](const ExportData::ResourceData *l_dependency) -> void { + resources[l_dependency->path] = l_dependency->get_as_resource_dictionary(); + LocalVector local_dependencies; + l_dependency->flatten_dependencies(&local_dependencies); + + PackedStringArray paths_array; + for (const ExportData::ResourceData *local_dependency : local_dependencies) { + if (local_dependency->path != l_dependency->path) { + paths_array.push_back(local_dependency->path); + } + if (!deps_dependencies.has(local_dependency->path)) { + // Prevent infinite recursion. + deps_dependencies[local_dependency->path] = {}; + _l_add_deps_dependencies(local_dependency); + } + } + paths_array.sort_custom(); + deps_dependencies[l_dependency->path] = paths_array; + }; + + // Loop through each dependencies to find their dependencies. + for (const ExportData::ResourceData *dependency : p_dependency->dependencies) { + _l_add_deps_dependencies(dependency); + } + + // Register the asked dependency itself. + _l_add_deps_dependencies(p_dependency); + + return deps; +} + +Error EditorExportPlatformWeb::ExportData::save_deps_json(const ResourceData *p_dependency) { + Dictionary deps = get_deps_json_dictionary(p_dependency); + String resource_path = p_dependency->get_resource_path(); + if (resource_path == p_dependency->remap_file.resource_path) { + if (resource_path.ends_with(SUFFIX_REMAP)) { + resource_path = resource_path.trim_suffix(SUFFIX_REMAP); + } else { + resource_path = resource_path.trim_suffix(SUFFIX_IMPORT); + } + } + + String deps_json_file_path = res_to_global(resource_path) + ".deps.json"; + Error error; + Ref deps_json_file = FileAccess::open(deps_json_file_path, FileAccess::WRITE, &error); + if (error != OK) { + ERR_PRINT(vformat(R"*(Could not write to "%s".)*", deps_json_file_path)); + return error; + } + deps_json_file->store_string(JSON::stringify(deps, String(" ").repeat(2))); + + return OK; +} + +/** + * EditorExportPlatformWeb + */ + Error EditorExportPlatformWeb::_extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa) { Ref io_fa; zlib_filefunc_def io = zipio_create_io(&io_fa); unzFile pkg = unzOpen2(p_template.utf8().get_data(), &io); if (!pkg) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not open template for export: \"%s\"."), p_template)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not open template for export: \"%s\"."), p_template)); return ERR_FILE_NOT_FOUND; } if (unzGoToFirstFile(pkg) != UNZ_OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Invalid export template: \"%s\"."), p_template)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Invalid export template: \"%s\"."), p_template)); unzClose(pkg); return ERR_FILE_CORRUPT; } @@ -90,7 +423,7 @@ Error EditorExportPlatformWeb::_extract_template(const String &p_template, const String dst = p_dir.path_join(file.replace("godot", p_name)); Ref f = FileAccess::open(dst, FileAccess::WRITE); if (f.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not write file: \"%s\"."), dst)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not write file: \"%s\"."), dst)); unzClose(pkg); return ERR_FILE_CANT_WRITE; } @@ -104,7 +437,7 @@ Error EditorExportPlatformWeb::_extract_template(const String &p_template, const Error EditorExportPlatformWeb::_write_or_error(const uint8_t *p_content, int p_size, String p_path) { Ref f = FileAccess::open(p_path, FileAccess::WRITE); if (f.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), p_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), p_path)); return ERR_FILE_CANT_WRITE; } f->store_buffer(p_content, p_size); @@ -129,14 +462,14 @@ void EditorExportPlatformWeb::_replace_strings(const HashMap &p_ } } -void EditorExportPlatformWeb::_fix_html(Vector &p_html, const Ref &p_preset, const String &p_name, bool p_debug, BitField p_flags, const Vector p_shared_objects, const Dictionary &p_file_sizes) { +void EditorExportPlatformWeb::_fix_html(Vector &p_html, const Ref &p_preset, const String &p_name, bool p_debug, BitField p_flags, const Vector p_shared_objects, const Dictionary &p_file_sizes, const Dictionary &p_async_pck_data) { // Engine.js config Dictionary config; Array libs; for (int i = 0; i < p_shared_objects.size(); i++) { libs.push_back(p_shared_objects[i].path.get_file()); } - Vector flags = gen_export_flags(p_flags & (~DEBUG_FLAG_DUMB_CLIENT)); + Vector flags = gen_export_flags(p_flags & (~EditorExportPlatformData::DEBUG_FLAG_DUMB_CLIENT)); Array args; for (int i = 0; i < flags.size(); i++) { args.push_back(flags[i]); @@ -153,6 +486,17 @@ void EditorExportPlatformWeb::_fix_html(Vector &p_html, const Refget("threads/godot_pool_size"); config["emscriptenPoolSize"] = p_preset->get("threads/emscripten_pool_size"); + AsyncLoadSetting async_initial_load_mode = (AsyncLoadSetting)(int)p_preset->get("async/initial_load_mode"); + switch (async_initial_load_mode) { + case ASYNC_LOAD_SETTING_LOAD_EVERYTHING: { + config["mainPack"] = p_name + ".pck"; + } break; + case ASYNC_LOAD_SETTING_MINIMUM_INITIAL_RESOURCES: { + config["mainPack"] = p_name + ".asyncpck"; + config["asyncPckData"] = p_async_pck_data; + } break; + } + String head_include; if (p_preset->get("html/export_icon")) { head_include += "\n"; @@ -200,7 +544,7 @@ Error EditorExportPlatformWeb::_add_manifest_icon(const Ref Error err = OK; icon = _load_icon_or_splash_image(p_icon, &err); if (err != OK || icon.is_null() || icon->is_empty()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not read file: \"%s\"."), p_icon)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not read file: \"%s\"."), p_icon)); return err; } if (icon->get_width() != p_size || icon->get_height() != p_size) { @@ -212,7 +556,7 @@ Error EditorExportPlatformWeb::_add_manifest_icon(const Ref } const Error err = icon->save_png(icon_dest); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not write file: \"%s\"."), icon_dest)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not write file: \"%s\"."), icon_dest)); return err; } Dictionary icon_dict; @@ -258,8 +602,19 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref &p_prese // Heavy files that are cached on demand. Array opt_cache_files = { name + ".wasm", - name + ".pck" }; + + AsyncLoadSetting async_initial_load_mode = (AsyncLoadSetting)(int)p_preset->get("async/initial_load_mode"); + switch (async_initial_load_mode) { + case ASYNC_LOAD_SETTING_LOAD_EVERYTHING: { + opt_cache_files.push_back(name + ".pck"); + } break; + + case ASYNC_LOAD_SETTING_MINIMUM_INITIAL_RESOURCES: { + // TODO: Add AsyncPCK contents to the cache. + } break; + } + if (extensions) { opt_cache_files.push_back(name + ".side.wasm"); for (int i = 0; i < p_shared_objects.size(); i++) { @@ -273,7 +628,7 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref &p_prese { Ref f = FileAccess::open(sw_path, FileAccess::READ); if (f.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("PWA"), vformat(TTR("Could not read file: \"%s\"."), sw_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("PWA"), vformat(TTR("Could not read file: \"%s\"."), sw_path)); return ERR_FILE_CANT_READ; } sw.resize(f->get_length()); @@ -293,7 +648,7 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref &p_prese const String offline_dest = dir.path_join(name + ".offline.html"); err = da->copy(ProjectSettings::get_singleton()->globalize_path(offline_page), offline_dest); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("PWA"), vformat(TTR("Could not read file: \"%s\"."), offline_dest)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("PWA"), vformat(TTR("Could not read file: \"%s\"."), offline_dest)); return err; } } @@ -368,6 +723,11 @@ void EditorExportPlatformWeb::get_export_options(List *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "async/initial_load_mode", PROPERTY_HINT_ENUM, "Load Everything,Load Minimum Initial Resources"), 0, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "async/initial_load_forced_files_filters_to_include"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "async/initial_load_forced_files_filters_to_exclude"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "async/initial_load_forced_files", PROPERTY_HINT_ARRAY_TYPE, MAKE_FILE_ARRAY_TYPE_HINT("*")), PackedStringArray())); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/extensions_support"), false)); // GDExtension support. r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/thread_support"), false, true)); // Thread support (i.e. run with or without COEP/COOP headers). r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_desktop"), true)); // S3TC @@ -394,6 +754,10 @@ void EditorExportPlatformWeb::get_export_options(List *r_options) } bool EditorExportPlatformWeb::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const { + if (p_option == "async/initial_load_forced_files" || p_option == "async/initial_load_forced_files_filters_to_include" || p_option == "async/initial_load_forced_files_filters_to_exclude") { + return (int)p_preset->get("async/initial_load_mode") != ASYNC_LOAD_SETTING_LOAD_EVERYTHING; + } + bool advanced_options_enabled = p_preset->are_advanced_options_enabled(); if (p_option == "custom_template/debug" || p_option == "custom_template/release") { return advanced_options_enabled; @@ -427,6 +791,13 @@ bool EditorExportPlatformWeb::has_valid_export_configuration(const Refget("async/initial_load_mode") != AsyncLoadSetting::ASYNC_LOAD_SETTING_LOAD_EVERYTHING) { + if (String(EditorExportPlatformUtils::get_project_setting(p_preset, "application/run/main_scene")).is_empty()) { + err += TTR("No main scene has been set. The main scene must be set for the web platform in order to preload the minimal files.") + "\n"; + } + } + bool valid = false; bool extensions = (bool)p_preset->get("variant/extensions_support"); bool thread_support = (bool)p_preset->get("variant/thread_support"); @@ -493,12 +864,20 @@ Error EditorExportPlatformWeb::export_project(const Ref &p_p const bool export_icon = p_preset->get("html/export_icon"); const bool pwa = p_preset->get("progressive_web_app/enabled"); - const String base_dir = p_path.get_base_dir(); - const String base_path = p_path.get_basename(); - const String base_name = p_path.get_file().get_basename(); + String path = p_path; + if (!path.is_absolute_path()) { + if (!path.begins_with("res://")) { + path = "res://" + path; + } + path = ProjectSettings::get_singleton()->globalize_path(path); + } + + const String base_dir = path.get_base_dir() + "/"; + const String base_path = path.get_basename(); + const String base_name = path.get_file().get_basename(); if (!DirAccess::exists(base_dir)) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir)); return ERR_FILE_BAD_PATH; } @@ -512,29 +891,349 @@ Error EditorExportPlatformWeb::export_project(const Ref &p_p } if (!template_path.is_empty() && !FileAccess::exists(template_path)) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Template file not found: \"%s\"."), template_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Template file not found: \"%s\"."), template_path)); return ERR_FILE_NOT_FOUND; } + Error error; + // Export pck and shared objects Vector shared_objects; - String pck_path = base_path + ".pck"; - Error error = save_pack(p_preset, p_debug, pck_path, &shared_objects); - if (error != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), pck_path)); - return error; - } + String pck_path; - { - Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - for (int i = 0; i < shared_objects.size(); i++) { - String dst = base_dir.path_join(shared_objects[i].path.get_file()); - error = da->copy(shared_objects[i].path, dst); + // Async PCK related. + Dictionary async_pck_data; + // Parse generated file sizes (pck and wasm, to help show a meaningful loading bar). + Dictionary file_sizes; + + AsyncLoadSetting async_initial_load_mode = (AsyncLoadSetting)(int)p_preset->get("async/initial_load_mode"); + switch (async_initial_load_mode) { + case ASYNC_LOAD_SETTING_LOAD_EVERYTHING: { + pck_path = base_path + ".pck"; + + error = save_pack(p_preset, p_debug, pck_path, &shared_objects); if (error != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), shared_objects[i].path.get_file())); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), pck_path)); return error; } - } + + { + Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + for (int i = 0; i < shared_objects.size(); i++) { + String dst = base_dir.path_join(shared_objects[i].path.get_file()); + error = da->copy(shared_objects[i].path, dst); + if (error != OK) { + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), shared_objects[i].path.get_file())); + return error; + } + } + } + + // Updating file sizes. + Ref f = FileAccess::open(pck_path, FileAccess::READ); + if (f.is_valid()) { + file_sizes[pck_path.get_file()] = (uint64_t)f->get_length(); + } + + } break; + + case ASYNC_LOAD_SETTING_MINIMUM_INITIAL_RESOURCES: { + pck_path = base_path + ".asyncpck"; + + if (DirAccess::dir_exists_absolute(pck_path)) { + Ref pck_path_access = DirAccess::create_for_path(pck_path); + pck_path_access->change_dir(pck_path); + pck_path_access->erase_contents_recursive(); + pck_path_access->change_dir(".."); + pck_path_access->remove_absolute(pck_path); + } + + ExportData export_data; + export_data.assets_directory = pck_path.path_join("assets"); + export_data.libraries_directory = pck_path.path_join("libraries"); + export_data.pack_data.path = "assets.sparsepck"; + export_data.pack_data.use_sparse_pck = true; + export_data.preset = p_preset; + + HashSet features_set = export_data.get_features_set(); + + error = export_project_files(p_preset, p_debug, &EditorExportPlatformWeb::_rename_and_store_file_in_async_pck, nullptr, &export_data); + if (error != OK) { + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write async pck: \"%s\"."), pck_path)); + return error; + } + + PackedByteArray encoded_data; + error = _generate_sparse_pck_metadata(p_preset, export_data.pack_data, encoded_data, true); + if (error != OK) { + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not encode contents of async pck: \"%s\"."), pck_path)); + return error; + } + + error = EditorExportPlatformUtils::store_file_at_path(export_data.assets_directory.path_join("assets.sparsepck"), encoded_data); + if (error != OK) { + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not store contents of async pck: \"%s\"."), pck_path)); + return error; + } + + // bool is_encrypted = p_preset->get_enc_pck() && p_preset->get_enc_directory(); + + { + Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + for (int i = 0; i < shared_objects.size(); i++) { + String dst = export_data.libraries_directory.path_join(shared_objects[i].path.get_file()); + error = da->copy(shared_objects[i].path, dst); + if (error != OK) { + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), shared_objects[i].path.get_file())); + return error; + } + } + } + + { + Dictionary async_pck_data_directories; + Dictionary async_pck_data_initial_load; + + async_pck_data["directories"] = async_pck_data_directories; + async_pck_data["initialLoad"] = async_pck_data_initial_load; + + const String PREFIX_ASSETS_DIRECTORY = export_data.assets_directory + "/"; + + const String SECTION_REMAP = "remap"; + const String SECTION_KEY_PATH = "path"; + const String SECTION_KEY_PATH_PREFIX = "path."; + + const String PATH_GODOT_DIR = ".godot/"; + + HashSet exported_files; + HashSet internal_files; + HashSet standalone_files; + HashSet remap_files; + HashSet import_files; + + uint64_t total_size = 0; + + Ref uid_cache = FileAccess::open(export_data.res_to_global(PATH_GODOT_UID_CACHE), FileAccess::READ, &error); + if (error != OK) { + return error; + } + + for (const String &exported_file : export_data.exported_files) { + String local_exported_file = PREFIX_RES + exported_file.trim_prefix(PREFIX_ASSETS_DIRECTORY).simplify_path(); + exported_files.insert(local_exported_file); + } + + for (const String &exported_file : exported_files) { + if (exported_file.begins_with(PATH_GODOT_DIR) || exported_file == PATH_PROJECT_BINARY || exported_file == PATH_ASSETS_SPARSEPCK) { + internal_files.insert(exported_file); + continue; + } + + if (exported_file.ends_with(SUFFIX_REMAP)) { + remap_files.insert(exported_file); + continue; + } else if (exported_file.ends_with(SUFFIX_IMPORT)) { + import_files.insert(exported_file); + continue; + } + + standalone_files.insert(exported_file); + } + + for (const String &internal_file : internal_files) { + export_data.add_dependency(internal_file, features_set, uid_cache, &error); + if (error != OK) { + return error; + } + } + + for (const String &remap_file : remap_files) { + export_data.add_dependency(remap_file.trim_suffix(SUFFIX_REMAP), features_set, uid_cache, &error); + if (error != OK) { + return error; + } + } + + for (const String &import_file : import_files) { + export_data.add_dependency(import_file.trim_suffix(SUFFIX_IMPORT), features_set, uid_cache, &error); + if (error != OK) { + return error; + } + } + + for (const String &standalone_file : standalone_files) { + export_data.add_dependency(standalone_file, features_set, uid_cache, &error); + if (error != OK) { + return error; + } + } + + for (ExportData::ResourceData &dependency : export_data.dependencies) { + if (dependency.path.begins_with(PREFIX_RES + PATH_GODOT_DIR)) { + continue; + } + error = export_data.save_deps_json(&dependency); + if (error != OK) { + return error; + } + } + + HashSet initial_load_dependencies; + { + Vector initial_load_in_filters; + Vector initial_load_ex_filters; + + Vector initial_load_in_split = String(p_preset->get("async/initial_load_forced_files_filters_to_include")).split(","); + for (int i = 0; i < initial_load_in_split.size(); i++) { + String initial_load_in_filter = initial_load_in_split[i].strip_edges(); + if (initial_load_in_filter.is_empty()) { + continue; + } + initial_load_in_filters.push_back(initial_load_in_filter); + } + + Vector initial_load_ex_split = String(p_preset->get("async/initial_load_forced_files_filters_to_exclude")).split(","); + for (int i = 0; i < initial_load_ex_split.size(); i++) { + String initial_load_ex_filter = initial_load_ex_split[i].strip_edges(); + if (initial_load_ex_filter.is_empty()) { + continue; + } + initial_load_ex_filters.push_back(initial_load_ex_filter); + } + + if (initial_load_in_filters.size() > 0) { + for (const ExportData::ResourceData &dependency : export_data.dependencies) { + const String &dependency_path = dependency.path; + bool add_as_initial_load = false; + for (const String &in_filter : initial_load_in_filters) { + if (dependency_path.matchn(in_filter) || dependency_path.trim_prefix(PREFIX_RES).matchn(in_filter)) { + add_as_initial_load = true; + break; + } + } + + for (const String &ex_filter : initial_load_ex_filters) { + if (dependency_path.matchn(ex_filter) || dependency_path.trim_prefix(PREFIX_RES).matchn(ex_filter)) { + add_as_initial_load = false; + break; + } + } + + if (add_as_initial_load) { + initial_load_dependencies.insert(&dependency); + } + } + } + } + + HashSet mandatory_initial_load_files = _get_mandatory_initial_load_files(p_preset); + for (const String &mandatory_initial_load_file : mandatory_initial_load_files) { + export_data.add_dependency(mandatory_initial_load_file, features_set, uid_cache); + } + for (const String &mandatory_initial_load_file : mandatory_initial_load_files) { + ExportData::ResourceData *mandatory_resource_data = export_data.dependencies_map[mandatory_initial_load_file]; + initial_load_dependencies.insert(mandatory_resource_data); + LocalVector mandatory_resource_data_dependencies; + mandatory_resource_data->flatten_dependencies(&mandatory_resource_data_dependencies); + for (const ExportData::ResourceData *mandatory_resource_data_dependency : mandatory_resource_data_dependencies) { + initial_load_dependencies.insert(mandatory_resource_data_dependency); + } + } + + { + PackedStringArray initial_load_paths; + HashSet initial_load_assets; + for (const ExportData::ResourceData *dependency : initial_load_dependencies) { + if (dependency->remap_file.exists || dependency->native_file.exists) { + initial_load_assets.insert(dependency); + } + } + + LocalVector initial_load_assets_data; + for (const ExportData::ResourceData *initial_load_asset : initial_load_assets) { + initial_load_assets_data.push_back(initial_load_asset); + } + + _add_resource_data_tree_message(initial_load_assets_data, "Files that will be initially loaded (sorted in alphabetical order):", true, false); + _add_resource_data_tree_message(initial_load_assets_data, "Files that will be initially loaded (sorted by size):", false, true); + + uint64_t initial_load_assets_size = initial_load_assets_data.size(); + for (uint64_t i = 0; i < initial_load_assets_size; i++) { + const ExportData::ResourceData *initial_load_asset = initial_load_assets_data[i]; + + uint64_t asset_size = 0; + if (initial_load_asset->remap_file.exists) { + asset_size += initial_load_asset->remap_file.size; + asset_size += initial_load_asset->remapped_file.size; + } else if (initial_load_asset->native_file.exists) { + asset_size += initial_load_asset->native_file.size; + } else { + ERR_FAIL_V(ERR_BUG); + } + + total_size += asset_size; + } + + StringBuilder log_entry_builder; + log_entry_builder.append("If some files seem to be missing from this list, be sure to edit \"async/initial_load_forced_files*\" in the preset settings.\n"); + log_entry_builder.append("For files not in this list, you will need to call `OS.async_pck_install_file()` beforehand.\n"); + log_entry_builder.append("\n"); + log_entry_builder.append(vformat("Total initial load size: %s", String::humanize_size(total_size))); + + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Initial load"), log_entry_builder.as_string()); + } + + { + async_pck_data_directories["assets"] = export_data.assets_directory.trim_prefix(base_dir); + async_pck_data_directories["libraries"] = export_data.libraries_directory.trim_prefix(base_dir); + } + + for (const ExportData::ResourceData *dependency : initial_load_dependencies) { + Dictionary dependency_dict; + Array dependency_dependencies; + Array dependency_files; + + dependency_dict["files"] = dependency_files; + + for (const ExportData::ResourceData *dependency_dependency : dependency->dependencies) { + dependency_dependencies.push_back(dependency_dependency->path); + } + + if (dependency_dependencies.size() > 0) { + dependency_dict["dependencies"] = dependency_dependencies; + } + + if (dependency->native_file.exists) { + dependency_files.push_back(dependency->native_file.resource_path); + } + if (dependency->remap_file.exists) { + dependency_files.push_back(dependency->remap_file.resource_path); + } + if (dependency->remapped_file.exists) { + dependency_files.push_back(dependency->remapped_file.resource_path); + } + + async_pck_data_initial_load[dependency->path] = dependency_dict; + } + + for (ExportData::ResourceData &dependency : export_data.dependencies) { + if (dependency.native_file.exists) { + file_sizes[dependency.native_file.absolute_path.trim_prefix(base_dir)] = dependency.native_file.size; + } + if (dependency.remap_file.exists) { + file_sizes[dependency.remap_file.absolute_path.trim_prefix(base_dir)] = dependency.remap_file.size; + } + if (dependency.remapped_file.exists) { + file_sizes[dependency.remapped_file.absolute_path.trim_prefix(base_dir)] = dependency.remapped_file.size; + } + } + } + } break; + + default: { + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR(R"*(Invalid `async/initial_load_mode` value: %s)*"), async_initial_load_mode)); + return ERR_INVALID_PARAMETER; + } break; } // Extract templates. @@ -544,13 +1243,7 @@ Error EditorExportPlatformWeb::export_project(const Ref &p_p return error; } - // Parse generated file sizes (pck and wasm, to help show a meaningful loading bar). - Dictionary file_sizes; - Ref f = FileAccess::open(pck_path, FileAccess::READ); - if (f.is_valid()) { - file_sizes[pck_path.get_file()] = (uint64_t)f->get_length(); - } - f = FileAccess::open(base_path + ".wasm", FileAccess::READ); + Ref f = FileAccess::open(base_path + ".wasm", FileAccess::READ); if (f.is_valid()) { file_sizes[base_name + ".wasm"] = (uint64_t)f->get_length(); } @@ -560,7 +1253,7 @@ Error EditorExportPlatformWeb::export_project(const Ref &p_p Vector html; f = FileAccess::open(html_path, FileAccess::READ); if (f.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not read HTML shell: \"%s\"."), html_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not read HTML shell: \"%s\"."), html_path)); return ERR_FILE_CANT_READ; } html.resize(f->get_length()); @@ -568,8 +1261,8 @@ Error EditorExportPlatformWeb::export_project(const Ref &p_p f.unref(); // close file. // Generate HTML file with replaced strings. - _fix_html(html, p_preset, base_name, p_debug, p_flags, shared_objects, file_sizes); - Error err = _write_or_error(html.ptr(), html.size(), p_path); + _fix_html(html, p_preset, base_name, p_debug, p_flags, shared_objects, file_sizes, async_pck_data); + Error err = _write_or_error(html.ptr(), html.size(), path); if (err != OK) { // Message is supplied by the subroutine method. return err; @@ -580,7 +1273,7 @@ Error EditorExportPlatformWeb::export_project(const Ref &p_p Ref splash = _get_project_splash(p_preset); const String splash_png_path = base_path + ".png"; if (splash->save_png(splash_png_path) != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), splash_png_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), splash_png_path)); return ERR_FILE_CANT_WRITE; } @@ -590,20 +1283,20 @@ Error EditorExportPlatformWeb::export_project(const Ref &p_p Ref favicon = _get_project_icon(p_preset); const String favicon_png_path = base_path + ".icon.png"; if (favicon->save_png(favicon_png_path) != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), favicon_png_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), favicon_png_path)); return ERR_FILE_CANT_WRITE; } favicon->resize(180, 180); const String apple_icon_png_path = base_path + ".apple-touch-icon.png"; if (favicon->save_png(apple_icon_png_path) != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), apple_icon_png_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), apple_icon_png_path)); return ERR_FILE_CANT_WRITE; } } // Generate the PWA worker and manifest if (pwa) { - err = _build_pwa(p_preset, p_path, shared_objects); + err = _build_pwa(p_preset, path, shared_objects); if (err != OK) { // Message is supplied by the subroutine method. return err; @@ -613,6 +1306,54 @@ Error EditorExportPlatformWeb::export_project(const Ref &p_p return OK; } +void EditorExportPlatformWeb::_add_resource_data_tree_message(LocalVector &p_resource_data_entries, const String &p_context, bool p_sort_with_file_no_case_comparator, bool p_sort_with_size_comparator) { + if (p_sort_with_file_no_case_comparator) { + p_resource_data_entries.sort_custom(); + } + if (p_sort_with_size_comparator) { + p_resource_data_entries.sort_custom(); + } + + StringBuilder log_entry_builder; + const String new_line_char = "\n"; + log_entry_builder.append(vformat("%s\n", p_context)); + + uint64_t initial_load_assets_size = p_resource_data_entries.size(); + for (uint64_t i = 0; i < initial_load_assets_size; i++) { + const ExportData::ResourceData *initial_load_asset = p_resource_data_entries[i]; + + uint64_t asset_size = 0; + if (initial_load_asset->remap_file.exists) { + asset_size += initial_load_asset->remap_file.size; + asset_size += initial_load_asset->remapped_file.size; + } else if (initial_load_asset->native_file.exists) { + asset_size += initial_load_asset->native_file.size; + } else { + ERR_FAIL(); + } + + String fork_char = i < initial_load_assets_size - 1 + ? U"├" + : U"â””"; + String parent_tree_line = i < initial_load_assets_size - 1 + ? U"|" + : U" "; + + log_entry_builder.append(vformat(UR"*(%s── 📦 "%s" [%s]%s)*", fork_char, initial_load_asset->path, String::humanize_size(asset_size), new_line_char)); + + if (initial_load_asset->remap_file.exists) { + log_entry_builder.append(vformat(UR"*(%s ├ 📤 "%s" [%s]%s)*", parent_tree_line, initial_load_asset->remap_file.resource_path, String::humanize_size(initial_load_asset->remap_file.size), new_line_char)); + log_entry_builder.append(vformat(UR"*(%s â”” 📤 "%s" [%s]%s)*", parent_tree_line, initial_load_asset->remapped_file.resource_path, String::humanize_size(initial_load_asset->remapped_file.size), new_line_char)); + } else if (initial_load_asset->native_file.exists) { + log_entry_builder.append(vformat(UR"*(%s â”” 📤 "%s" [%s]%s)*", parent_tree_line, initial_load_asset->native_file.resource_path, String::humanize_size(initial_load_asset->native_file.size), new_line_char)); + } + } + + log_entry_builder.append("\n====================\n"); + + add_message(EditorExportPlatformData::EXPORT_MESSAGE_INFO, TTR("Initial load"), log_entry_builder.as_string()); +} + bool EditorExportPlatformWeb::poll_export() { Ref preset; @@ -793,7 +1534,7 @@ Error EditorExportPlatformWeb::run(const Ref &p_preset, int switch (p_option) { // Run in Browser. case 0: { - Error err = _export_project(p_preset, p_debug_flags); + Error err = _run_export_project(p_preset, p_debug_flags); if (err != OK) { return err; } @@ -806,7 +1547,7 @@ Error EditorExportPlatformWeb::run(const Ref &p_preset, int // Start HTTP Server. case 1: { - Error err = _export_project(p_preset, p_debug_flags); + Error err = _run_export_project(p_preset, p_debug_flags); if (err != OK) { return err; } @@ -823,7 +1564,7 @@ Error EditorExportPlatformWeb::run(const Ref &p_preset, int switch (p_option) { // Run in Browser. case 0: { - Error err = _export_project(p_preset, p_debug_flags); + Error err = _run_export_project(p_preset, p_debug_flags); if (err != OK) { return err; } @@ -832,7 +1573,7 @@ Error EditorExportPlatformWeb::run(const Ref &p_preset, int // Re-export Project. case 1: { - return _export_project(p_preset, p_debug_flags); + return _run_export_project(p_preset, p_debug_flags); } break; // Stop HTTP Server. @@ -850,13 +1591,13 @@ Error EditorExportPlatformWeb::run(const Ref &p_preset, int return FAILED; } -Error EditorExportPlatformWeb::_export_project(const Ref &p_preset, int p_debug_flags) { +Error EditorExportPlatformWeb::_run_export_project(const Ref &p_preset, int p_debug_flags) { const String dest = EditorPaths::get_singleton()->get_temp_dir().path_join("web"); Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); if (!da->dir_exists(dest)) { Error err = da->make_dir_recursive(dest); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Could not create HTTP server directory: %s."), dest)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Could not create HTTP server directory: %s."), dest)); return err; } } @@ -871,7 +1612,7 @@ Error EditorExportPlatformWeb::_export_project(const Ref &p_ DirAccess::remove_file_or_error(basepath + ".audio.worklet.js"); DirAccess::remove_file_or_error(basepath + ".audio.position.worklet.js"); DirAccess::remove_file_or_error(basepath + ".service.worker.js"); - DirAccess::remove_file_or_error(basepath + ".pck"); + DirAccess::remove_file_or_error(basepath + ".asyncpck"); DirAccess::remove_file_or_error(basepath + ".png"); DirAccess::remove_file_or_error(basepath + ".side.wasm"); DirAccess::remove_file_or_error(basepath + ".wasm"); @@ -904,7 +1645,7 @@ Error EditorExportPlatformWeb::_start_server(const String &p_bind_host, const ui server->stop(); Error err = server->listen(p_bind_port, bind_ip, p_use_tls, tls_key, tls_cert); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Error starting HTTP server: %d."), err)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Error starting HTTP server: %d."), err)); } return err; } @@ -914,33 +1655,124 @@ Error EditorExportPlatformWeb::_stop_server() { return OK; } -Ref EditorExportPlatformWeb::get_run_icon() const { - return run_icon; +Error EditorExportPlatformWeb::_rename_and_store_file_in_async_pck(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta) { + ExportData *export_data = static_cast(p_userdata); + const String simplified_path = EditorExportPlatform::simplify_path(p_path); + + Vector encoded_data; + EditorExportPlatformData::SavedData saved_data; + Error err = EditorExportPlatformUtils::store_temp_file(simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, p_delta, encoded_data, saved_data); + if (err != OK) { + return err; + } + + const String target_path = export_data->assets_directory.path_join(simplified_path.trim_prefix("res://")); + export_data->exported_files.insert(target_path); + err = EditorExportPlatformUtils::store_file_at_path(target_path, encoded_data); + + export_data->pack_data.file_ofs.push_back(saved_data); + + return OK; } -void EditorExportPlatformWeb::initialize() { - if (EditorNode::get_singleton()) { - server.instantiate(); +HashSet EditorExportPlatformWeb::_get_mandatory_initial_load_files(const Ref &p_preset) { + HashSet mandatory_initial_load_files; - Ref img = memnew(Image); - const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); + { + // Main scene. + mandatory_initial_load_files.insert( + EditorExportPlatformUtils::get_path_from_dependency( + EditorExportPlatformUtils::get_project_setting(p_preset, "application/run/main_scene"))); + } - ImageLoaderSVG::create_image_from_string(img, _web_logo_svg, EDSCALE, upsample, false); - logo = ImageTexture::create_from_image(img); + { + // Translation files. + PackedStringArray translations = EditorExportPlatformUtils::get_project_setting(p_preset, "internationalization/locale/translations"); + for (const String &translation : translations) { + mandatory_initial_load_files.insert(EditorExportPlatformUtils::get_path_from_dependency(translation)); + } + } - ImageLoaderSVG::create_image_from_string(img, _web_run_icon_svg, EDSCALE, upsample, false); - run_icon = ImageTexture::create_from_image(img); + { + // Autoload files. + HashMap autoload_list = ProjectSettings::get_singleton()->get_autoload_list(); + for (const KeyValue &key_value : autoload_list) { + mandatory_initial_load_files.insert( + EditorExportPlatformUtils::get_path_from_dependency(key_value.value.path)); + } + } - Ref theme = EditorNode::get_singleton()->get_editor_theme(); - if (theme.is_valid()) { - stop_icon = theme->get_icon(SNAME("Stop"), EditorStringName(EditorIcons)); - restart_icon = theme->get_icon(SNAME("Reload"), EditorStringName(EditorIcons)); - } else { - stop_icon.instantiate(); - restart_icon.instantiate(); + { + // Global class files. + LocalVector global_classes; + ScriptServer::get_global_class_list(global_classes); + + for (const StringName &global_class : global_classes) { + String global_class_path = ScriptServer::get_global_class_path(global_class); + mandatory_initial_load_files.insert( + EditorExportPlatformUtils::get_path_from_dependency(global_class_path)); } } + + { + // Single files. + auto _l_add_project_setting_if_file_exists = [&](const String &l_project_setting) -> void { + const String project_setting_file = ResourceUID::ensure_path(EditorExportPlatformUtils::get_project_setting(p_preset, l_project_setting)); + String path = EditorExportPlatformUtils::get_path_from_dependency(project_setting_file); + if (FileAccess::exists(path)) { + mandatory_initial_load_files.insert(path); + } + }; + + // Icon path. + _l_add_project_setting_if_file_exists("application/config/icon"); + // Default bus layout path. + _l_add_project_setting_if_file_exists("audio/buses/default_bus_layout"); + // Certificate bundle override. + _l_add_project_setting_if_file_exists("network/tls/certificate_bundle_override"); + // Default environment. + _l_add_project_setting_if_file_exists("rendering/environment/defaults/default_environment"); + // Default XR action map. + _l_add_project_setting_if_file_exists("xr/openxr/default_action_map"); + } + + { + // Export-related files. + mandatory_initial_load_files.insert(PATH_PROJECT_BINARY); + mandatory_initial_load_files.insert(PATH_ASSETS_SPARSEPCK); + mandatory_initial_load_files.insert(PATH_GODOT_UID_CACHE); + mandatory_initial_load_files.insert(PATH_GODOT_GLOBAL_SCRIPT_CLASS_CACHE); + } + + return mandatory_initial_load_files; +} + +Ref EditorExportPlatformWeb::get_run_icon() const { + return run_icon; } -EditorExportPlatformWeb::~EditorExportPlatformWeb() { +void EditorExportPlatformWeb::initialize() { + if (!EditorNode::get_singleton()) { + return; + } + + server.instantiate(); + + Ref img = memnew(Image); + const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); + + ImageLoaderSVG::create_image_from_string(img, _web_logo_svg, EDSCALE, upsample, false); + logo = ImageTexture::create_from_image(img); + + ImageLoaderSVG::create_image_from_string(img, _web_run_icon_svg, EDSCALE, upsample, false); + run_icon = ImageTexture::create_from_image(img); + + Ref theme = EditorNode::get_singleton()->get_editor_theme(); + if (theme.is_valid()) { + stop_icon = theme->get_icon(SNAME("Stop"), EditorStringName(EditorIcons)); + restart_icon = theme->get_icon(SNAME("Reload"), EditorStringName(EditorIcons)); + } else { + stop_icon.instantiate(); + restart_icon.instantiate(); + } } diff --git a/platform/web/export/export_plugin.h b/platform/web/export/export_plugin.h index 2f8efcd240cf..7d99bc9f197c 100644 --- a/platform/web/export/export_plugin.h +++ b/platform/web/export/export_plugin.h @@ -30,13 +30,10 @@ #pragma once +#include "core/io/file_access.h" +#include "editor/export/editor_export_preset.h" #include "editor_http_server.h" -#include "core/config/project_settings.h" -#include "core/io/image_loader.h" -#include "core/io/stream_peer_tls.h" -#include "core/io/tcp_server.h" -#include "core/io/zip_io.h" #include "editor/editor_node.h" #include "editor/editor_string_names.h" #include "editor/export/editor_export_platform.h" @@ -47,12 +44,98 @@ class ImageTexture; class EditorExportPlatformWeb : public EditorExportPlatform { GDCLASS(EditorExportPlatformWeb, EditorExportPlatform); + static inline const String PREFIX_RES = "res://"; + static inline const String SUFFIX_IMPORT = ".import"; + static inline const String SUFFIX_REMAP = ".remap"; + + static inline const String PATH_PROJECT_BINARY = "res://project.binary"; + static inline const String PATH_ASSETS_SPARSEPCK = "res://assets.sparsepck"; + static inline const String PATH_GODOT_UID_CACHE = "res://.godot/uid_cache.bin"; + static inline const String PATH_GODOT_GLOBAL_SCRIPT_CLASS_CACHE = "res://.godot/global_script_class_cache.cfg"; + enum RemoteDebugState { REMOTE_DEBUG_STATE_UNAVAILABLE, REMOTE_DEBUG_STATE_AVAILABLE, REMOTE_DEBUG_STATE_SERVING, }; + enum AsyncLoadSetting { + ASYNC_LOAD_SETTING_LOAD_EVERYTHING = 0, + ASYNC_LOAD_SETTING_MINIMUM_INITIAL_RESOURCES = 1, + }; + + struct ExportData { + struct File { + bool exists = false; + String resource_path; + String absolute_path; + uint32_t size = 0; + String md5; + String sha256; + + Dictionary get_as_dictionary() const { + Dictionary data; + data["size"] = size; + // data["md5"] = md5; + // data["sha256"] = sha256; + return data; + } + }; + + struct ResourceData { + struct SizeComparator { + _ALWAYS_INLINE_ bool operator()(const ExportData::ResourceData *p_a, const ExportData::ResourceData *p_b) const { + return p_a->get_size() < p_b->get_size(); + } + }; + struct FileNoCaseComparator { + _ALWAYS_INLINE_ bool operator()(const ExportData::ResourceData *p_a, const ExportData::ResourceData *p_b) const { + return ::FileNoCaseComparator()(p_a->path, p_b->path); + } + }; + + String path; + File native_file; + File remap_file; + File remapped_file; + LocalVector dependencies; + + uint32_t get_size() const; + Dictionary get_as_resource_dictionary() const; + String get_resource_path() const; + void flatten_dependencies(LocalVector *p_deps) const; + }; + + List dependencies; + HashMap dependencies_map; + EditorExportPlatformData::PackData pack_data; + String assets_directory; + String libraries_directory; + bool debug; + LocalVector libraries; + Ref preset; + + HashSet exported_files; + + HashSet get_features_set() const; + String res_to_global(const String &p_res_path) const { + ERR_FAIL_COND_V_MSG(!p_res_path.begins_with(PREFIX_RES), String(), vformat(R"*(Cannot convert non-resource path ("%s") to global.)*", p_res_path)); + String res_path = simplify_path(p_res_path); + return assets_directory.path_join(res_path.trim_prefix(PREFIX_RES)); + } + String global_to_res(const String &p_global_path) const { + return "res://" + p_global_path.trim_prefix(assets_directory.trim_suffix("/") + "/"); + } + String global_to_local(const String &p_global_path) const { + return p_global_path.trim_prefix(assets_directory.get_base_dir()); + } + + ResourceData *add_dependency(const String &p_path, const HashSet &p_features_set, Ref p_uid_cache, /* bool p_encrypt = false, */ Error *r_error = nullptr); + void update_file(File *p_file, const String &p_resource_path); + Dictionary get_deps_json_dictionary(const ResourceData *p_dependency); + Error save_deps_json(const ResourceData *p_dependency); + }; + Ref logo; Ref run_icon; Ref stop_icon; @@ -107,16 +190,22 @@ class EditorExportPlatformWeb : public EditorExportPlatform { Error _extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa); void _replace_strings(const HashMap &p_replaces, Vector &r_template); - void _fix_html(Vector &p_html, const Ref &p_preset, const String &p_name, bool p_debug, BitField p_flags, const Vector p_shared_objects, const Dictionary &p_file_sizes); + void _fix_html(Vector &p_html, const Ref &p_preset, const String &p_name, bool p_debug, BitField p_flags, const Vector p_shared_objects, const Dictionary &p_file_sizes, const Dictionary &p_async_pck_data_jsondeps_json); + Error _add_manifest_icon(const Ref &p_preset, const String &p_path, const String &p_icon, int p_size, Array &r_arr); Error _build_pwa(const Ref &p_preset, const String p_path, const Vector &p_shared_objects); Error _write_or_error(const uint8_t *p_content, int p_len, String p_path); - Error _export_project(const Ref &p_preset, int p_debug_flags); + Error _run_export_project(const Ref &p_preset, int p_debug_flags); Error _launch_browser(const String &p_bind_host, uint16_t p_bind_port, bool p_use_tls); Error _start_server(const String &p_bind_host, uint16_t p_bind_port, bool p_use_tls); Error _stop_server(); + void _add_resource_data_tree_message(LocalVector &p_resource_data_entries, const String &p_context, bool p_sort_with_file_no_case_comparator = false, bool p_sort_with_size_comparator = false); + + static HashSet _get_mandatory_initial_load_files(const Ref &p_preset); + static Error _rename_and_store_file_in_async_pck(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const PackedByteArray &p_key, uint64_t p_seed, bool p_delta); + public: virtual void get_preset_features(const Ref &p_preset, List *r_features) const override; @@ -151,5 +240,4 @@ class EditorExportPlatformWeb : public EditorExportPlatform { String get_debug_protocol() const override { return "ws://"; } virtual void initialize() override; - ~EditorExportPlatformWeb(); }; diff --git a/platform/web/godot_js.h b/platform/web/godot_js.h index 29b291d566a4..cdd0e2ca6893 100644 --- a/platform/web/godot_js.h +++ b/platform/web/godot_js.h @@ -38,14 +38,23 @@ extern "C" { #endif +typedef struct { + uint32_t length; + char *data; +} godot_js_string; + // Emscripten extern char *godot_js_emscripten_get_version(); -// Config +// Runtime. +typedef char *(*godot_js_runtime_get_config_file_as_json_callback)(const char *p_config_file_data); +extern void godot_js_runtime_set_get_config_file_as_json_cb(godot_js_runtime_get_config_file_as_json_callback p_callback); + +// Config. extern void godot_js_config_locale_get(char *p_ptr, int p_ptr_max); extern void godot_js_config_canvas_id_get(char *p_ptr, int p_ptr_max); -// OS +// OS. extern void godot_js_os_finish_async(void (*p_callback)()); extern void godot_js_os_request_quit_cb(void (*p_callback)()); extern int godot_js_os_fs_is_persistent(); @@ -55,10 +64,12 @@ extern void godot_js_os_shell_open(const char *p_uri); extern int godot_js_os_hw_concurrency_get(); extern int godot_js_os_thread_pool_size_get(); extern int godot_js_os_has_feature(const char *p_ftr); +extern int godot_js_os_asyncpck_install_file(const char *p_pck_dir, const char *p_path); +extern char *godot_js_os_asyncpck_install_file_get_status(const char *p_pck_dir, const char *p_path, const int32_t *p_return_string_length); extern int godot_js_pwa_cb(void (*p_callback)()); extern int godot_js_pwa_update(); -// Input +// Input. extern void godot_js_input_mouse_button_cb(int (*p_callback)(int p_pressed, int p_button, double p_x, double p_y, int p_modifiers)); extern void godot_js_input_mouse_move_cb(void (*p_callback)(double p_x, double p_y, double p_rel_x, double p_rel_y, int p_modifiers, double p_pressure)); extern void godot_js_input_mouse_wheel_cb(int (*p_callback)(int p_delta_mode, double p_delta_x, double p_delta_y)); @@ -71,7 +82,7 @@ extern void godot_js_set_ime_position(int p_x, int p_y); extern void godot_js_set_ime_cb(void (*p_input)(int p_type, const char *p_text), void (*p_callback)(int p_type, int p_repeat, int p_modifiers), char r_code[32], char r_key[32]); extern int godot_js_is_ime_focused(); -// Input gamepad +// Input gamepad. extern void godot_js_input_gamepad_cb(void (*p_on_change)(int p_index, int p_connected, const char *p_id, const char *p_guid)); extern int godot_js_input_gamepad_sample(); extern int godot_js_input_gamepad_sample_count(); @@ -79,7 +90,7 @@ extern int godot_js_input_gamepad_sample_get(int p_idx, float r_btns[16], int32_ extern void godot_js_input_paste_cb(void (*p_callback)(const char *p_text)); extern void godot_js_input_drop_files_cb(void (*p_callback)(const char **p_filev, int p_filec)); -// TTS +// TTS. extern int godot_js_tts_is_speaking(); extern int godot_js_tts_is_paused(); extern int godot_js_tts_get_voices(void (*p_callback)(int p_size, const char **p_voices)); @@ -88,7 +99,7 @@ extern void godot_js_tts_pause(); extern void godot_js_tts_resume(); extern void godot_js_tts_stop(); -// Display +// Display. extern int godot_js_display_screen_dpi_get(); extern double godot_js_display_pixel_ratio_get(); extern void godot_js_display_alert(const char *p_text); @@ -96,11 +107,11 @@ extern int godot_js_display_touchscreen_is_available(); extern int godot_js_display_is_swap_ok_cancel(); extern void godot_js_display_setup_canvas(int p_width, int p_height, int p_fullscreen, int p_hidpi); -// Display canvas +// Display canvas. extern void godot_js_display_canvas_focus(); extern int godot_js_display_canvas_is_focused(); -// Display window +// Display window. extern void godot_js_display_desired_size_set(int p_width, int p_height); extern int godot_js_display_size_update(); extern void godot_js_display_window_size_get(int32_t *p_x, int32_t *p_y); @@ -111,11 +122,11 @@ extern void godot_js_display_window_title_set(const char *p_text); extern void godot_js_display_window_icon_set(const uint8_t *p_ptr, int p_len); extern int godot_js_display_has_webgl(int p_version); -// Display clipboard +// Display clipboard. extern int godot_js_display_clipboard_set(const char *p_text); extern int godot_js_display_clipboard_get(void (*p_callback)(const char *p_text)); -// Display cursor +// Display cursor. extern void godot_js_display_cursor_set_shape(const char *p_cursor); extern int godot_js_display_cursor_is_hidden(); extern void godot_js_display_cursor_set_custom_shape(const char *p_shape, const uint8_t *p_ptr, int p_len, int p_hotspot_x, int p_hotspot_y); @@ -123,12 +134,12 @@ extern void godot_js_display_cursor_set_visible(int p_visible); extern void godot_js_display_cursor_lock_set(int p_lock); extern int godot_js_display_cursor_is_locked(); -// Display listeners +// Display listeners. extern void godot_js_display_fullscreen_cb(void (*p_callback)(int p_fullscreen)); extern void godot_js_display_window_blur_cb(void (*p_callback)()); extern void godot_js_display_notification_cb(void (*p_callback)(int p_notification), int p_enter, int p_exit, int p_in, int p_out); -// Display Virtual Keyboard +// Display Virtual Keyboard. extern int godot_js_display_vk_available(); extern int godot_js_display_tts_available(); extern void godot_js_display_vk_cb(void (*p_input)(const char *p_text, int p_cursor)); diff --git a/platform/web/js/engine/config.js b/platform/web/js/engine/config.js index d867376e4151..edda832109c5 100644 --- a/platform/web/js/engine/config.js +++ b/platform/web/js/engine/config.js @@ -52,6 +52,14 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused- * @type {?string} */ mainPack: null, + /** + * Main scene to preload. + * + * @memberof EngineConfig + * @default + * @type {?Object} + */ + asyncPckData: null, /** * Specify a language code to select the proper localization for the game. * @@ -271,6 +279,7 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused- this.fileSizes = parse('fileSizes', this.fileSizes); this.emscriptenPoolSize = parse('emscriptenPoolSize', this.emscriptenPoolSize); this.godotPoolSize = parse('godotPoolSize', this.godotPoolSize); + this.asyncPckData = parse('asyncPckData', this.asyncPckData); this.args = parse('args', this.args); this.onExecute = parse('onExecute', this.onExecute); this.onExit = parse('onExit', this.onExit); @@ -360,6 +369,9 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused- return { 'canvas': this.canvas, 'canvasResizePolicy': this.canvasResizePolicy, + 'mainPack': this.mainPack, + 'asyncPckData': this.asyncPckData, + 'fileSizes': this.fileSizes, 'locale': locale, 'persistentDrops': this.persistentDrops, 'virtualKeyboard': this.experimentalVK, diff --git a/platform/web/js/engine/engine.js b/platform/web/js/engine/engine.js index 1aeeb62f18de..f6d2c6d4fa52 100644 --- a/platform/web/js/engine/engine.js +++ b/platform/web/js/engine/engine.js @@ -78,36 +78,38 @@ const Engine = (function () { if (initPromise) { return initPromise; } + + preloader.init({ + fileSizes: this.config.fileSizes, + }); + if (loadPromise == null) { if (!basePath) { - initPromise = Promise.reject(new Error('A base path must be provided when calling `init` and the engine is not loaded.')); + const initPromiseError = new Error('A base path must be provided when calling `init` and the engine is not loaded.'); + initPromise = Promise.reject(initPromiseError); return initPromise; } + Engine.load(basePath, this.config.fileSizes[`${basePath}.wasm`]); } - const me = this; - function doInit(promise) { - // Care! Promise chaining is bogus with old emscripten versions. - // This caused a regression with the Mono build (which uses an older emscripten version). - // Make sure to test that when refactoring. - return new Promise(function (resolve, reject) { - promise.then(function (response) { - const cloned = new Response(response.clone().body, { 'headers': [['content-type', 'application/wasm']] }); - Godot(me.config.getModuleConfig(loadPath, cloned)).then(function (module) { - const paths = me.config.persistentPaths; - module['initFS'](paths).then(function (err) { - me.rtenv = module; - if (me.config.unloadAfterInit) { - Engine.unload(); - } - resolve(); - }); - }); - }); - }); - } + + const doInit = async () => { + const loadResponse = await loadPromise; + const clonedResponse = new Response(loadResponse.clone().body, { 'headers': [['content-type', 'application/wasm']] }); + const module = await Godot(this.config.getModuleConfig(loadPath, clonedResponse)); + const paths = this.config.persistentPaths; + const err = await module['initFS'](paths); + if (err != null) { + window['console'].error('Error while initializing Godot:', err); + } + this.rtenv = module; + if (this.config.unloadAfterInit) { + Engine.unload(); + } + }; + preloader.setProgressFunc(this.config.onProgress); - initPromise = doInit(loadPromise); + initPromise = doInit(); return initPromise; }, @@ -140,44 +142,55 @@ const Engine = (function () { * * Fails if a canvas cannot be found on the page, or not specified in the configuration. * + * @async * @param {EngineConfig} override An optional configuration override. - * @return {Promise} Promise that resolves once the engine started. + * @return {void} */ - start: function (override) { + start: async function (override) { this.config.update(override); const me = this; - return me.init().then(function () { - if (!me.rtenv) { - return Promise.reject(new Error('The engine must be initialized before it can be started')); - } + await me.init(); - let config = {}; - try { - config = me.config.getGodotConfig(function () { - me.rtenv = null; - }); - } catch (e) { - return Promise.reject(e); - } - // Godot configuration. - me.rtenv['initConfig'](config); + if (!me.rtenv) { + throw new Error('The engine must be initialized before it can be started'); + } - // Preload GDExtension libraries. - if (me.config.gdextensionLibs.length > 0 && !me.rtenv['loadDynamicLibrary']) { - return Promise.reject(new Error('GDExtension libraries are not supported by this engine version. ' - + 'Enable "Extensions Support" for your export preset and/or build your custom template with "dlink_enabled=yes".')); - } - return new Promise(function (resolve, reject) { - for (const file of preloader.preloadedFiles) { - me.rtenv['copyToFS'](file.path, file.buffer); - } - preloader.preloadedFiles.length = 0; // Clear memory - me.rtenv['callMain'](me.config.args); - initPromise = null; - me.installServiceWorker(); - resolve(); + let config = {}; + try { + config = me.config.getGodotConfig(function () { + me.rtenv = null; }); - }); + } catch (err) { + const newErr = new Error('Error geeting Godot config.'); + newErr.cause = err; + throw newErr; + } + + // Godot configuration. + me.rtenv['initConfig'](config); + await me.rtenv['initOS'](); + + // Preload GDExtension libraries. + if (me.config.gdextensionLibs.length > 0 && !me.rtenv['loadDynamicLibrary']) { + throw new Error( + 'GDExtension libraries are not supported by this engine version. ' + + 'Enable "Extensions Support" for your export preset and/or build your custom template with "dlink_enabled=yes".' + ); + } + + try { + for (const file of preloader.preloadedFiles) { + me.rtenv['copyToFS'](file.path, file.buffer); + } + preloader.preloadedFiles.length = 0; // Clear memory + me.rtenv['callMain'](me.config.args); + initPromise = null; + me.installServiceWorker(); + } catch (err) { + const newErr = new Error('Error while initializing.'); + newErr.cause = err; + throw newErr; + } }, /** @@ -193,20 +206,60 @@ const Engine = (function () { * @param {EngineConfig} override An optional configuration override. * @return {Promise} Promise that resolves once the game started. */ - startGame: function (override) { + startGame: async function (override) { this.config.update(override); + this.insertImportMap(); + // Add main-pack argument. const exe = this.config.executable; - const pack = this.config.mainPack || `${exe}.pck`; + let pack = this.config.mainPack || `${exe}.pck`; + if (pack.endsWith('/')) { + pack = pack.substring(0, pack.length - 1); + } + this.config.args = ['--main-pack', pack].concat(this.config.args); // Start and init with execName as loadPath if not inited. const me = this; - return Promise.all([ - this.init(exe), - this.preloadFile(pack, pack), - ]).then(function () { - return me.start.apply(me); - }); + const filesToPreload = []; + + if (pack.endsWith('.asyncpck')) { + if (this.config.asyncPckData == null) { + throw new Error('No Main Scene dependencies found.'); + } + + const asyncPckData = this.config['asyncPckData']; + const asyncPckAssetsDir = asyncPckData['directories']['assets']; + + const asyncPckInitialLoadFilesSet = new Set(); + const asyncPckDataInitialLoad = asyncPckData['initialLoad']; + for (const value of Object.values(asyncPckDataInitialLoad)) { + for (const resourcePath of Object.values(value['files'])) { + asyncPckInitialLoadFilesSet.add(resourcePath); + } + } + + const asyncPckInitialLoadFiles = Array.from(asyncPckInitialLoadFilesSet); + + const resToLocal = (pPath) => { + const PREFIX_RES = 'res://'; + let path = pPath; + if (path.startsWith(PREFIX_RES)) { + path = path.substring('res://'.length); + } + return `${asyncPckAssetsDir}/${path}`; + }; + + for (const resourcePath of asyncPckInitialLoadFiles) { + const pathToPreload = resToLocal(resourcePath); + filesToPreload.push(this.preloadFile(pathToPreload, pathToPreload)); + } + } else { + filesToPreload.push(this.preloadFile(pack, pack)); + } + + await Promise.all([this.init(exe), ...filesToPreload]); + + return me.start.apply(me); }, /** @@ -249,6 +302,27 @@ const Engine = (function () { } return Promise.resolve(); }, + + /** + * Install the JavaScript module import map. + */ + insertImportMap() { + const IMPORTMAP_ID = 'godotengine-importmap-engine'; + if (document.getElementById(IMPORTMAP_ID)) { + return; + } + const scriptElement = document.createElement('script'); + scriptElement.id = IMPORTMAP_ID; + scriptElement.type = 'importmap'; + const scriptElementContent = { + imports: { + '@godotengine/utils/concurrencyQueueManager': `./${this.config.executable}.utils.concurrency.js`, + '@godotengine/utils/wait': `./${this.config.executable}.utils.wait.js`, + }, + }; + scriptElement.textContent = JSON.stringify(scriptElementContent, null, 2); + document.head.insertAdjacentElement('beforeend', scriptElement); + }, }; Engine.prototype = proto; diff --git a/platform/web/js/engine/preloader.js b/platform/web/js/engine/preloader.js index 564c68d26449..545a986a0d35 100644 --- a/platform/web/js/engine/preloader.js +++ b/platform/web/js/engine/preloader.js @@ -1,47 +1,70 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-unused-vars + const DOWNLOAD_ATTEMPTS_MAX = 4; + const loadingFiles = {}; + const lastProgress = { loaded: 0, total: 0 }; + let progressFunc = null; + let concurrencyQueueManager = null; + let filesSizeTotal = 0; + + this.preloadedFiles = []; + function getTrackedResponse(response, load_status) { - function onloadprogress(reader, controller) { - return reader.read().then(function (result) { - if (load_status.done) { - return Promise.resolve(); - } - if (result.value) { - controller.enqueue(result.value); - load_status.loaded += result.value.length; - } - if (!result.done) { - return onloadprogress(reader, controller); - } + async function onLoadProgress(reader, controller) { + const { done, value } = await reader.read(); + if (load_status.done) { + return Promise.resolve(); + } + if (done) { load_status.done = true; return Promise.resolve(); - }); + } + controller.enqueue(value); + load_status.loaded += value.byteLength; + return onLoadProgress(reader, controller); } + const reader = response.body.getReader(); return new Response(new ReadableStream({ - start: function (controller) { - onloadprogress(reader, controller).then(function () { + start: async function (controller) { + try { + await onLoadProgress(reader, controller); + } finally { controller.close(); - }); + } }, }), { headers: response.headers }); } - function loadFetch(file, tracker, fileSize, raw) { - tracker[file] = { - total: fileSize || 0, - loaded: 0, - done: false, - }; - return fetch(file).then(function (response) { + async function loadFetch(file, fileSize, raw) { + if (file in loadingFiles) { + loadingFiles[file].requested = true; + } else { + loadingFiles[file] = { + file, + total: fileSize || 0, + loaded: 0, + requested: true, + done: false, + }; + } + + try { + const response = await fetch(file); + if (!response.ok) { - return Promise.reject(new Error(`Failed loading file '${file}'`)); + throw new Error(`Got response ${response.status}: ${response.statusText}`); } - const tr = getTrackedResponse(response, tracker[file]); + const tr = getTrackedResponse(response, loadingFiles[file]); if (raw) { return Promise.resolve(tr); } + return tr.arrayBuffer(); - }); + } catch (error) { + const newError = new Error(`loadFetch for "${file}" failed:`); + newError.cause = error; + throw newError; + } } function retry(func, attempts = 1) { @@ -58,64 +81,79 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-un return func().catch(onerror); } - const DOWNLOAD_ATTEMPTS_MAX = 4; - const loadingFiles = {}; - const lastProgress = { loaded: 0, total: 0 }; - let progressFunc = null; - - const animateProgress = function () { + this.animateProgress = () => { let loaded = 0; let total = 0; - let totalIsValid = true; - let progressIsFinal = true; + const requestedFiles = Object.values(loadingFiles) + .filter((pLoadingFile) => pLoadingFile.requested); + let progressIsFinal = false; + if (requestedFiles.length > 0 && requestedFiles.every((pRequestedFile) => pRequestedFile.done)) { + progressIsFinal = true; + } - Object.keys(loadingFiles).forEach(function (file) { - const stat = loadingFiles[file]; - if (!stat.done) { - progressIsFinal = false; + // eslint-disable-next-line no-unused-vars + for (const [_file, status] of Object.entries(loadingFiles)) { + if (!status.requested) { + continue; } - if (!totalIsValid || stat.total === 0) { - totalIsValid = false; - total = 0; - } else { - total += stat.total; + + if (!status.done) { + progressIsFinal = false; } - loaded += stat.loaded; - }); - if (loaded !== lastProgress.loaded || total !== lastProgress.total) { + + total += status.total; + loaded += status.loaded; + } + + if (loaded !== lastProgress.loaded || (!filesSizeTotal && total !== lastProgress.total)) { lastProgress.loaded = loaded; lastProgress.total = total; + if (typeof progressFunc === 'function') { progressFunc(loaded, total); } } if (!progressIsFinal) { - requestAnimationFrame(animateProgress); + window.requestAnimationFrame(() => this.animateProgress()); } }; - this.animateProgress = animateProgress; - this.setProgressFunc = function (callback) { progressFunc = callback; }; - this.loadPromise = function (file, fileSize, raw = false) { - return retry(loadFetch.bind(null, file, loadingFiles, fileSize, raw), DOWNLOAD_ATTEMPTS_MAX); + this.loadPromise = async function (file, fileSize, raw = false) { + if (concurrencyQueueManager == null) { + const { ConcurrencyQueueManager } = await import('@godotengine/utils/concurrencyQueueManager'); + // Another `loadPromise()` could have ended while awaiting. + if (concurrencyQueueManager == null) { + concurrencyQueueManager = new ConcurrencyQueueManager(); + } + } + + try { + return await concurrencyQueueManager.queue(() => retry( + loadFetch.bind(null, file, fileSize, raw), + DOWNLOAD_ATTEMPTS_MAX + )); + } catch (error) { + const newError = new Error(`An error occurred while running \`Preloader.loadPromise("${file}", ${fileSize}, raw = ${raw})\``); + newError.cause = error; + throw error; + } }; - this.preloadedFiles = []; - this.preload = function (pathOrBuffer, destPath, fileSize) { + this.preload = async (pathOrBuffer, destPath, fileSize) => { let buffer = null; if (typeof pathOrBuffer === 'string') { const me = this; - return this.loadPromise(pathOrBuffer, fileSize).then(function (buf) { - me.preloadedFiles.push({ - path: destPath || pathOrBuffer, - buffer: buf, - }); - return Promise.resolve(); + const buf = await this.loadPromise(pathOrBuffer, fileSize); + me.preloadedFiles.push({ + path: destPath || pathOrBuffer, + buffer: buf, + fileSize, }); + return; } else if (pathOrBuffer instanceof ArrayBuffer) { buffer = new Uint8Array(pathOrBuffer); } else if (ArrayBuffer.isView(pathOrBuffer)) { @@ -125,9 +163,28 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-un this.preloadedFiles.push({ path: destPath, buffer: pathOrBuffer, + fileSize, }); - return Promise.resolve(); + return; + } + throw new Error('Invalid object for preloading'); + }; + + this.init = (pOptions = {}) => { + const { + fileSizes: loadingFileSizes = {}, + } = pOptions; + + for (const [file, fileSize] of Object.entries(loadingFileSizes)) { + loadingFiles[file] = { + file, + total: fileSize || 0, + loaded: 0, + requested: false, + done: false, + }; + filesSizeTotal += fileSize; } - return Promise.reject(new Error('Invalid object for preloading')); + filesSizeTotal = Object.values(loadingFileSizes).reduce((pAccumulator, pFileSize) => pAccumulator + pFileSize, 0); }; }; diff --git a/platform/web/js/libs/library_godot_os.js b/platform/web/js/libs/library_godot_os.js index 0e931b794e91..a689614ae6a9 100644 --- a/platform/web/js/libs/library_godot_os.js +++ b/platform/web/js/libs/library_godot_os.js @@ -64,6 +64,9 @@ const GodotConfig = { godot_pool_size: 4, on_execute: null, on_exit: null, + mainPack: '', + asyncPckData: null, + fileSizes: null, init_config: function (p_opts) { GodotConfig.canvas_resize_policy = p_opts['canvasResizePolicy']; @@ -74,6 +77,9 @@ const GodotConfig = { GodotConfig.godot_pool_size = p_opts['godotPoolSize']; GodotConfig.on_execute = p_opts['onExecute']; GodotConfig.on_exit = p_opts['onExit']; + GodotConfig.mainPack = p_opts['mainPack']; + GodotConfig.asyncPckData = p_opts['asyncPckData'] ?? null; + GodotConfig.fileSizes = p_opts['fileSizes'] ?? null; if (p_opts['focusCanvas']) { GodotConfig.canvas.focus(); } @@ -90,6 +96,9 @@ const GodotConfig = { GodotConfig.persistent_drops = false; GodotConfig.on_execute = null; GodotConfig.on_exit = null; + GodotConfig.mainPack = ''; + GodotConfig.asyncPckData = null; + GodotConfig.fileSizes = null; }, }, @@ -227,17 +236,555 @@ const GodotFS = { }; mergeInto(LibraryManager.library, GodotFS); -const GodotOS = { +class AsyncPCKFile { + static get Status() { + return Object.freeze({ + STATUS_ERROR: 'STATUS_ERROR', + STATUS_IDLE: 'STATUS_IDLE', + STATUS_LOADING: 'STATUS_LOADING', + STATUS_INSTALLED: 'STATUS_INSTALLED', + }); + } + + constructor(pAsyncPCKPath, pPath, pSize) { + this.asyncPCKPath = pAsyncPCKPath; + this.path = pPath; + { + const assetsDir = GodotOS.asyncPCKGetAsyncPCKAssetsDir(this.asyncPCKPath); + this.localPath = `${assetsDir}/${GodotOS._removeResPrefix(this.path)}`; + } + + this._status = GodotOS.AsyncPCKFile.Status.STATUS_IDLE; + this._installed = false; + this._size = pSize; + this._progress = 0; + this._progressRatio = 0; + this._loadPromise = null; + this._error = null; + } + + get status() { + return this._status; + } + + get size() { + return this._size; + } + + get progress() { + return this._progress; + } + + get progressRatio() { + return this._progressRatio; + } + + get error() { + return this._error; + } + + _addToProgress(pAddedBytes) { + this._setProgress(this._progress + pAddedBytes); + } + + _setProgress(pProgress) { + let progress = pProgress; + if (progress <= 0) { + progress = 0; + } + this._progress = progress; + if (this._progress > this._size) { + this._size = progress; + } + if (this._size > 0) { + this._progressRatio = this._size / progress; + } + } + + async load() { + if (this._loadPromise != null) { + return this._loadPromise; + } + + if (this._installed) { + GodotRuntime.print( + `AsyncPCKFile "${this.path}" (of AsyncPck "${this.asyncPCKPath}") is already installed, skipping loading.` + ); + return Promise.resolve(); + } + if (this._status == GodotOS.AsyncPCKFile.Status.STATUS_LOADING) { + GodotRuntime.print( + `AsyncPCKFile "${this.path}" (of AsyncPck "${this.asyncPCKPath}") is currently loading, skipping loading.` + ); + return Promise.resolve(); + } + this._status = GodotOS.AsyncPCKFile.Status.STATUS_LOADING; + + this._loadPromise = this._load(); + return await this._loadPromise; + } + + async _load() { + try { + const fileBuffer = await this._loadAttempt(); + + GodotFS.copy_to_fs(this.localPath, fileBuffer); + this._status = GodotOS.AsyncPCKFile.Status.STATUS_INSTALLED; + } catch (err) { + this._status = GodotOS.AsyncPCKFile.Status.STATUS_ERROR; + + const newError = new Error( + `AsyncPCKFile "${this.path}" (of AsyncPck "${this.asyncPCKPath}"): error while loading "${this.localPath}"` + ); + newError.cause = err; + this._error = newError; + throw newError; + } finally { + this._loadPromise = null; + } + } + + async _loadAttempt(pRetryCount = 0) { + try { + const fileResponse = await GodotOS.asyncPCKFetch(this.localPath); + if (!fileResponse.ok) { + this._status = GodotOS.AsyncPCKFile.Status.STATUS_ERROR; + throw new Error(`Couldn't load file "${this.localPath}".`); + } + + const chunks = []; + const reader = fileResponse.body.getReader(); + + while (true) { + const { done, value: chunk } = await reader.read(); // eslint-disable-line no-await-in-loop + if (done) { + break; + } + this._addToProgress(chunk.byteLength); + chunks.push(chunk); + } + + const fileBuffer = new Uint8Array(this._progress); + let filePosition = 0; + for (const chunk of chunks) { + fileBuffer.set(chunk, filePosition); + filePosition += chunk.byteLength; + } + + return fileBuffer; + } catch (err) { + const newError = new Error( + `AsyncPCKFile "${this.path}" (of AsyncPck "${this.asyncPCKPath}"): error while attempting to load "${this.localPath}". (attempt ${pRetryCount + 1}/${GodotOS._asyncPCKFetchMaxRetry})` + ); + newError.cause = err; + + if (pRetryCount == GodotOS._asyncPCKFetchMaxRetry) { + const maxRetryError = new Error(`Maximum retry count (${GodotOS._asyncPCKFetchMaxRetry}) reached.`); + maxRetryError.cause = newError; + throw maxRetryError; + } + + GodotRuntime.error(newError); + this._error = newError; + + // Exponent wait. + await GodotOS._wait(GodotOS._asyncPCKWaitTimeBaseMs * 2 ** pRetryCount, 'ms'); + return this._loadAttempt(pRetryCount + 1); + } + } + + flagAsInstalled() { + this._status = GodotOS.AsyncPCKFile.Status.STATUS_INSTALLED; + this._progress = this._size; + this._progressRatio = 1.0; + } + + getAsJsonObject() { + let error = ''; + if (this._error != null) { + error = this._error.message; + } + + return { + local_path: this.localPath, + status: this._status, + size: this._size, + progress: this._progress, + progress_ratio: this._progressRatio, + error, + }; + } +} + +class AsyncPCKResource { + static createAndInitialize(pAsyncPCK, pPath, pFiles, pDependencies = []) { + const asyncPCKResource = new GodotOS.AsyncPCKResource(pAsyncPCK, pPath); + asyncPCKResource.initialize(pFiles, pDependencies); + asyncPCKResource.insertInInstallMap(); + return asyncPCKResource; + } + + static create(pAsyncPCK, pPath) { + const asyncPCKResource = new GodotOS.AsyncPCKResource(pAsyncPCK, pPath); + asyncPCKResource.insertInInstallMap(); + return asyncPCKResource; + } + + constructor(pAsyncPCKPath, pPath) { + this.asyncPCKPath = pAsyncPCKPath; + this.path = pPath; + this.files = []; + this.dependencies = []; + + this._initialized = false; + this._loadPromise = null; + } + + initialize(pFiles, pDependencies = []) { + if (this._initialized) { + throw new Error(`Cannot initialize AsyncPCKResource more than once. ("${this.path}" of "${this.asyncPCKPath}")`); + } + this._initialized = true; + + this.dependencies = pDependencies; + + for (const [filePath, fileDefinition] of Object.entries(pFiles)) { + const asyncPCKFile = new GodotOS.AsyncPCKFile(this.asyncPCKPath, filePath, fileDefinition['size']); + this.files.push(asyncPCKFile); + } + } + + get initialized() { + return this._initialized; + } + + get size() { + return this.files.reduce((pAccumulatorValue, pFile) => pAccumulatorValue + pFile.size, 0); + } + + get progress() { + return this.files.reduce((pAccumulatorValue, pFile) => pAccumulatorValue + pFile.progress, 0); + } + + get progressRatio() { + const currentSize = this.size; + if (currentSize <= 0) { + return 0; + } + return this.progress / currentSize; + } + + get status() { + if (this.files.find(GodotOS.AsyncPCKResource.isStatusError) != null) { + return GodotOS.AsyncPCKFile.Status.STATUS_ERROR; + } + if (this.files.length > 0 && this.files.every(GodotOS.AsyncPCKResource.isStatusInstalled)) { + return GodotOS.AsyncPCKFile.Status.STATUS_INSTALLED; + } + if (this.files.find(GodotOS.AsyncPCKResource.isStatusLoading) != null) { + return GodotOS.AsyncPCKFile.Status.STATUS_LOADING; + } + return GodotOS.AsyncPCKFile.Status.STATUS_IDLE; + } + + get errors() { + return this.files + .filter(GodotOS.AsyncPCKResource.isStatusError) + .map((pFile) => pFile.error) + .filter((pFileError) => pFileError !== ''); + } + + get allDependencies() { + const dependenciesMap = new Map(); + for (const dependency of this.dependencies) { + const asyncPCKResource = GodotOS.asyncPCKGetAsyncPCKResource(this.asyncPCKPath, dependency); + if (asyncPCKResource == null) { + throw new Error( + `Cannot get dependencies of a non-resource ("${dependency}" of "${this.asyncPCKPath}")` + ); + } + asyncPCKResource._getAllDependencies(dependenciesMap); + } + return Object.fromEntries(dependenciesMap); + } + + _getAllDependencies(pDependenciesMap) { + if (pDependenciesMap.has(this.path)) { + return; + } + pDependenciesMap.set(this.path, this.getAsJsonObject({ withDependencies: false })); + for (const dependency of this.dependencies) { + const asyncPCKResource = GodotOS.asyncPCKGetAsyncPCKResource(this.asyncPCKPath, dependency); + if (asyncPCKResource == null) { + throw new Error( + `Cannot get dependencies of a non-resource ("${dependency}" of "${this.asyncPCKPath}")` + ); + } + asyncPCKResource._getAllDependencies(pDependenciesMap); + } + } + + get allDependenciesResources() { + const dependenciesResources = []; + const allDependencies = this.allDependencies; + for (const dependency of Object.keys(allDependencies)) { + const asyncPCKResource = GodotOS.asyncPCKGetAsyncPCKResource(this.asyncPCKPath, dependency); + if (asyncPCKResource == null) { + throw new Error( + `Cannot get dependencies of a non-resource ("${dependency}" of "${this.asyncPCKPath}")` + ); + } + dependenciesResources.push(asyncPCKResource); + } + return dependenciesResources; + } + + async load() { + if (this._loadPromise != null) { + return this._loadPromise; + } + if (this.status == GodotOS.AsyncPCKFile.Status.STATUS_INSTALLED) { + return Promise.resolve(); + } + this._loadPromise = this._load(); + return await this._loadPromise; + } + + async _load() { + try { + await Promise.allSettled( + this.files.map((pFile) => { + if (pFile.status == GodotOS.AsyncPCKFile.Status.STATUS_INSTALLED) { + return Promise.resolve(); + } + return pFile.load(pFile); + }) + ); + } catch (err) { + const newError = new Error( + `AsyncPCKResource "${this.path}" (of AsyncPCK "${this.asyncPCKPath}"): error while loading"` + ); + newError.cause = err; + throw newError; + } finally { + this._loadPromise = null; + } + } + + flagAsInstalled() { + for (const file of this.files) { + file.flagAsInstalled(); + } + } + + insertInInstallMap() { + if (!GodotOS._asyncPCKInstallMap.has(this.asyncPCKPath)) { + GodotOS._asyncPCKInstallMap.set(this.asyncPCKPath, new Map()); + } + if (GodotOS._asyncPCKInstallMap.get(this.asyncPCKPath).has(this.path)) { + const asyncPCKResource = GodotOS._asyncPCKInstallMap.get(this.asyncPCKPath).get(this.path); + if (!asyncPCKResource.isTemporary) { + throw new Error( + `AsyncPCKResource "${this.path}" (of AsyncPCK "${this.asyncPCKPath}"): cannot install over a non-temporary AsyncPCKResource"` + ); + } + } + GodotOS._asyncPCKInstallMap.get(this.asyncPCKPath).set(this.path, this); + } + + removeFromInstallMap() { + if (!GodotOS._asyncPCKInstallMap.has(this.asyncPCKPath)) { + return; + } + GodotOS._asyncPCKInstallMap.get(this.asyncPCKPath).delete(this.path); + } + + getAsJsonObject(pOptions = {}) { + const { withDependencies = true } = pOptions; + const jsonData = {}; + if (withDependencies) { + const dependenciesResources = this.allDependenciesResources; + jsonData['files'] = {}; + jsonData['files'][this.path] = this.getAsJsonObject({ withDependencies: false }); + for (const dependencyResource of dependenciesResources) { + jsonData['files'][dependencyResource.path] = dependencyResource.getAsJsonObject({ + withDependencies: false, + }); + } + + const jsonDataSize + = this.size + + dependenciesResources.reduce( + (pAccumulator, pDependencyResource) => pAccumulator + pDependencyResource.size, + 0 + ); + const jsonDataProgress + = this.progress + + dependenciesResources.reduce( + (pAccumulator, pDependencyResource) => pAccumulator + pDependencyResource.progress, + 0 + ); + let jsonDataProgressRatio = 0; + if (jsonDataSize > 0) { + jsonDataProgressRatio = jsonDataProgress / jsonDataSize; + } + + const jsonDataErrors = Object.assign( + {}, + (() => { + const thisInstanceErrors = this.errors; + if (thisInstanceErrors.length === 0) { + return {}; + } + return { [this.path]: thisInstanceErrors }; + })(), + ...dependenciesResources.map((pDependencyResource) => { + const errors = pDependencyResource.errors; + if (errors.length === 0) { + return {}; + } + return { + [pDependencyResource.path]: pDependencyResource.errors, + }; + }) + ); + + jsonData['size'] = jsonDataSize; + jsonData['progress'] = jsonDataProgress; + jsonData['progress_ratio'] = jsonDataProgressRatio; + jsonData['errors'] = jsonDataErrors; + + let status = this.status; + if ( + status == GodotOS.AsyncPCKFile.Status.STATUS_IDLE + || status == GodotOS.AsyncPCKFile.Status.STATUS_INSTALLED + ) { + if (dependenciesResources.find(GodotOS.AsyncPCKResource.isStatusError) != null) { + status = GodotOS.AsyncPCKFile.Status.STATUS_ERROR; + } + if (dependenciesResources.length > 0 && dependenciesResources.every(GodotOS.AsyncPCKResource.isStatusInstalled)) { + status = GodotOS.AsyncPCKFile.Status.STATUS_INSTALLED; + } + if (dependenciesResources.find(GodotOS.AsyncPCKResource.isStatusLoading) != null) { + status = GodotOS.AsyncPCKFile.Status.STATUS_LOADING; + } + } + jsonData['status'] = status; + } else { + jsonData['size'] = this.size; + jsonData['progress'] = this.progress; + jsonData['progress_ratio'] = this.progressRatio; + jsonData['status'] = this.status; + jsonData['errors'] = this.errors; + } + + return jsonData; + } + + static isStatus(pStatus, pFile) { + return pFile.status == pStatus; + } + + static isStatusError(pFile) { + return GodotOS.AsyncPCKResource.isStatus(GodotOS.AsyncPCKFile.Status.STATUS_ERROR, pFile); + } + + static isStatusLoading(pFile) { + return GodotOS.AsyncPCKResource.isStatus(GodotOS.AsyncPCKFile.Status.STATUS_LOADING, pFile); + } + + static isStatusInstalled(pFile) { + return GodotOS.AsyncPCKResource.isStatus(GodotOS.AsyncPCKFile.Status.STATUS_INSTALLED, pFile); + } +} + +const _GodotOS = { $GodotOS__deps: ['$GodotRuntime', '$GodotConfig', '$GodotFS'], $GodotOS__postset: [ + 'Module["initOS"] = async () => { await GodotOS.init(); };', 'Module["request_quit"] = function() { GodotOS.request_quit() };', 'Module["onExit"] = GodotOS.cleanup;', 'GodotOS._fs_sync_promise = Promise.resolve();', - ].join(''), + 'GodotOS._asyncPCKInstallMap = new Map();', + ].join(' '), $GodotOS: { request_quit: function () {}, _async_cbs: [], _fs_sync_promise: null, + AsyncPCKFile: AsyncPCKFile, + AsyncPCKResource: AsyncPCKResource, + _asyncPCKInstallMap: null, + _asyncPCKConcurrencyQueueManager: null, + _asyncPCKFetchMaxRetry: 5, + _asyncPCKWaitTimeBaseMs: 100, + _mainPack: '', + _prefixRes: 'res://', + + _trimLastSlash: function (pPath) { + if (pPath.endsWith('/')) { + return pPath.substring(0, pPath.length - 1); + } + return pPath; + }, + _addResPrefix: function (pPath) { + let path = pPath; + if (!path.startsWith(GodotOS._prefixRes)) { + path = GodotOS._prefixRes + path; + } + return path; + }, + _removeResPrefix: function (pPath) { + let path = pPath; + if (path.startsWith(GodotOS._prefixRes)) { + path = path.substring(GodotOS._prefixRes.length); + } + return path; + }, + + init: async function () { + const { wait } = await import('@godotengine/utils/wait'); + GodotOS._wait = wait; + + GodotOS._mainPack = GodotConfig.mainPack ?? ''; + if (GodotOS._mainPack.endsWith('.asyncpck')) { + const { ConcurrencyQueueManager } = await import('@godotengine/utils/concurrencyQueueManager'); + // eslint-disable-next-line require-atomic-updates -- We set `GodotOS._concurrencyQueueManager` only once: at init time. + GodotOS._asyncPCKConcurrencyQueueManager = new ConcurrencyQueueManager(); + GodotOS.initAsyncPck(); + } + }, + + initAsyncPck: function () { + const data = GodotConfig.asyncPckData; + const fileSizes = GodotConfig.fileSizes; + + const initialLoad = data['initialLoad']; + for (const [resourceKey, resourceValue] of Object.entries(initialLoad)) { + const dependencies = {}; + for (const resourceDependency of resourceValue['files']) { + let assetsPath = data.directories.assets; + if (!assetsPath.endsWith('/')) { + assetsPath += '/'; + } + const fileSizePath = assetsPath + resourceDependency.substring('res://'.length); + dependencies[resourceDependency] = { + size: fileSizes[fileSizePath], + }; + } + let asyncPckResource = GodotOS.asyncPCKGetAsyncPCKResource(GodotOS._mainPack, resourceKey); + if (asyncPckResource != null) { + continue; + } + + asyncPckResource = GodotOS.AsyncPCKResource.createAndInitialize( + GodotOS._mainPack, + resourceKey, + dependencies, + resourceValue?.dependencies ?? [] + ); + asyncPckResource.flagAsInstalled(); + } + }, atexit: function (p_promise_cb) { GodotOS._async_cbs.push(p_promise_cb); @@ -253,20 +800,116 @@ const GodotOS = { }, finish_async: function (callback) { - GodotOS._fs_sync_promise.then(function (err) { - const promises = []; - GodotOS._async_cbs.forEach(function (cb) { - promises.push(new Promise(cb)); + GodotOS._fs_sync_promise + .then(function (err) { + const promises = []; + GodotOS._async_cbs.forEach(function (cb) { + promises.push(new Promise(cb)); + }); + return Promise.all(promises); + }) + .then(function () { + return GodotFS.sync(); // Final FS sync. + }) + .then(function (err) { + // Always deferred. + setTimeout(function () { + callback(); + }, 0); }); - return Promise.all(promises); - }).then(function () { - return GodotFS.sync(); // Final FS sync. - }).then(function (err) { - // Always deferred. - setTimeout(function () { - callback(); - }, 0); + }, + + asyncPCKFetch: async function (...pArgs) { + if (GodotOS._asyncPCKConcurrencyQueueManager == null) { + throw new ReferenceError('`GodotOS._asyncPCKConcurrencyQueueManager` is null.'); + } + return await GodotOS._asyncPCKConcurrencyQueueManager.queue(() => fetch(...pArgs)); + }, + + asyncPCKGetAsyncPCKAssetsDir: function (pPckDir) { + let pckDir = GodotOS._trimLastSlash(pPckDir); + if (pckDir.endsWith('.asyncpck')) { + pckDir = `${pckDir}/assets`; + } + return pckDir; + }, + + asyncPCKGetAsyncPCKResource: function (pPckDir, pPath) { + if (!GodotOS._asyncPCKInstallMap.has(pPckDir)) { + return null; + } + const path = GodotOS._addResPrefix(pPath); + if (!GodotOS._asyncPCKInstallMap.get(pPckDir).has(path)) { + return null; + } + return GodotOS._asyncPCKInstallMap.get(pPckDir).get(path); + }, + + asyncPCKInstallFile: async function (pPckDir, pPath) { + let asyncPCKResource = GodotOS.asyncPCKGetAsyncPCKResource(pPckDir, pPath); + if (asyncPCKResource != null) { + if ( + asyncPCKResource.status != GodotOS.AsyncPCKFile.Status.STATUS_LOADING + && asyncPCKResource.status != GodotOS.AsyncPCKFile.Status.STATUS_INSTALLED + ) { + // `GodotOS.AsyncPCKResource.load()` returns it's loading promise if it exists. + await asyncPCKResource.load(); + } + return; + } + asyncPCKResource = GodotOS.AsyncPCKResource.create(pPckDir, pPath); + + const assetsDir = GodotOS.asyncPCKGetAsyncPCKAssetsDir(pPckDir); + const path = GodotOS._removeResPrefix(pPath); + const depsJsonPath = `${assetsDir}/${path}.deps.json`; + const depsJsonResponse = await GodotOS.asyncPCKFetch(depsJsonPath); + if (!depsJsonResponse.ok) { + GodotRuntime.error(`Couldn't load dependencies file "${depsJsonPath}".`); + asyncPCKResource.removeFromInstallMap(); + return; + } + + const remapResponseJson = await depsJsonResponse.json(); + const dependencies = remapResponseJson['dependencies']; + const resources = remapResponseJson['resources']; + + // Initialize the desired resource ASAP. + asyncPCKResource.initialize(resources[pPath].files, Object.keys(dependencies)); + + const localAsyncPCKResources = Object.entries(resources).map(([pResourcePath, pResourceDefinition]) => { + let localAsyncPCKResource = GodotOS.asyncPCKGetAsyncPCKResource(pPckDir, pResourcePath); + const resourceFiles = pResourceDefinition['files']; + const resourceDependencies = dependencies?.[pResourcePath] ?? []; + + if (localAsyncPCKResource == null) { + localAsyncPCKResource = GodotOS.AsyncPCKResource.createAndInitialize( + pPckDir, + pResourcePath, + resourceFiles, + resourceDependencies + ); + } else if (!localAsyncPCKResource.initialized) { + localAsyncPCKResource.initialize(resourceFiles, resourceDependencies); + } + + return localAsyncPCKResource; }); + + await Promise.allSettled( + localAsyncPCKResources.map(async (pAsyncPCKResource) => { + await pAsyncPCKResource.load(); + }) + ); + }, + + asyncPCKInstallFileGetStatus: function (pPckDir, pPath) { + const path = GodotOS._addResPrefix(pPath); + const asyncPCKResource = GodotOS.asyncPCKGetAsyncPCKResource(pPckDir, path); + if (asyncPCKResource == null) { + return null; + } + const jsonObject = asyncPCKResource.getAsJsonObject(); + return jsonObject; }, }, @@ -322,6 +965,37 @@ const GodotOS = { return 0; }, + godot_js_os_asyncpck_install_file__proxy: 'sync', + godot_js_os_asyncpck_install_file__sig: 'ipp', + godot_js_os_asyncpck_install_file: function (pPckDirPtr, pPathPtr) { + const pckDir = GodotOS._trimLastSlash(GodotRuntime.parseString(pPckDirPtr)); + const path = GodotOS._addResPrefix(GodotOS._trimLastSlash(GodotRuntime.parseString(pPathPtr))); + + GodotOS.asyncPCKInstallFile(pckDir, path).catch((err) => { + GodotRuntime.error(`GodotOS.installAsyncFile("${pckDir}", "${path}")`, err); + }); + return 0; + }, + + godot_js_os_asyncpck_install_file_get_status__proxy: 'sync', + godot_js_os_asyncpck_install_file_get_status__sig: 'ppp', + godot_js_os_asyncpck_install_file_get_status: function (pPckDirPtr, pPathPtr, pReturnStringLengthPtr) { + const pckDir = GodotOS._trimLastSlash(GodotRuntime.parseString(pPckDirPtr)); + const path = GodotOS._addResPrefix(GodotOS._trimLastSlash(GodotRuntime.parseString(pPathPtr))); + + const status = GodotOS.asyncPCKInstallFileGetStatus(pckDir, path); + if (status == null) { + return 0; + } + + const statusJson = JSON.stringify(status, null, 2); + const statusJsonPtr = GodotRuntime.allocString(statusJson); + if (pReturnStringLengthPtr !== 0) { + GodotRuntime.setHeapValue(pReturnStringLengthPtr, GodotRuntime.strlen(statusJson), 'i32'); + } + return statusJsonPtr; + }, + godot_js_os_execute__proxy: 'sync', godot_js_os_execute__sig: 'ii', godot_js_os_execute: function (p_json) { @@ -378,8 +1052,8 @@ const GodotOS = { }, }; -autoAddDeps(GodotOS, '$GodotOS'); -mergeInto(LibraryManager.library, GodotOS); +autoAddDeps(_GodotOS, '$GodotOS'); +mergeInto(LibraryManager.library, _GodotOS); /* * Godot event listeners. diff --git a/platform/web/js/libs/library_godot_runtime.js b/platform/web/js/libs/library_godot_runtime.js index 4114aa566ed4..96293d562025 100644 --- a/platform/web/js/libs/library_godot_runtime.js +++ b/platform/web/js/libs/library_godot_runtime.js @@ -30,6 +30,44 @@ const GodotRuntime = { $GodotRuntime: { + _getConfigFileAsJsonCallback: null, + + getConfigFileFromString: function (pConfigFileDataAsString) { + if (pConfigFileDataAsString.length == 0) { + return null; + } + if (GodotRuntime._getConfigFileAsJsonCallback == null) { + GodotRuntime.error('Could not get config file as JSON, callback not yet set.'); + return null; + } + globalThis['GodotRuntime'] = GodotRuntime; + globalThis['HEAPU32'] = HEAPU32; + globalThis['HEAPU8'] = HEAPU8; + globalThis['HEAP8'] = HEAP8; + + let configFileDataPtr = GodotRuntime.allocString(pConfigFileDataAsString); + const configFilePtr = GodotRuntime._getConfigFileAsJsonCallback(configFileDataPtr); + GodotRuntime.free(configFileDataPtr); + configFileDataPtr = 0; + + if (configFilePtr === 0) { + GodotRuntime.error('configFilePtr is nullptr'); + return null; + } + const configFile = GodotRuntime.parseString(configFilePtr); + + if (configFile.length === 0) { + GodotRuntime.error('configFile is empty', configFile); + return null; + } + const configFileJson = JSON.parse(configFile); + + GodotRuntime.free(configFileDataPtr); + configFileDataPtr = 0; + + return configFileJson; + }, + /* * Functions */ @@ -129,6 +167,12 @@ const GodotRuntime = { return stringToUTF8Array(p_str, HEAP8, p_ptr, p_len); }, }, + + godot_js_runtime_set_get_config_file_as_json_cb__proxy: 'async', + godot_js_runtime_set_get_config_file_as_json_cb__sig: 'pp', + godot_js_runtime_set_get_config_file_as_json_cb: function (pCallbackPtr) { + GodotRuntime._getConfigFileAsJsonCallback = GodotRuntime.get_func(pCallbackPtr); + }, }; autoAddDeps(GodotRuntime, '$GodotRuntime'); mergeInto(LibraryManager.library, GodotRuntime); diff --git a/platform/web/js/modules/utils/concurrency.js b/platform/web/js/modules/utils/concurrency.js new file mode 100644 index 000000000000..b67a452e0652 --- /dev/null +++ b/platform/web/js/modules/utils/concurrency.js @@ -0,0 +1,130 @@ +/** + * @file Simple concurrency queue manager. + * @example + * // This will limit the number of simultaneous fetch calls to 2. + * const manager = ConcurrencyQueueManager({ limit: 2 }); + * for (let i; i < 100; i++) { + * manager.queue(async () => { + * const targetUrl = new URL('https://example.org/'); + * targetUrl.searchParams.set("i", i); + * return await fetch(targetUrl); + * }); + * } + */ + +const defaultValues = Object.freeze({ + concurrencyLimit: 5, +}); + +/** + * @template T + * @typedef {() => Promise} PromiseWrapper + * @typedef {{ + * symbol: Symbol, + * promiseWrapper: PromiseWrapper, + * }} ConcurrencyQueueManagerItem + */ + +/** + * @typedef {{ + * limit: number, + * }} ConcurrencyQueueManagerOptions + */ + +export class ConcurrencyQueueManager { + /** @type {number} */ + #limit = defaultValues.concurrencyLimit; + /** @type {ConcurrencyQueueManagerItem[]} */ + #queue = []; + /** @type {ConcurrencyQueueManagerItem[]} */ + #active = []; + + #eventTarget = new EventTarget(); + + /** + * @constructor + * @param {ConcurrencyQueueManagerOptions} [pOptions={}] + */ + constructor(pOptions = {}) { + const { limit = defaultValues.concurrencyLimit } = pOptions; + this.#limit = limit; + } + + get limit() { + return this.#limit; + } + + /** + * Adds a function that returns a promise to the queue. Will return after the promise is executed. + * @async + * @template T + * @param {PromiseWrapper} pPromiseWrapper + * @returns {Promise} + * @throws + */ + async queue(pPromiseWrapper) { + if (pPromiseWrapper == null) { + throw new ReferenceError('pPromiseWrapper is null.'); + } + + const queueItem = await this.#waitForActiveQueueItem(pPromiseWrapper); + try { + return await queueItem.promiseWrapper(); + } catch (error) { + const newError = new Error('ConcurrencyQueueManager detected an error in a managed promise.'); + newError.cause = error; + throw error; + } finally { + const queueIndex = this.#active.indexOf(queueItem); + this.#active.splice(queueIndex, 1); + while (this.#queue.length > 0 && this.#active.length < this.#limit) { + const concurrencyQueueItem = this.#queue[0]; + this.#queue.splice(0, 1); + this.#active.push(queueItem); + this.#eventTarget.dispatchEvent(new CustomEvent('queuenext', { detail: concurrencyQueueItem })); + } + } + } + + /** + * Waits for the queue to be available. + * @template T + * @param {PromiseWrapper} pPromiseWrapper + * @returns {Promise>} + * @throws + */ + #waitForActiveQueueItem(pPromiseWrapper) { + if (pPromiseWrapper == null) { + throw new ReferenceError('pPromiseWrapper is null.'); + } + + const symbol = Symbol('ConcurrencyQueueItemId'); + /** @type {ConcurrencyQueueManagerItem} */ + const queueItem = { + symbol, + promiseWrapper: pPromiseWrapper, + }; + + if (this.#active.length < this.#limit) { + this.#active.push(queueItem); + return queueItem; + } + + this.#queue.push(queueItem); + return new Promise((pResolve, _pReject) => { + /** @type {(event: CustomEvent) => void} */ + const onQueueNext = (pEvent) => { + if (pEvent?.detail?.symbol !== symbol) { + return; + } + this.#eventTarget.removeEventListener('queuenext', onQueueNext); + pResolve(pEvent.detail); + }; + this.#eventTarget.addEventListener('queuenext', onQueueNext); + }); + } +} + +export default { + ConcurrencyQueueManager, +}; diff --git a/platform/web/js/modules/utils/wait.js b/platform/web/js/modules/utils/wait.js new file mode 100644 index 000000000000..a60852215f76 --- /dev/null +++ b/platform/web/js/modules/utils/wait.js @@ -0,0 +1,47 @@ +/** + * @file Wait utilities. + */ + +/** + * Waits the specified amount of time then returns. + * @async + * @param {number} pNumber + * @param {string} [pUnit="s"] + * @returns {Promise} + * @throws + */ +export function wait(pNumber, pUnit = 's') { + if (typeof pNumber != 'number') { + throw new TypeError('pNumber is not a number.'); + } + if (typeof pUnit != 'string') { + throw new TypeError('pUnit is not a string.'); + } + + let waitTime = 0; + + switch (pUnit.toLowerCase()) { + case 's': + case 'second': + case 'seconds': + waitTime = pNumber * 1000; + break; + + case 'ms': + case 'millisecond': + case 'milliseconds': + waitTime = pNumber; + break; + + default: + throw new Error(`Unknown pUnit (${pUnit})`); + } + + return new Promise((resolve) => { + setTimeout(() => resolve(), waitTime); + }); +} + +export default { + wait, +}; diff --git a/platform/web/os_web.cpp b/platform/web/os_web.cpp index 9666bd8882e7..7c5e1beadc41 100644 --- a/platform/web/os_web.cpp +++ b/platform/web/os_web.cpp @@ -31,14 +31,12 @@ #include "os_web.h" #include "api/javascript_bridge_singleton.h" -#include "display_server_web.h" -#include "godot_js.h" -#include "ip_web.h" -#include "net_socket_web.h" - #include "core/config/project_settings.h" #include "core/debugger/engine_debugger.h" +#include "core/io/config_file.h" #include "core/io/file_access.h" +#include "core/io/file_access_pack.h" +#include "core/io/json.h" #include "core/os/main_loop.h" #include "core/profiling/profiling.h" #include "drivers/unix/dir_access_unix.h" @@ -47,6 +45,11 @@ #include "modules/modules_enabled.gen.h" // For websocket. +#include "display_server_web.h" +#include "godot_js.h" +#include "ip_web.h" +#include "net_socket_web.h" + #include #include #include @@ -161,6 +164,70 @@ int OS_Web::get_default_thread_pool_size() const { #endif } +Error OS_Web::async_pck_install_file(const String &p_path) const { + String path = ResourceUID::ensure_path(p_path); + ERR_FAIL_COND_V_MSG(!path.begins_with("res://"), ERR_FILE_BAD_PATH, vformat(R"*(Not able to install "%s" from a ".asyncpck".)*", path)); + + if (FileAccess::exists(path)) { + return OK; + } + + Error err; + String pck_path = async_pck_get_async_pck_path(path, &err); + if (err != OK) { + return err; + } + + String pck_base_dir = pck_path.get_base_dir(); + err = static_cast(godot_js_os_asyncpck_install_file(pck_base_dir.utf8().get_data(), path.utf8().get_data())); + return err; +} + +Dictionary OS_Web::async_pck_install_file_get_status(const String &p_path) const { + String path = ResourceUID::ensure_path(p_path); + + if (FileAccess::exists(path)) { + Dictionary status; + status["files"] = Dictionary(); + status["size"] = 0; + status["progress"] = 0; + status["progress_ratio"] = 1; + status["status"] = "STATUS_INSTALLED"; + return status; + return Dictionary(); + } + + Error err; + String pck_path = async_pck_get_async_pck_path(path, &err); + if (err != OK) { + Dictionary status; + status["files"] = Dictionary(); + status["size"] = 0; + status["progress"] = 0; + status["progress_ratio"] = 1; + status["status"] = "STATUS_ERROR"; + status["errors"] = Dictionary(); + return status; + } + + String pck_base_dir = pck_path.get_base_dir(); + + int32_t status_text_length = 0; + char *status_text_ptr = godot_js_os_asyncpck_install_file_get_status(pck_base_dir.utf8().get_data(), path.utf8().get_data(), &status_text_length); + if (status_text_ptr == nullptr || status_text_length <= 0) { + Dictionary status; + status["files"] = Dictionary(); + status["size"] = 0; + status["progress"] = 0; + status["progress_ratio"] = 1; + status["status"] = "STATUS_ERROR"; + status["errors"] = Dictionary(); + return status; + } + Dictionary status = JSON::parse_string(String::utf8(status_text_ptr, status_text_length)); + return status; +} + bool OS_Web::_check_internal_feature_support(const String &p_feature) { if (p_feature == "web") { return true; @@ -264,6 +331,35 @@ void OS_Web::update_pwa_state_callback() { } } +char *OS_Web::get_config_as_json_callback(const char *p_config_file_data_ptr) { + ERR_FAIL_NULL_V(p_config_file_data_ptr, nullptr); + String config_file_data_as_string = String::utf8(p_config_file_data_ptr); + ERR_FAIL_COND_V(config_file_data_as_string.is_empty(), nullptr); + + Ref config_file; + config_file.instantiate(); + config_file->parse(config_file_data_as_string); + + Dictionary json_config_file_data; + for (const String §ion : config_file->get_sections()) { + Dictionary sectionData; + for (const String &key : config_file->get_section_keys(section)) { + sectionData[key] = config_file->get_value(section, key); + } + json_config_file_data[section] = sectionData; + } + String json_config_file_data_as_string = JSON::stringify(json_config_file_data, String(" ").repeat(2)); + size_t json_config_file_data_len = json_config_file_data_as_string.utf8().size(); + char *returned_json_config_file_data_ptr = (char *)memalloc(sizeof(char) * json_config_file_data_len); + ERR_FAIL_NULL_V(returned_json_config_file_data_ptr, nullptr); + memcpy(returned_json_config_file_data_ptr, json_config_file_data_as_string.utf8().ptr(), json_config_file_data_len); + godot_js_string *js_string = (godot_js_string *)memalloc(sizeof(godot_js_string)); + js_string->length = json_config_file_data_len; + js_string->data = returned_json_config_file_data_ptr; + ERR_FAIL_COND_V(js_string->data != returned_json_config_file_data_ptr, nullptr); + return returned_json_config_file_data_ptr; +} + void OS_Web::force_fs_sync() { if (is_userfs_persistent()) { idb_needs_sync = true; @@ -303,6 +399,7 @@ OS_Web::OS_Web() { setenv("LANG", locale_ptr, true); godot_js_pwa_cb(&OS_Web::update_pwa_state_callback); + godot_js_runtime_set_get_config_file_as_json_cb(&OS_Web::get_config_as_json_callback); if (AudioDriverWeb::is_available()) { audio_drivers.push_back(memnew(AudioDriverWorklet)); diff --git a/platform/web/os_web.h b/platform/web/os_web.h index 43bcfa6cab8f..c5340a1cfc2a 100644 --- a/platform/web/os_web.h +++ b/platform/web/os_web.h @@ -58,6 +58,7 @@ class OS_Web : public OS_Unix { WASM_EXPORT static void dir_access_remove_callback(const String &p_file); WASM_EXPORT static void fs_sync_callback(); WASM_EXPORT static void update_pwa_state_callback(); + WASM_EXPORT static char *get_config_as_json_callback(const char *p_config_file_data); protected: void initialize() override; @@ -93,6 +94,10 @@ class OS_Web : public OS_Unix { String get_unique_id() const override; int get_default_thread_pool_size() const override; + bool async_pck_is_supported() const override { return true; } + Error async_pck_install_file(const String &p_path) const override; + Dictionary async_pck_install_file_get_status(const String &p_path) const override; + String get_executable_path() const override; Error shell_open(const String &p_uri) override; String get_name() const override; diff --git a/platform/windows/export/export_plugin.cpp b/platform/windows/export/export_plugin.cpp index 13116a3ac96d..3d6929719b4d 100644 --- a/platform/windows/export/export_plugin.cpp +++ b/platform/windows/export/export_plugin.cpp @@ -138,7 +138,7 @@ Error EditorExportPlatformWindows::_process_icon(const Ref & valid_icon_count++; } else { int size = (icon_size[i] == 0) ? 256 : icon_size[i]; - add_message(EXPORT_MESSAGE_WARNING, TTR("Resources Modification"), vformat(TTR("Icon size \"%d\" is missing."), size)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Resources Modification"), vformat(TTR("Icon size \"%d\" is missing."), size)); } } ERR_FAIL_COND_V(valid_icon_count == 0, ERR_CANT_OPEN); @@ -212,7 +212,7 @@ Error EditorExportPlatformWindows::export_project(const Ref } else { String exe_arch = _get_exe_arch(template_path); if (arch != exe_arch) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Mismatching custom export template executable architecture: found \"%s\", expected \"%s\"."), exe_arch, arch)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Mismatching custom export template executable architecture: found \"%s\", expected \"%s\"."), exe_arch, arch)); return ERR_CANT_CREATE; } } @@ -235,7 +235,7 @@ Error EditorExportPlatformWindows::export_project(const Ref Ref tmp_app_dir = DirAccess::create_for_path(tmp_dir_path); if (export_as_zip) { if (tmp_app_dir.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not create and open the directory: \"%s\""), tmp_dir_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not create and open the directory: \"%s\""), tmp_dir_path)); return ERR_CANT_CREATE; } if (DirAccess::exists(tmp_dir_path)) { @@ -321,7 +321,7 @@ Error EditorExportPlatformWindows::export_project(const Ref Ref tmp_dir = DirAccess::create_for_path(path.get_base_dir()); err = tmp_dir->rename(pck_path, path); if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), vformat(TTR("Failed to rename temporary file \"%s\"."), pck_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), vformat(TTR("Failed to rename temporary file \"%s\"."), pck_path)); } } @@ -526,7 +526,7 @@ Error EditorExportPlatformWindows::_add_data(const Ref &p_pr String tmp_icon_path = EditorPaths::get_singleton()->get_temp_dir().path_join("_tmp.ico"); if (!icon_path.is_empty()) { if (_process_icon(p_preset, icon_path, tmp_icon_path) != OK) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Resources Modification"), vformat(TTR("Invalid icon file \"%s\"."), icon_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Resources Modification"), vformat(TTR("Invalid icon file \"%s\"."), icon_path)); icon_path = String(); } } @@ -546,7 +546,7 @@ Error EditorExportPlatformWindows::_code_sign(const Ref &p_p #ifdef WINDOWS_ENABLED String signtool_path = EDITOR_GET("export/windows/signtool"); if (!signtool_path.is_empty() && !FileAccess::exists(signtool_path)) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Could not find signtool executable at \"%s\"."), signtool_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Could not find signtool executable at \"%s\"."), signtool_path)); return ERR_FILE_NOT_FOUND; } if (signtool_path.is_empty()) { @@ -555,7 +555,7 @@ Error EditorExportPlatformWindows::_code_sign(const Ref &p_p #else String signtool_path = EDITOR_GET("export/windows/osslsigncode"); if (!signtool_path.is_empty() && !FileAccess::exists(signtool_path)) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Could not find osslsigncode executable at \"%s\"."), signtool_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Could not find osslsigncode executable at \"%s\"."), signtool_path)); return ERR_FILE_NOT_FOUND; } if (signtool_path.is_empty()) { @@ -575,7 +575,7 @@ Error EditorExportPlatformWindows::_code_sign(const Ref &p_p args.push_back("/f"); args.push_back(p_preset->get_or_env("codesign/identity", ENV_WIN_CODESIGN_ID)); } else { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("No identity found.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("No identity found.")); return FAILED; } } else if (id_type == 2) { //Windows certificate store @@ -583,11 +583,11 @@ Error EditorExportPlatformWindows::_code_sign(const Ref &p_p args.push_back("/sha1"); args.push_back(p_preset->get_or_env("codesign/identity", ENV_WIN_CODESIGN_ID)); } else { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("No identity found.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("No identity found.")); return FAILED; } } else { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Invalid identity type.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Invalid identity type.")); return FAILED; } #else @@ -596,7 +596,7 @@ Error EditorExportPlatformWindows::_code_sign(const Ref &p_p args.push_back("-pkcs12"); args.push_back(p_preset->get_or_env("codesign/identity", ENV_WIN_CODESIGN_ID)); } else { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("No identity found.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("No identity found.")); return FAILED; } #endif @@ -628,7 +628,7 @@ Error EditorExportPlatformWindows::_code_sign(const Ref &p_p args.push_back(p_preset->get("codesign/timestamp_server_url")); #endif } else { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Invalid timestamp server.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Invalid timestamp server.")); return FAILED; } } @@ -677,9 +677,9 @@ Error EditorExportPlatformWindows::_code_sign(const Ref &p_p Error err = OS::get_singleton()->execute(signtool_path, args, &str, nullptr, true); if (err != OK || str.contains("not found") || str.contains("not recognized")) { #ifdef WINDOWS_ENABLED - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start signtool executable. Configure signtool path in the Editor Settings (Export > Windows > signtool), or disable \"Codesign\" in the export preset.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start signtool executable. Configure signtool path in the Editor Settings (Export > Windows > signtool), or disable \"Codesign\" in the export preset.")); #else - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start osslsigncode executable. Configure signtool path in the Editor Settings (Export > Windows > osslsigncode), or disable \"Codesign\" in the export preset.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start osslsigncode executable. Configure signtool path in the Editor Settings (Export > Windows > osslsigncode), or disable \"Codesign\" in the export preset.")); #endif return err; } @@ -690,7 +690,7 @@ Error EditorExportPlatformWindows::_code_sign(const Ref &p_p #else if (str.contains("Failed")) { #endif - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Signtool failed to sign executable: %s."), str)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Signtool failed to sign executable: %s."), str)); return FAILED; } @@ -699,13 +699,13 @@ Error EditorExportPlatformWindows::_code_sign(const Ref &p_p err = tmp_dir->remove(p_path); if (err != OK) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Failed to remove temporary file \"%s\"."), p_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Failed to remove temporary file \"%s\"."), p_path)); return err; } err = tmp_dir->rename(p_path + "_signed", p_path); if (err != OK) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Failed to rename temporary file \"%s\"."), p_path + "_signed")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Failed to rename temporary file \"%s\"."), p_path + "_signed")); return err; } #endif @@ -807,13 +807,13 @@ Error EditorExportPlatformWindows::fixup_embedded_pck(const String &p_path, int6 // Patch the header of the "pck" section in the PE file so that it corresponds to the embedded data. if (p_embedded_size + p_embedded_start >= 0x100000000) { // Check for total executable size. - add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Windows executables cannot be >= 4 GiB.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Windows executables cannot be >= 4 GiB.")); return ERR_INVALID_DATA; } Ref f = FileAccess::open(p_path, FileAccess::READ_WRITE); if (f.is_null()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), vformat(TTR("Failed to open executable file \"%s\"."), p_path)); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), vformat(TTR("Failed to open executable file \"%s\"."), p_path)); return ERR_CANT_OPEN; } @@ -825,7 +825,7 @@ Error EditorExportPlatformWindows::fixup_embedded_pck(const String &p_path, int6 f->seek(pe_pos); uint32_t magic = f->get_32(); if (magic != 0x00004550) { - add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Executable file header corrupted.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Executable file header corrupted.")); return ERR_FILE_CORRUPT; } } @@ -910,7 +910,7 @@ Error EditorExportPlatformWindows::fixup_embedded_pck(const String &p_path, int6 f->close(); if (pck_old_pos == -1) { - add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Executable \"pck\" section not found.")); + add_message(EditorExportPlatformData::EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Executable \"pck\" section not found.")); return ERR_FILE_CORRUPT; } return OK; @@ -1047,7 +1047,7 @@ Error EditorExportPlatformWindows::run(const Ref &p_preset, } } - const bool use_remote = p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT); + const bool use_remote = p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(EditorExportPlatformData::DEBUG_FLAG_DUMB_CLIENT); int dbg_port = EDITOR_GET("network/debug/remote_port"); print_line("Creating temporary directory..."); diff --git a/pyproject.toml b/pyproject.toml index b1245da9f471..c924af08878c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,13 @@ skip = [ "platform/android/java/lib/src/main/java/com/*", "platform/web/package-lock.json", ] +ignore-regex = [ + # => `\\b([lpr][A-Z]+?.*?)\\b` + # JavaScript variables (pEvent, pResolve) that gets matched as misspellings + # (i.e. pEvent ==> prevent, pReject ==> project, prefect). + + "\\b([lpr][A-Z]+?.*?)\\b" +] ignore-words-list = [ "breaked", "cancelled", diff --git a/scene/main/async_pck_installer.cpp b/scene/main/async_pck_installer.cpp new file mode 100644 index 000000000000..c313a290d633 --- /dev/null +++ b/scene/main/async_pck_installer.cpp @@ -0,0 +1,574 @@ +/**************************************************************************/ +/* async_pck_installer.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "async_pck_installer.h" + +#include "core/config/engine.h" +#include "core/io/resource_loader.h" +#include "core/os/os.h" + +void AsyncPCKInstaller::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + } break; + + case NOTIFICATION_READY: { + if (autostart) { + start(); + } + } break; + + case NOTIFICATION_PROCESS: { + update(); + } break; + + case NOTIFICATION_EXIT_TREE: { + set_process(false); + } break; + } +} + +void AsyncPCKInstaller::update() { + PackedStringArray processed_file_paths = _get_processed_file_paths(); + + InstallerStatus new_status = get_status(); + switch (new_status) { + case INSTALLER_STATUS_IDLE: + case INSTALLER_STATUS_LOADING: { + // Do nothing. + } break; + + case INSTALLER_STATUS_INSTALLED: + case INSTALLER_STATUS_ERROR: { + set_process(false); + return; + } break; + + case INSTALLER_STATUS_MAX: { + set_process(false); + ERR_FAIL(); + } break; + } + + const static String KEY_FILES = "files"; + const static String KEY_STATUS = "status"; + const static String KEY_SIZE = "size"; + const static String KEY_PROGRESS = "progress"; + const static String KEY_PROGRESS_RATIO = "progress_ratio"; + const static String KEY_ERRORS = "errors"; + + const static String STATUS_IDLE = "STATUS_IDLE"; + const static String STATUS_LOADING = "STATUS_LOADING"; + const static String STATUS_ERROR = "STATUS_ERROR"; + const static String STATUS_INSTALLED = "STATUS_INSTALLED"; + + HashMap files_status; + + // Utility lambdas. + auto _l_get_status_enum_value = [](const String &l_status_value) -> InstallerStatus { + if (l_status_value == STATUS_IDLE) { + return INSTALLER_STATUS_IDLE; + } else if (l_status_value == STATUS_LOADING) { + return INSTALLER_STATUS_LOADING; + } else if (l_status_value == STATUS_ERROR) { + return INSTALLER_STATUS_ERROR; + } else if (l_status_value == STATUS_INSTALLED) { + return INSTALLER_STATUS_INSTALLED; + } + ERR_FAIL_V(INSTALLER_STATUS_ERROR); + }; + + auto _l_get_file_progress_dictionary = [&](const Dictionary &l_file_progress) -> Dictionary { + Dictionary file_progress; + InstallerStatus file_status = _l_get_status_enum_value(l_file_progress[KEY_STATUS]); + + file_progress[KEY_STATUS] = file_status; + file_progress[KEY_SIZE] = l_file_progress[KEY_SIZE]; + file_progress[KEY_PROGRESS] = l_file_progress[KEY_PROGRESS]; + file_progress[KEY_PROGRESS_RATIO] = l_file_progress[KEY_PROGRESS_RATIO]; + + if (file_status == INSTALLER_STATUS_ERROR) { + file_progress[KEY_ERRORS] = l_file_progress[KEY_ERRORS]; + } + + return file_progress; + }; + + // Update status of each file. + for (const KeyValue &key_value : file_paths_status) { + String file_path = key_value.key; + + Dictionary status = OS::get_singleton()->async_pck_install_file_get_status(file_path); + Dictionary files = status[KEY_FILES]; + for (const KeyValue &file_key_value : files) { + if (files_status.has(file_key_value.key)) { + continue; + } + files_status.insert(file_key_value.key, file_key_value.value); + } + + InstallerStatus file_status = _l_get_status_enum_value(status[KEY_STATUS]); + set_file_path_status(file_path, file_status); + + switch (file_status) { + case INSTALLER_STATUS_IDLE: { + // Do nothing. + } break; + + case INSTALLER_STATUS_LOADING: { + emit_signal(SIGNAL_FILE_PROGRESS, file_path, _l_get_file_progress_dictionary(status)); + } break; + + case INSTALLER_STATUS_INSTALLED: { + emit_signal(SIGNAL_FILE_PROGRESS, file_path, _l_get_file_progress_dictionary(status)); + emit_signal(SIGNAL_FILE_INSTALLED, file_path); + } break; + + case INSTALLER_STATUS_ERROR: { + emit_signal(SIGNAL_FILE_ERROR, status[KEY_ERRORS]); + } break; + + case INSTALLER_STATUS_MAX: { + ERR_FAIL(); + } break; + } + } + + // Trigger signals based on the new status. + new_status = get_status(); + + switch (new_status) { + case INSTALLER_STATUS_IDLE: + case INSTALLER_STATUS_ERROR: { + // Do nothing. + } break; + + case INSTALLER_STATUS_LOADING: + case INSTALLER_STATUS_INSTALLED: { + uint64_t progress_total = 0; + uint64_t size_total = 0; + double progress_ratio = 0; + + for (const KeyValue &key_value : files_status) { + size_total += (uint64_t)key_value.value[KEY_SIZE]; + progress_total += (uint64_t)key_value.value[KEY_PROGRESS]; + } + + Dictionary files_progress; + files_progress[KEY_SIZE] = size_total; + files_progress[KEY_PROGRESS] = progress_total; + if (size_total > 0) { + progress_ratio = (double)progress_total / (double)size_total; + } + files_progress[KEY_PROGRESS_RATIO] = progress_ratio; + + emit_signal(SIGNAL_PROGRESS, files_progress); + } break; + + case INSTALLER_STATUS_MAX: { + ERR_FAIL(); + } break; + } +} + +void AsyncPCKInstaller::start() { + if (Engine::get_singleton()->is_editor_hint()) { + return; + } + if (started) { + return; + } + started = true; + + PackedStringArray processed_file_paths = _get_processed_file_paths(); + + // Check if the resources are already installed. + for (const String &file_path : processed_file_paths) { + if (OS::get_singleton()->async_pck_is_supported()) { + if (!OS::get_singleton()->async_pck_is_file_installable(file_path)) { + set_file_path_status(file_path, INSTALLER_STATUS_INSTALLED); + } + } else { + if (ResourceLoader::exists(file_path)) { + set_file_path_status(file_path, INSTALLER_STATUS_INSTALLED); + } else { + set_file_path_status(file_path, INSTALLER_STATUS_ERROR); + emit_signal(SIGNAL_FILE_ERROR, file_path, Array({ vformat(R"*(File "%s" doesn't exist.)*", file_path) })); + } + } + } + + for (const String &file_path : processed_file_paths) { + if (file_paths_status.has(file_path) && file_paths_status[file_path] != INSTALLER_STATUS_IDLE) { + continue; + } + + Error err = OS::get_singleton()->async_pck_install_file(file_path); + if (err == OK) { + set_file_path_status(file_path, INSTALLER_STATUS_LOADING); + } else { + set_file_path_status(file_path, INSTALLER_STATUS_ERROR); + } + } + + set_process(true); +} + +bool AsyncPCKInstaller::set_file_path_status(const String &p_path, InstallerStatus p_status) { + ERR_FAIL_COND_V_MSG(!_get_processed_file_paths().has(ResourceUID::ensure_path(p_path)), false, vformat(R"*("%s" is not in `file_paths`.)*", p_path)); + + InstallerStatus old_status = get_status(); + + if (file_paths_status.has(p_path) && file_paths_status.get(p_path) == p_status) { + // Nothing changed. + return false; + } else { + file_paths_status.insert(p_path, p_status); + } + + // Set the flags as dirty. + status_dirty = true; + install_needed_dirty = true; + + // Check for changed installer status. + InstallerStatus new_status = get_status(); + if (old_status == new_status) { + // The installer status didn't change. + // We still return `true` here because the path status did change. + return true; + } + + // Installer state change side-effects. + String status_name; + switch (new_status) { + case INSTALLER_STATUS_IDLE: { + status_name = "INSTALLER_STATUS_IDLE"; + } break; + case INSTALLER_STATUS_LOADING: { + status_name = "INSTALLER_STATUS_LOADING"; + } break; + case INSTALLER_STATUS_INSTALLED: { + status_name = "INSTALLER_STATUS_INSTALLED"; + } break; + case INSTALLER_STATUS_ERROR: { + status_name = "INSTALLER_STATUS_ERROR"; + } break; + case INSTALLER_STATUS_MAX: { + status_name = "INSTALLER_STATUS_MAX"; + } break; + } + emit_signal(SIGNAL_STATUS_CHANGED); + + switch (new_status) { + case INSTALLER_STATUS_IDLE: { + set_process(started); + } break; + + case INSTALLER_STATUS_LOADING: { + set_process(true); + } break; + + case INSTALLER_STATUS_INSTALLED: + case INSTALLER_STATUS_ERROR: { + set_process(false); + } break; + + case INSTALLER_STATUS_MAX: { + ERR_FAIL_V(true); + } break; + } + + // Return that the path status did change. + return true; +} + +void AsyncPCKInstaller::set_autostart(bool p_autostart) { + autostart = p_autostart; +} + +bool AsyncPCKInstaller::get_autostart() const { + return autostart; +} + +void AsyncPCKInstaller::set_file_paths(const PackedStringArray &p_file_paths) { + ERR_MAIN_THREAD_GUARD; + + if (file_paths == p_file_paths) { + return; + } + + PackedStringArray before_processed_file_paths; + HashSet removed_paths; + + before_processed_file_paths = _get_processed_file_paths(); + + file_paths = p_file_paths; + + // Gather removed paths. + for (const KeyValue &key_value : file_paths_status) { + if (file_paths.has(key_value.key)) { + continue; + } + removed_paths.insert(key_value.key); + } + + // Actually remove paths. + for (const String &path_to_remove : removed_paths) { + file_paths.erase(path_to_remove); + } + + // Check if `file_paths` actually changed. + PackedStringArray current_processed_file_paths = _get_processed_file_paths(); + if (current_processed_file_paths == before_processed_file_paths) { + return; + } + + // Emit `file_removed` signal. + for (const String &file_path : before_processed_file_paths) { + if (current_processed_file_paths.has(file_path)) { + continue; + } + emit_signal(SIGNAL_FILE_REMOVED, file_path); + } + + // Emit `file_added` signal. + for (const String &file_path : current_processed_file_paths) { + if (before_processed_file_paths.has(file_path)) { + continue; + } + emit_signal(SIGNAL_FILE_ADDED, file_path); + } + + if (!started) { + return; + } + + // Start new installing files. + for (const String &file_path : current_processed_file_paths) { + if (before_processed_file_paths.has(file_path)) { + continue; + } + if (file_paths_status.has(file_path) && file_paths_status[file_path] != INSTALLER_STATUS_IDLE) { + continue; + } + + if (OS::get_singleton()->async_pck_is_supported()) { + if (OS::get_singleton()->async_pck_is_file_installable(file_path)) { + Error err = OS::get_singleton()->async_pck_install_file(file_path); + if (err == OK) { + set_file_path_status(file_path, INSTALLER_STATUS_LOADING); + } else { + set_file_path_status(file_path, INSTALLER_STATUS_ERROR); + } + } else { + set_file_path_status(file_path, INSTALLER_STATUS_INSTALLED); + } + } else { + if (ResourceLoader::exists(file_path)) { + set_file_path_status(file_path, INSTALLER_STATUS_INSTALLED); + } else { + set_file_path_status(file_path, INSTALLER_STATUS_ERROR); + } + } + } +} + +PackedStringArray AsyncPCKInstaller::get_file_paths() const { + return file_paths; +} + +String AsyncPCKInstaller::_process_file_path(const String &p_path) const { + String path = p_path.strip_edges(); + if (path.is_empty()) { + return path; + } + return ResourceUID::ensure_path(path); +} + +PackedStringArray AsyncPCKInstaller::_get_processed_file_paths() const { + HashSet processed_file_paths_set; + for (const String &file_path : file_paths) { + String processed_file_path = _process_file_path(file_path); + if (processed_file_path.is_empty()) { + continue; + } + processed_file_paths_set.insert(processed_file_path); + } + + PackedStringArray processed_file_paths; + for (const String &processed_file_path : processed_file_paths_set) { + processed_file_paths.push_back(processed_file_path); + } + return processed_file_paths; +} + +AsyncPCKInstaller::InstallerStatus AsyncPCKInstaller::get_status() const { + if (!status_dirty) { + return status_cached; + } + + InstallerStatus status = INSTALLER_STATUS_IDLE; + + if (file_paths_status.is_empty()) { + return INSTALLER_STATUS_IDLE; + } + + for (const KeyValue &key_value : file_paths_status) { +#define CASE_INSTALLER_STATUS_MAX \ + case INSTALLER_STATUS_MAX: { \ + ERR_FAIL_V(INSTALLER_STATUS_ERROR); \ + } break + + switch (status) { + case INSTALLER_STATUS_IDLE: { + switch (key_value.value) { + case INSTALLER_STATUS_IDLE: { + // Do nothing, the state is the same. + } break; + + case INSTALLER_STATUS_LOADING: + case INSTALLER_STATUS_INSTALLED: { + status = key_value.value; + } break; + + case INSTALLER_STATUS_ERROR: { + return INSTALLER_STATUS_ERROR; + } break; + + CASE_INSTALLER_STATUS_MAX; + } + } break; + + case INSTALLER_STATUS_LOADING: { + switch (key_value.value) { + case INSTALLER_STATUS_LOADING: { + // Do nothing, the state is the same. + } break; + + case INSTALLER_STATUS_IDLE: { + // Do nothing, as `INSTALLER_STATUS_LOADING` > `INSTALLER_STATUS_IDLE`. + } break; + + case INSTALLER_STATUS_INSTALLED: { + // Do nothing, as the state is still loading even if there's + // some files that are done. + } break; + + case INSTALLER_STATUS_ERROR: { + return INSTALLER_STATUS_ERROR; + } break; + + CASE_INSTALLER_STATUS_MAX; + } + } break; + + case INSTALLER_STATUS_INSTALLED: { + switch (key_value.value) { + case INSTALLER_STATUS_INSTALLED: { + // Do nothing, the state is the same. + } break; + + case INSTALLER_STATUS_IDLE: + case INSTALLER_STATUS_LOADING: { + // As there's some status that are installed, + // we can assume that the idle files will be + // loaded in a few moments. + status = INSTALLER_STATUS_LOADING; + } break; + + case INSTALLER_STATUS_ERROR: { + return INSTALLER_STATUS_ERROR; + } break; + + CASE_INSTALLER_STATUS_MAX; + } + } break; + + case INSTALLER_STATUS_ERROR: { + return INSTALLER_STATUS_ERROR; + } break; + + CASE_INSTALLER_STATUS_MAX; + } + +#undef CASE_INSTALLER_STATUS_MAX + } + + status_cached = status; + status_dirty = false; + return status; +} + +bool AsyncPCKInstaller::are_files_installable() const { + if (!install_needed_dirty) { + return install_needed_cached; + } + + bool install_needed = false; + + PackedStringArray processed_file_paths = _get_processed_file_paths(); + for (const String &file_path : processed_file_paths) { + if (OS::get_singleton()->async_pck_is_file_installable(file_path)) { + install_needed = true; + break; + } + } + + install_needed_cached = install_needed; + install_needed_dirty = false; + return install_needed; +} + +void AsyncPCKInstaller::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_autostart", "autostart"), &AsyncPCKInstaller::set_autostart); + ClassDB::bind_method(D_METHOD("get_autostart"), &AsyncPCKInstaller::get_autostart); + ClassDB::bind_method(D_METHOD("set_file_paths", "file_paths"), &AsyncPCKInstaller::set_file_paths); + ClassDB::bind_method(D_METHOD("get_file_paths"), &AsyncPCKInstaller::get_file_paths); + + ClassDB::bind_method(D_METHOD("get_status"), &AsyncPCKInstaller::get_status); + ClassDB::bind_method(D_METHOD("are_files_installable"), &AsyncPCKInstaller::are_files_installable); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "autostart"), "set_autostart", "get_autostart"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "file_paths", PROPERTY_HINT_ARRAY_TYPE, MAKE_FILE_ARRAY_TYPE_HINT("*")), "set_file_paths", "get_file_paths"); + + ADD_SIGNAL(MethodInfo(SIGNAL_FILE_ADDED, PropertyInfo(Variant::STRING, "file", PROPERTY_HINT_FILE))); + ADD_SIGNAL(MethodInfo(SIGNAL_FILE_REMOVED, PropertyInfo(Variant::STRING, "file", PROPERTY_HINT_FILE))); + ADD_SIGNAL(MethodInfo(SIGNAL_FILE_PROGRESS, PropertyInfo(Variant::STRING, "file", PROPERTY_HINT_FILE), PropertyInfo(Variant::DICTIONARY, "progress_data"))); + ADD_SIGNAL(MethodInfo(SIGNAL_FILE_INSTALLED, PropertyInfo(Variant::STRING, "file", PROPERTY_HINT_FILE))); + ADD_SIGNAL(MethodInfo(SIGNAL_FILE_ERROR, PropertyInfo(Variant::ARRAY, "errors"))); + ADD_SIGNAL(MethodInfo(SIGNAL_PROGRESS, PropertyInfo(Variant::DICTIONARY, "progress_data"))); + ADD_SIGNAL(MethodInfo(SIGNAL_STATUS_CHANGED)); + + BIND_ENUM_CONSTANT(INSTALLER_STATUS_IDLE); + BIND_ENUM_CONSTANT(INSTALLER_STATUS_LOADING); + BIND_ENUM_CONSTANT(INSTALLER_STATUS_INSTALLED); + BIND_ENUM_CONSTANT(INSTALLER_STATUS_ERROR); + BIND_ENUM_CONSTANT(INSTALLER_STATUS_MAX); +} diff --git a/scene/main/async_pck_installer.h b/scene/main/async_pck_installer.h new file mode 100644 index 000000000000..4885a82a8c8f --- /dev/null +++ b/scene/main/async_pck_installer.h @@ -0,0 +1,92 @@ +/**************************************************************************/ +/* async_pck_installer.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "scene/main/node.h" + +class AsyncPCKInstaller : public Node { + GDCLASS(AsyncPCKInstaller, Node); + + const static inline String SIGNAL_FILE_ADDED = "file_added"; + const static inline String SIGNAL_FILE_REMOVED = "file_removed"; + const static inline String SIGNAL_FILE_PROGRESS = "file_progress"; + const static inline String SIGNAL_FILE_INSTALLED = "file_installed"; + const static inline String SIGNAL_FILE_ERROR = "file_error"; + const static inline String SIGNAL_PROGRESS = "progress"; + const static inline String SIGNAL_STATUS_CHANGED = "status_changed"; + +public: + enum InstallerStatus { + INSTALLER_STATUS_IDLE, + INSTALLER_STATUS_LOADING, + INSTALLER_STATUS_INSTALLED, + INSTALLER_STATUS_ERROR, + INSTALLER_STATUS_MAX, + }; + +private: + bool autostart = false; + bool started = false; + + mutable bool status_dirty = true; + mutable InstallerStatus status_cached = INSTALLER_STATUS_IDLE; + + mutable bool install_needed_dirty = true; + mutable bool install_needed_cached = false; + + PackedStringArray file_paths; + HashMap file_paths_status; + + String _process_file_path(const String &p_path) const; + PackedStringArray _get_processed_file_paths() const; + +protected: + void _notification(int p_what); + static void _bind_methods(); + + void update(); + + bool set_file_path_status(const String &p_path, InstallerStatus p_status); + +public: + void start(); + + void set_autostart(bool p_autostart); + bool get_autostart() const; + + void set_file_paths(const PackedStringArray &p_resources_paths); + PackedStringArray get_file_paths() const; + + InstallerStatus get_status() const; + bool are_files_installable() const; +}; + +VARIANT_ENUM_CAST(AsyncPCKInstaller::InstallerStatus); diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index da9d0fbc949a..57278399d6de 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -94,6 +94,7 @@ #include "scene/gui/texture_rect.h" #include "scene/gui/tree.h" #include "scene/gui/video_stream_player.h" +#include "scene/main/async_pck_installer.h" #include "scene/main/canvas_item.h" #include "scene/main/canvas_layer.h" #include "scene/main/http_request.h" @@ -455,6 +456,7 @@ void register_scene_types() { GDREGISTER_CLASS(CanvasLayer); GDREGISTER_CLASS(ResourcePreloader); GDREGISTER_CLASS(Window); + GDREGISTER_CLASS(AsyncPCKInstaller); GDREGISTER_CLASS(StatusIndicator); diff --git a/scene/scene_string_names.h b/scene/scene_string_names.h index 355e7889f0d0..17d361b7d23c 100644 --- a/scene/scene_string_names.h +++ b/scene/scene_string_names.h @@ -158,6 +158,11 @@ class SceneStringNames { const StringName state_started = "state_started"; const StringName state_finished = "state_finished"; + const StringName item_edited = "item_edited"; + const StringName check_propagated_to_item = "check_propagated_to_item"; + + const StringName timeout = "timeout"; + const StringName FlatButton = "FlatButton"; };