diff --git a/core/io/file_access_pack.cpp b/core/io/file_access_pack.cpp index eec27ce0aa7..1340382eaa4 100644 --- a/core/io/file_access_pack.cpp +++ b/core/io/file_access_pack.cpp @@ -102,6 +102,22 @@ void PackedData::add_pack_source(PackSource *p_source) { } } +uint8_t *PackedData::get_file_hash(const String &p_path) { + PathMD5 pmd5(p_path.md5_buffer()); + HashMap::Iterator E = files.find(pmd5); + if (!E || E->value.offset == 0) { + return nullptr; + } + + return E->value.md5; +} + +void PackedData::clear() { + files.clear(); + _free_packed_dirs(root); + root = memnew(PackedDir); +} + PackedData *PackedData::singleton = nullptr; PackedData::PackedData() { diff --git a/core/io/file_access_pack.h b/core/io/file_access_pack.h index 595a36bca4e..57b7a5f87f5 100644 --- a/core/io/file_access_pack.h +++ b/core/io/file_access_pack.h @@ -111,6 +111,7 @@ private: 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); // for PackSource + uint8_t *get_file_hash(const String &p_path); void set_disabled(bool p_disabled) { disabled = p_disabled; } _FORCE_INLINE_ bool is_disabled() const { return disabled; } @@ -118,6 +119,8 @@ public: static PackedData *get_singleton() { return singleton; } Error add_pack(const String &p_path, bool p_replace_files, uint64_t p_offset); + void clear(); + _FORCE_INLINE_ Ref try_open_path(const String &p_path); _FORCE_INLINE_ bool has_path(const String &p_path); diff --git a/doc/classes/EditorExportPlatform.xml b/doc/classes/EditorExportPlatform.xml index d4084e84a0a..8792bbedc34 100644 --- a/doc/classes/EditorExportPlatform.xml +++ b/doc/classes/EditorExportPlatform.xml @@ -42,6 +42,18 @@ Creates a PCK archive at [param path] for the specified [param preset]. + + + + + + + + + Creates a patch PCK archive at [param path] for the specified [param preset], containing only the files that have changed since the last patch. + [b]Note:[/b] [param patches] is an optional override of the set of patches defined in the export preset. When empty the patches defined in the export preset will be used instead. + + @@ -75,6 +87,18 @@ Create a ZIP archive at [param path] for the specified [param preset]. + + + + + + + + + Create a patch ZIP archive at [param path] for the specified [param preset], containing only the files that have changed since the last patch. + [b]Note:[/b] [param patches] is an optional override of the set of patches defined in the export preset. When empty the patches defined in the export preset will be used instead. + + @@ -151,6 +175,15 @@ If [param embed] is [code]true[/code], PCK content is appended to the end of [param path] file and return [Dictionary] additionally include following keys: [code]embedded_start: int[/code] (embedded PCK offset) and [code]embedded_size: int[/code] (embedded PCK size). + + + + + + + Saves patch PCK archive and returns [Dictionary] with the following keys: [code]result: Error[/code], [code]so_files: Array[/code] (array of the shared/static objects which contains dictionaries with the following keys: [code]path: String[/code], [code]tags: PackedStringArray[/code], and [code]target_folder: String[/code]). + + @@ -160,6 +193,15 @@ Saves ZIP archive and returns [Dictionary] with the following keys: [code]result: Error[/code], [code]so_files: Array[/code] (array of the shared/static objects which contains dictionaries with the following keys: [code]path: String[/code], [code]tags: PackedStringArray[/code], and [code]target_folder: String[/code]). + + + + + + + Saves patch ZIP archive and returns [Dictionary] with the following keys: [code]result: Error[/code], [code]so_files: Array[/code] (array of the shared/static objects which contains dictionaries with the following keys: [code]path: String[/code], [code]tags: PackedStringArray[/code], and [code]target_folder: String[/code]). + + diff --git a/doc/classes/EditorExportPlatformExtension.xml b/doc/classes/EditorExportPlatformExtension.xml index ef589e2f585..f2d14b17107 100644 --- a/doc/classes/EditorExportPlatformExtension.xml +++ b/doc/classes/EditorExportPlatformExtension.xml @@ -36,7 +36,21 @@ [b]Optional.[/b] Creates a PCK archive at [param path] for the specified [param preset]. - This method is called when "Export PCK/ZIP" button is pressed in the export dialog, and PCK is selected as a file type. + This method is called when "Export PCK/ZIP" button is pressed in the export dialog, with "Export as Patch" disabled, and PCK is selected as a file type. + + + + + + + + + + + [b]Optional.[/b] + Creates a patch PCK archive at [param path] for the specified [param preset], containing only the files that have changed since the last patch. + This method is called when "Export PCK/ZIP" button is pressed in the export dialog, with "Export as Patch" enabled, and PCK is selected as a file type. + [b]Note:[/b] The patches provided in [param patches] have already been loaded when this method is called and are merely provided as context. When empty the patches defined in the export preset have been loaded instead. @@ -61,7 +75,21 @@ [b]Optional.[/b] Create a ZIP archive at [param path] for the specified [param preset]. - This method is called when "Export PCK/ZIP" button is pressed in the export dialog, and ZIP is selected as a file type. + This method is called when "Export PCK/ZIP" button is pressed in the export dialog, with "Export as Patch" disabled, and ZIP is selected as a file type. + + + + + + + + + + + [b]Optional.[/b] + Create a ZIP archive at [param path] for the specified [param preset], containing only the files that have changed since the last patch. + This method is called when "Export PCK/ZIP" button is pressed in the export dialog, with "Export as Patch" enabled, and ZIP is selected as a file type. + [b]Note:[/b] The patches provided in [param patches] have already been loaded when this method is called and are merely provided as context. When empty the patches defined in the export preset have been loaded instead. diff --git a/doc/classes/EditorExportPreset.xml b/doc/classes/EditorExportPreset.xml index bba79364e4b..314f74340aa 100644 --- a/doc/classes/EditorExportPreset.xml +++ b/doc/classes/EditorExportPreset.xml @@ -109,6 +109,12 @@ Returns export option value or value of environment variable if it is set. + + + + Returns the list of packs on which to base a patch export on. + + diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 184176391ad..9654cd31576 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -1007,9 +1007,17 @@ void EditorNode::_fs_changed() { export_preset->update_value_overrides(); if (export_defer.pack_only) { // Only export .pck or .zip data pack. if (export_path.ends_with(".zip")) { - err = platform->export_zip(export_preset, export_defer.debug, export_path); + if (export_defer.patch) { + err = platform->export_zip_patch(export_preset, export_defer.debug, export_path, export_defer.patches); + } else { + err = platform->export_zip(export_preset, export_defer.debug, export_path); + } } else if (export_path.ends_with(".pck")) { - err = platform->export_pack(export_preset, export_defer.debug, export_path); + if (export_defer.patch) { + err = platform->export_pack_patch(export_preset, export_defer.debug, export_path, export_defer.patches); + } else { + err = platform->export_pack(export_preset, export_defer.debug, export_path); + } } else { ERR_PRINT(vformat("Export path \"%s\" doesn't end with a supported extension.", export_path)); err = FAILED; @@ -5147,12 +5155,14 @@ void EditorNode::_begin_first_scan() { requested_first_scan = true; } -Error EditorNode::export_preset(const String &p_preset, const String &p_path, bool p_debug, bool p_pack_only, bool p_android_build_template) { +Error EditorNode::export_preset(const String &p_preset, const String &p_path, bool p_debug, bool p_pack_only, bool p_android_build_template, bool p_patch, const Vector &p_patches) { export_defer.preset = p_preset; export_defer.path = p_path; export_defer.debug = p_debug; export_defer.pack_only = p_pack_only; export_defer.android_build_template = p_android_build_template; + export_defer.patch = p_patch; + export_defer.patches = p_patches; cmdline_export_mode = true; return OK; } diff --git a/editor/editor_node.h b/editor/editor_node.h index 109cacdf0e4..9a7d3534ce1 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -246,6 +246,8 @@ private: bool debug = false; bool pack_only = false; bool android_build_template = false; + bool patch = false; + Vector patches; } export_defer; static EditorNode *singleton; @@ -879,7 +881,7 @@ public: void _copy_warning(const String &p_str); - Error export_preset(const String &p_preset, const String &p_path, bool p_debug, bool p_pack_only, bool p_android_build_template); + Error export_preset(const String &p_preset, const String &p_path, bool p_debug, bool p_pack_only, bool p_android_build_template, bool p_patch, const Vector &p_patches); bool is_project_exporting() const; Control *get_gui_base() { return gui_base; } diff --git a/editor/export/editor_export.cpp b/editor/export/editor_export.cpp index 975a601ae19..6ca83c5e25c 100644 --- a/editor/export/editor_export.cpp +++ b/editor/export/editor_export.cpp @@ -83,6 +83,8 @@ void EditorExport::_save() { config->set_value(section, "include_filter", preset->get_include_filter()); config->set_value(section, "exclude_filter", preset->get_exclude_filter()); config->set_value(section, "export_path", preset->get_export_path()); + config->set_value(section, "patches", preset->get_patches()); + config->set_value(section, "encryption_include_filters", preset->get_enc_in_filter()); config->set_value(section, "encryption_exclude_filters", preset->get_enc_ex_filter()); config->set_value(section, "encrypt_pck", preset->get_enc_pck()); @@ -303,6 +305,7 @@ void EditorExport::load_config() { preset->set_exclude_filter(config->get_value(section, "exclude_filter")); preset->set_export_path(config->get_value(section, "export_path", "")); preset->set_script_export_mode(config->get_value(section, "script_export_mode", EditorExportPreset::MODE_SCRIPT_BINARY_TOKENS_COMPRESSED)); + preset->set_patches(config->get_value(section, "patches", Vector())); if (config->has_section_key(section, "encrypt_pck")) { preset->set_enc_pck(config->get_value(section, "encrypt_pck")); diff --git a/editor/export/editor_export_platform.cpp b/editor/export/editor_export_platform.cpp index 983f4ee36ae..58737c53edb 100644 --- a/editor/export/editor_export_platform.cpp +++ b/editor/export/editor_export_platform.cpp @@ -167,6 +167,44 @@ bool EditorExportPlatform::fill_log_messages(RichTextLabel *p_log, Error p_err) return has_messages; } +bool EditorExportPlatform::_check_hash(const uint8_t *p_hash, const Vector &p_data) { + if (p_hash == nullptr) { + return false; + } + + unsigned char hash[16]; + Error err = CryptoCore::md5(p_data.ptr(), p_data.size(), hash); + if (err != OK) { + return false; + } + + for (int i = 0; i < 16; i++) { + if (p_hash[i] != hash[i]) { + return false; + } + } + + return true; +} + +Error EditorExportPlatform::_load_patches(const Vector &p_patches) { + Error err = OK; + if (!p_patches.is_empty()) { + 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)); + return err; + } + } + } + return err; +} + +void EditorExportPlatform::_unload_patches() { + PackedData::get_singleton()->clear(); +} + Error EditorExportPlatform::_save_pack_file(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) { ERR_FAIL_COND_V_MSG(p_total < 1, ERR_PARAMETER_RANGE_ERROR, "Must select at least one file to export."); @@ -237,6 +275,14 @@ Error EditorExportPlatform::_save_pack_file(void *p_userdata, const String &p_pa return OK; } +Error EditorExportPlatform::_save_pack_patch_file(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) { + if (_check_hash(PackedData::get_singleton()->get_file_hash(p_path), p_data)) { + return OK; + } + + return _save_pack_file(p_userdata, p_path, p_data, p_file, p_total, p_enc_in_filters, p_enc_ex_filters, p_key); +} + Error EditorExportPlatform::_save_zip_file(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) { ERR_FAIL_COND_V_MSG(p_total < 1, ERR_PARAMETER_RANGE_ERROR, "Must select at least one file to export."); @@ -260,6 +306,8 @@ Error EditorExportPlatform::_save_zip_file(void *p_userdata, const String &p_pat zipWriteInFileInZip(zip, p_data.ptr(), p_data.size()); zipCloseFileInZip(zip); + zd->file_count += 1; + if (zd->ep->step(TTR("Storing File:") + " " + p_path, 2 + p_file * 100 / p_total, false)) { return ERR_SKIP; } @@ -267,6 +315,14 @@ Error EditorExportPlatform::_save_zip_file(void *p_userdata, const String &p_pat return OK; } +Error EditorExportPlatform::_save_zip_patch_file(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) { + if (_check_hash(PackedData::get_singleton()->get_file_hash(p_path), p_data)) { + return OK; + } + + return _save_zip_file(p_userdata, p_path, p_data, p_file, p_total, p_enc_in_filters, p_enc_ex_filters, p_key); +} + Ref EditorExportPlatform::get_option_icon(int p_index) const { Ref theme = EditorNode::get_singleton()->get_editor_theme(); ERR_FAIL_COND_V(theme.is_null(), Ref()); @@ -1561,7 +1617,7 @@ Dictionary EditorExportPlatform::_save_pack(const Ref &p_pre Vector so_files; int64_t embedded_start = 0; int64_t embedded_size = 0; - Error err_code = save_pack(p_preset, p_debug, p_path, &so_files, p_embed, &embedded_start, &embedded_size); + Error err_code = save_pack(p_preset, p_debug, p_path, &so_files, nullptr, p_embed, &embedded_start, &embedded_size); Dictionary ret; ret["result"] = err_code; @@ -1605,9 +1661,55 @@ Dictionary EditorExportPlatform::_save_zip(const Ref &p_pres return ret; } -Error EditorExportPlatform::save_pack(const Ref &p_preset, bool p_debug, const String &p_path, Vector *p_so_files, bool p_embed, int64_t *r_embedded_start, int64_t *r_embedded_size) { +Dictionary EditorExportPlatform::_save_pack_patch(const Ref &p_preset, bool p_debug, const String &p_path) { + Vector so_files; + Error err_code = save_pack_patch(p_preset, p_debug, p_path, &so_files); + + Dictionary ret; + ret["result"] = err_code; + if (err_code == OK) { + Array arr; + for (const SharedObject &E : so_files) { + Dictionary so; + so["path"] = E.path; + so["tags"] = E.tags; + so["target_folder"] = E.target; + arr.push_back(so); + } + ret["so_files"] = arr; + } + + return ret; +} + +Dictionary EditorExportPlatform::_save_zip_patch(const Ref &p_preset, bool p_debug, const String &p_path) { + Vector so_files; + Error err_code = save_zip_patch(p_preset, p_debug, p_path, &so_files); + + Dictionary ret; + ret["result"] = err_code; + if (err_code == OK) { + Array arr; + for (const SharedObject &E : so_files) { + Dictionary so; + so["path"] = E.path; + so["tags"] = E.tags; + so["target_folder"] = E.target; + arr.push_back(so); + } + ret["so_files"] = arr; + } + + return ret; +} + +Error EditorExportPlatform::save_pack(const Ref &p_preset, bool p_debug, const String &p_path, Vector *p_so_files, EditorExportSaveFunction p_save_func, bool p_embed, int64_t *r_embedded_start, int64_t *r_embedded_size) { EditorProgress ep("savepack", TTR("Packing"), 102, true); + if (p_save_func == nullptr) { + p_save_func = _save_pack_file; + } + // Create the temporary export directory if it doesn't exist. Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); da->make_dir_recursive(EditorPaths::get_singleton()->get_cache_dir()); @@ -1624,7 +1726,7 @@ Error EditorExportPlatform::save_pack(const Ref &p_preset, b pd.f = ftmp; pd.so_files = p_so_files; - Error err = export_project_files(p_preset, p_debug, _save_pack_file, &pd, _pack_add_shared_object); + Error err = export_project_files(p_preset, p_debug, p_save_func, &pd, _pack_add_shared_object); // Close temp file. pd.f.unref(); @@ -1636,6 +1738,12 @@ Error EditorExportPlatform::save_pack(const Ref &p_preset, b return err; } + if (pd.file_ofs.is_empty()) { + DirAccess::remove_file_or_error(tmppath); + add_message(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 Ref f; @@ -1831,28 +1939,56 @@ Error EditorExportPlatform::save_pack(const Ref &p_preset, b return OK; } -Error EditorExportPlatform::save_zip(const Ref &p_preset, bool p_debug, const String &p_path, Vector *p_so_files) { +Error EditorExportPlatform::save_pack_patch(const Ref &p_preset, bool p_debug, const String &p_path, Vector *p_so_files, bool p_embed, int64_t *r_embedded_start, int64_t *r_embedded_size) { + return save_pack(p_preset, p_debug, p_path, p_so_files, _save_pack_patch_file, p_embed, r_embedded_start, r_embedded_size); +} + +Error EditorExportPlatform::save_zip(const Ref &p_preset, bool p_debug, const String &p_path, Vector *p_so_files, EditorExportSaveFunction p_save_func) { EditorProgress ep("savezip", TTR("Packing"), 102, true); + if (p_save_func == nullptr) { + p_save_func = _save_zip_file; + } + + String tmppath = EditorPaths::get_singleton()->get_cache_dir().path_join("packtmp"); + Ref io_fa; zlib_filefunc_def io = zipio_create_io(&io_fa); - zipFile zip = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io); + zipFile zip = zipOpen2(tmppath.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io); ZipData zd; zd.ep = &ep; zd.zip = zip; zd.so_files = p_so_files; - Error err = export_project_files(p_preset, p_debug, _save_zip_file, &zd, _zip_add_shared_object); + Error err = export_project_files(p_preset, p_debug, p_save_func, &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.")); } zipClose(zip, nullptr); + Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + + if (zd.file_count == 0) { + da->remove(tmppath); + add_message(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)); + } + return OK; } +Error EditorExportPlatform::save_zip_patch(const Ref &p_preset, bool p_debug, const String &p_path, Vector *p_so_files) { + return save_zip(p_preset, p_debug, p_path, p_so_files, _save_zip_patch_file); +} + Error EditorExportPlatform::export_pack(const Ref &p_preset, bool p_debug, const String &p_path, BitField p_flags) { ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); return save_pack(p_preset, p_debug, p_path); @@ -1863,6 +1999,28 @@ Error EditorExportPlatform::export_zip(const Ref &p_preset, return save_zip(p_preset, p_debug, p_path); } +Error EditorExportPlatform::export_pack_patch(const Ref &p_preset, bool p_debug, const String &p_path, const Vector &p_patches, BitField p_flags) { + ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); + Error err = _load_patches(p_patches.is_empty() ? p_preset->get_patches() : p_patches); + if (err != OK) { + return err; + } + err = save_pack_patch(p_preset, p_debug, p_path); + _unload_patches(); + return err; +} + +Error EditorExportPlatform::export_zip_patch(const Ref &p_preset, bool p_debug, const String &p_path, const Vector &p_patches, BitField p_flags) { + ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); + Error err = _load_patches(p_patches.is_empty() ? p_preset->get_patches() : p_patches); + if (err != OK) { + return err; + } + err = save_zip_patch(p_preset, p_debug, p_path); + _unload_patches(); + return err; +} + Vector EditorExportPlatform::gen_export_flags(BitField p_flags) { Vector ret; String host = EDITOR_GET("network/debug/remote_host"); @@ -2115,6 +2273,8 @@ void EditorExportPlatform::_bind_methods() { ClassDB::bind_method(D_METHOD("save_pack", "preset", "debug", "path", "embed"), &EditorExportPlatform::_save_pack, DEFVAL(false)); ClassDB::bind_method(D_METHOD("save_zip", "preset", "debug", "path"), &EditorExportPlatform::_save_zip); + ClassDB::bind_method(D_METHOD("save_pack_patch", "preset", "debug", "path"), &EditorExportPlatform::_save_pack_patch); + ClassDB::bind_method(D_METHOD("save_zip_patch", "preset", "debug", "path"), &EditorExportPlatform::_save_zip_patch); ClassDB::bind_method(D_METHOD("gen_export_flags", "flags"), &EditorExportPlatform::gen_export_flags); @@ -2123,6 +2283,8 @@ void EditorExportPlatform::_bind_methods() { ClassDB::bind_method(D_METHOD("export_project", "preset", "debug", "path", "flags"), &EditorExportPlatform::export_project, DEFVAL(0)); ClassDB::bind_method(D_METHOD("export_pack", "preset", "debug", "path", "flags"), &EditorExportPlatform::export_pack, DEFVAL(0)); ClassDB::bind_method(D_METHOD("export_zip", "preset", "debug", "path", "flags"), &EditorExportPlatform::export_zip, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("export_pack_patch", "preset", "debug", "path", "patches", "flags"), &EditorExportPlatform::export_pack_patch, DEFVAL(PackedStringArray()), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("export_zip_patch", "preset", "debug", "path", "patches", "flags"), &EditorExportPlatform::export_zip_patch, DEFVAL(PackedStringArray()), DEFVAL(0)); ClassDB::bind_method(D_METHOD("clear_messages"), &EditorExportPlatform::clear_messages); ClassDB::bind_method(D_METHOD("add_message", "type", "category", "message"), &EditorExportPlatform::add_message); diff --git a/editor/export/editor_export_platform.h b/editor/export/editor_export_platform.h index 95f21ceafb3..ef3274c5e4c 100644 --- a/editor/export/editor_export_platform.h +++ b/editor/export/editor_export_platform.h @@ -101,6 +101,7 @@ private: void *zip = nullptr; EditorProgress *ep = nullptr; Vector *so_files = nullptr; + int file_count = 0; }; Vector messages; @@ -109,10 +110,14 @@ private: 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 bool _check_hash(const uint8_t *p_hash, const Vector &p_data); + static Error _save_pack_file(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); + static Error _save_pack_patch_file(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); static Error _pack_add_shared_object(void *p_userdata, const SharedObject &p_so); static Error _save_zip_file(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); + static Error _save_zip_patch_file(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); static Error _zip_add_shared_object(void *p_userdata, const SharedObject &p_so); struct ScriptCallbackData { @@ -188,6 +193,9 @@ protected: Error ssh_run_on_remote_no_wait(const String &p_host, const String &p_port, const Vector &p_ssh_args, const String &p_cmd_args, OS::ProcessID *r_pid = nullptr, int p_port_fwd = -1) const; Error ssh_push_to_remote(const String &p_host, const String &p_port, const Vector &p_scp_args, const String &p_src_file, const String &p_dst_file) const; + Error _load_patches(const Vector &p_patches); + void _unload_patches(); + public: virtual void get_preset_features(const Ref &p_preset, List *r_features) const = 0; @@ -284,8 +292,14 @@ public: Dictionary _save_pack(const Ref &p_preset, bool p_debug, const String &p_path, bool p_embed = false); Dictionary _save_zip(const Ref &p_preset, bool p_debug, const String &p_path); - Error save_pack(const Ref &p_preset, bool p_debug, const String &p_path, Vector *p_so_files = nullptr, bool p_embed = false, int64_t *r_embedded_start = nullptr, int64_t *r_embedded_size = nullptr); - Error save_zip(const Ref &p_preset, bool p_debug, const String &p_path, Vector *p_so_files = nullptr); + Dictionary _save_pack_patch(const Ref &p_preset, bool p_debug, const String &p_path); + Dictionary _save_zip_patch(const Ref &p_preset, bool p_debug, const String &p_path); + + Error save_pack(const Ref &p_preset, bool p_debug, const String &p_path, Vector *p_so_files = nullptr, EditorExportSaveFunction p_save_func = nullptr, bool p_embed = false, int64_t *r_embedded_start = nullptr, int64_t *r_embedded_size = nullptr); + Error save_zip(const Ref &p_preset, bool p_debug, const String &p_path, Vector *p_so_files = nullptr, EditorExportSaveFunction p_save_func = nullptr); + + Error save_pack_patch(const Ref &p_preset, bool p_debug, const String &p_path, Vector *p_so_files = nullptr, bool p_embed = false, int64_t *r_embedded_start = nullptr, int64_t *r_embedded_size = nullptr); + Error save_zip_patch(const Ref &p_preset, bool p_debug, const String &p_path, Vector *p_so_files = nullptr); virtual bool poll_export() { return false; } virtual int get_options_count() const { return 0; } @@ -307,6 +321,8 @@ public: virtual Error export_project(const Ref &p_preset, bool p_debug, const String &p_path, BitField p_flags = 0) = 0; virtual Error export_pack(const Ref &p_preset, bool p_debug, const String &p_path, BitField p_flags = 0); virtual Error export_zip(const Ref &p_preset, bool p_debug, const String &p_path, BitField p_flags = 0); + virtual Error export_pack_patch(const Ref &p_preset, bool p_debug, const String &p_path, const Vector &p_patches = Vector(), BitField p_flags = 0); + virtual Error export_zip_patch(const Ref &p_preset, bool p_debug, const String &p_path, const Vector &p_patches = Vector(), BitField p_flags = 0); virtual void get_platform_features(List *r_features) const = 0; virtual void resolve_platform_feature_priorities(const Ref &p_preset, HashSet &p_features) {} virtual String get_debug_protocol() const { return "tcp://"; } diff --git a/editor/export/editor_export_platform_extension.cpp b/editor/export/editor_export_platform_extension.cpp index 808a2076e2e..5b75ff19a4c 100644 --- a/editor/export/editor_export_platform_extension.cpp +++ b/editor/export/editor_export_platform_extension.cpp @@ -71,6 +71,8 @@ void EditorExportPlatformExtension::_bind_methods() { GDVIRTUAL_BIND(_export_project, "preset", "debug", "path", "flags"); GDVIRTUAL_BIND(_export_pack, "preset", "debug", "path", "flags"); GDVIRTUAL_BIND(_export_zip, "preset", "debug", "path", "flags"); + GDVIRTUAL_BIND(_export_pack_patch, "preset", "debug", "path", "patches", "flags"); + GDVIRTUAL_BIND(_export_zip_patch, "preset", "debug", "path", "patches", "flags"); GDVIRTUAL_BIND(_get_platform_features); @@ -291,6 +293,44 @@ Error EditorExportPlatformExtension::export_zip(const Ref &p return save_zip(p_preset, p_debug, p_path); } +Error EditorExportPlatformExtension::export_pack_patch(const Ref &p_preset, bool p_debug, const String &p_path, const Vector &p_patches, BitField p_flags) { + ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); + + Error err = _load_patches(p_patches.is_empty() ? p_preset->get_patches() : p_patches); + if (err != OK) { + return err; + } + + Error ret = FAILED; + if (GDVIRTUAL_CALL(_export_pack_patch, p_preset, p_debug, p_path, p_patches, p_flags, ret)) { + _unload_patches(); + return ret; + } + + err = save_pack_patch(p_preset, p_debug, p_path); + _unload_patches(); + return err; +} + +Error EditorExportPlatformExtension::export_zip_patch(const Ref &p_preset, bool p_debug, const String &p_path, const Vector &p_patches, BitField p_flags) { + ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); + + Error err = _load_patches(p_patches.is_empty() ? p_preset->get_patches() : p_patches); + if (err != OK) { + return err; + } + + Error ret = FAILED; + if (GDVIRTUAL_CALL(_export_zip_patch, p_preset, p_debug, p_path, p_patches, p_flags, ret)) { + _unload_patches(); + return ret; + } + + err = save_zip_patch(p_preset, p_debug, p_path); + _unload_patches(); + return err; +} + void EditorExportPlatformExtension::get_platform_features(List *r_features) const { Vector ret; if (GDVIRTUAL_REQUIRED_CALL(_get_platform_features, ret) && r_features) { diff --git a/editor/export/editor_export_platform_extension.h b/editor/export/editor_export_platform_extension.h index 6391e65ac18..d1db9226980 100644 --- a/editor/export/editor_export_platform_extension.h +++ b/editor/export/editor_export_platform_extension.h @@ -136,6 +136,12 @@ public: virtual Error export_zip(const Ref &p_preset, bool p_debug, const String &p_path, BitField p_flags = 0) override; GDVIRTUAL4R(Error, _export_zip, Ref, bool, const String &, BitField); + virtual Error export_pack_patch(const Ref &p_preset, bool p_debug, const String &p_path, const Vector &p_patches = Vector(), BitField p_flags = 0) override; + GDVIRTUAL5R(Error, _export_pack_patch, Ref, bool, const String &, const Vector &, BitField); + + virtual Error export_zip_patch(const Ref &p_preset, bool p_debug, const String &p_path, const Vector &p_patches = Vector(), BitField p_flags = 0) override; + GDVIRTUAL5R(Error, _export_zip_patch, Ref, bool, const String &, const Vector &, BitField); + virtual void get_platform_features(List *r_features) const override; GDVIRTUAL0RC(Vector, _get_platform_features); diff --git a/editor/export/editor_export_platform_pc.cpp b/editor/export/editor_export_platform_pc.cpp index 24d89b7f347..31a5b60d772 100644 --- a/editor/export/editor_export_platform_pc.cpp +++ b/editor/export/editor_export_platform_pc.cpp @@ -194,7 +194,7 @@ Error EditorExportPlatformPC::export_project_data(const Ref int64_t embedded_pos; int64_t embedded_size; - Error err = save_pack(p_preset, p_debug, pck_path, &so_files, p_preset->get("binary_format/embed_pck"), &embedded_pos, &embedded_size); + Error err = save_pack(p_preset, p_debug, pck_path, &so_files, 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.")); diff --git a/editor/export/editor_export_preset.cpp b/editor/export/editor_export_preset.cpp index 9f805666d0a..1ca72348e20 100644 --- a/editor/export/editor_export_preset.cpp +++ b/editor/export/editor_export_preset.cpp @@ -79,6 +79,7 @@ void EditorExportPreset::_bind_methods() { ClassDB::bind_method(D_METHOD("get_include_filter"), &EditorExportPreset::get_include_filter); ClassDB::bind_method(D_METHOD("get_exclude_filter"), &EditorExportPreset::get_exclude_filter); ClassDB::bind_method(D_METHOD("get_custom_features"), &EditorExportPreset::get_custom_features); + ClassDB::bind_method(D_METHOD("get_patches"), &EditorExportPreset::get_patches); ClassDB::bind_method(D_METHOD("get_export_path"), &EditorExportPreset::get_export_path); ClassDB::bind_method(D_METHOD("get_encryption_in_filter"), &EditorExportPreset::get_enc_in_filter); ClassDB::bind_method(D_METHOD("get_encryption_ex_filter"), &EditorExportPreset::get_enc_ex_filter); @@ -366,6 +367,42 @@ EditorExportPreset::FileExportMode EditorExportPreset::get_file_export_mode(cons return p_default; } +void EditorExportPreset::add_patch(const String &p_path, int p_at_pos) { + ERR_FAIL_COND_EDMSG(patches.has(p_path), vformat("Failed to add patch \"%s\". Patches must be unique.", p_path)); + + if (p_at_pos < 0) { + patches.push_back(p_path); + } else { + patches.insert(p_at_pos, p_path); + } + + EditorExport::singleton->save_presets(); +} + +void EditorExportPreset::set_patch(int p_index, const String &p_path) { + remove_patch(p_index); + add_patch(p_path, p_index); +} + +String EditorExportPreset::get_patch(int p_index) { + ERR_FAIL_INDEX_V(p_index, patches.size(), String()); + return patches[p_index]; +} + +void EditorExportPreset::remove_patch(int p_index) { + ERR_FAIL_INDEX(p_index, patches.size()); + patches.remove_at(p_index); + EditorExport::singleton->save_presets(); +} + +void EditorExportPreset::set_patches(const Vector &p_patches) { + patches = p_patches; +} + +Vector EditorExportPreset::get_patches() const { + return patches; +} + void EditorExportPreset::set_custom_features(const String &p_custom_features) { custom_features = p_custom_features; EditorExport::singleton->save_presets(); diff --git a/editor/export/editor_export_preset.h b/editor/export/editor_export_preset.h index f220477461f..af3a23fc50d 100644 --- a/editor/export/editor_export_preset.h +++ b/editor/export/editor_export_preset.h @@ -74,6 +74,8 @@ private: bool advanced_options_enabled = false; bool dedicated_server = false; + Vector patches; + friend class EditorExport; friend class EditorExportPlatform; @@ -144,6 +146,13 @@ public: void set_exclude_filter(const String &p_exclude); String get_exclude_filter() const; + void add_patch(const String &p_path, int p_at_pos = -1); + void set_patch(int p_index, const String &p_path); + String get_patch(int p_index); + void remove_patch(int p_index); + void set_patches(const Vector &p_patches); + Vector get_patches() const; + void set_custom_features(const String &p_custom_features); String get_custom_features() const; diff --git a/editor/export/project_export.cpp b/editor/export/project_export.cpp index be9e0f78ece..f9137082d70 100644 --- a/editor/export/project_export.cpp +++ b/editor/export/project_export.cpp @@ -102,11 +102,13 @@ void ProjectExportDialog::_notification(int p_what) { case NOTIFICATION_THEME_CHANGED: { duplicate_preset->set_icon(presets->get_editor_theme_icon(SNAME("Duplicate"))); delete_preset->set_icon(presets->get_editor_theme_icon(SNAME("Remove"))); + patch_add_btn->set_icon(get_editor_theme_icon(SNAME("Add"))); } break; case NOTIFICATION_READY: { duplicate_preset->set_icon(presets->get_editor_theme_icon(SNAME("Duplicate"))); delete_preset->set_icon(presets->get_editor_theme_icon(SNAME("Remove"))); + patch_add_btn->set_icon(get_editor_theme_icon(SNAME("Add"))); connect(SceneStringName(confirmed), callable_mp(this, &ProjectExportDialog::_export_pck_zip)); _update_export_all(); } break; @@ -248,6 +250,7 @@ void ProjectExportDialog::_edit_preset(int p_index) { duplicate_preset->set_disabled(true); delete_preset->set_disabled(true); sections->hide(); + patches->clear(); export_error->hide(); export_templates_error->hide(); return; @@ -292,6 +295,21 @@ void ProjectExportDialog::_edit_preset(int p_index) { exclude_filters->set_text(current->get_exclude_filter()); server_strip_message->set_visible(current->get_export_filter() == EditorExportPreset::EXPORT_CUSTOMIZED); + patches->clear(); + TreeItem *patch_root = patches->create_item(); + Vector patch_list = current->get_patches(); + for (int i = 0; i < patch_list.size(); i++) { + TreeItem *patch = patches->create_item(patch_root); + const String &patch_path = patch_list[i]; + patch->set_cell_mode(0, TreeItem::CELL_MODE_STRING); + patch->set_editable(0, true); + patch->set_text(0, patch_path.get_file()); + patch->set_tooltip_text(0, patch_path); + patch->set_metadata(0, i); + patch->add_button(0, get_editor_theme_icon(SNAME("Remove")), 0); + patch->add_button(0, get_editor_theme_icon(SNAME("FileBrowse")), 1); + } + _fill_resource_tree(); bool needs_templates; @@ -664,6 +682,7 @@ void ProjectExportDialog::_duplicate_preset() { preset->set_export_filter(current->get_export_filter()); preset->set_include_filter(current->get_include_filter()); preset->set_exclude_filter(current->get_exclude_filter()); + preset->set_patches(current->get_patches()); preset->set_custom_features(current->get_custom_features()); for (const KeyValue &E : current->get_values()) { @@ -720,8 +739,22 @@ Variant ProjectExportDialog::get_drag_data_fw(const Point2 &p_point, Control *p_ return d; } - } + } else if (p_from == patches) { + TreeItem *item = patches->get_item_at_position(p_point); + if (item) { + int item_metadata = item->get_metadata(0); + Dictionary d; + d["type"] = "export_patch"; + d["patch"] = item_metadata; + + Label *label = memnew(Label); + label->set_text(item->get_text(0)); + patches->set_drag_preview(label); + + return d; + } + } return Variant(); } @@ -735,6 +768,18 @@ bool ProjectExportDialog::can_drop_data_fw(const Point2 &p_point, const Variant if (presets->get_item_at_position(p_point, true) < 0 && !presets->is_pos_at_end_of_items(p_point)) { return false; } + } else if (p_from == patches) { + Dictionary d = p_data; + if (d.get("type", "") != "export_patch") { + return false; + } + + TreeItem *item = patches->get_item_at_position(p_point); + if (!item) { + return false; + } + + patches->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN); } return true; @@ -771,6 +816,31 @@ void ProjectExportDialog::drop_data_fw(const Point2 &p_point, const Variant &p_d } else { _edit_preset(presets->get_item_count() - 1); } + } else if (p_from == patches) { + Dictionary d = p_data; + int from_pos = d["patch"]; + + TreeItem *item = patches->get_item_at_position(p_point); + if (!item) { + return; + } + + int to_pos = item->get_metadata(0); + + if (patches->get_drop_section_at_position(p_point) > 0) { + to_pos++; + } + + if (to_pos > from_pos) { + to_pos--; + } + + Ref preset = get_current_preset(); + String patch = preset->get_patch(from_pos); + preset->remove_patch(from_pos); + preset->add_patch(patch, to_pos); + + _update_current_preset(); } } @@ -1026,6 +1096,75 @@ void ProjectExportDialog::_set_file_export_mode(int p_id) { _propagate_file_export_mode(include_files->get_root(), EditorExportPreset::MODE_FILE_NOT_CUSTOMIZED); } +void ProjectExportDialog::_patch_tree_button_clicked(Object *p_item, int p_column, int p_id, int p_mouse_button_index) { + TreeItem *ti = Object::cast_to(p_item); + + patch_index = ti->get_metadata(0); + + Ref current = get_current_preset(); + ERR_FAIL_COND(current.is_null()); + + if (p_id == 0) { + Vector preset_patches = current->get_patches(); + ERR_FAIL_INDEX(patch_index, preset_patches.size()); + patch_erase->set_text(vformat(TTR("Delete patch '%s' from list?"), preset_patches[patch_index].get_file())); + patch_erase->popup_centered(); + } else { + patch_dialog->popup_file_dialog(); + } +} + +void ProjectExportDialog::_patch_tree_item_edited() { + TreeItem *item = patches->get_edited(); + if (!item) { + return; + } + + Ref current = get_current_preset(); + ERR_FAIL_COND(current.is_null()); + + int index = item->get_metadata(0); + String patch_path = item->get_text(0); + + current->set_patch(index, patch_path); + item->set_tooltip_text(0, patch_path); +} + +void ProjectExportDialog::_patch_file_selected(const String &p_path) { + Ref current = get_current_preset(); + ERR_FAIL_COND(current.is_null()); + + String relative_path = ProjectSettings::get_singleton()->get_resource_path().path_to_file(p_path); + + Vector preset_patches = current->get_patches(); + if (patch_index >= preset_patches.size()) { + current->add_patch(relative_path); + } else { + current->set_patch(patch_index, relative_path); + } + + _update_current_preset(); +} + +void ProjectExportDialog::_patch_delete_confirmed() { + Ref current = get_current_preset(); + ERR_FAIL_COND(current.is_null()); + + Vector preset_patches = current->get_patches(); + if (patch_index < preset_patches.size()) { + current->remove_patch(patch_index); + _update_current_preset(); + } +} + +void ProjectExportDialog::_patch_add_pack_pressed() { + Ref current = get_current_preset(); + ERR_FAIL_COND(current.is_null()); + + patch_index = current->get_patches().size(); + patch_dialog->popup_file_dialog(); +} + void ProjectExportDialog::_export_pck_zip() { Ref current = get_current_preset(); ERR_FAIL_COND(current.is_null()); @@ -1044,11 +1183,20 @@ void ProjectExportDialog::_export_pck_zip_selected(const String &p_path) { const Dictionary &fd_option = export_pck_zip->get_selected_options(); bool export_debug = fd_option.get(TTR("Export With Debug"), true); + bool export_as_patch = fd_option.get(TTR("Export As Patch"), true); if (p_path.ends_with(".zip")) { - platform->export_zip(current, export_debug, p_path); + if (export_as_patch) { + platform->export_zip_patch(current, export_debug, p_path); + } else { + platform->export_zip(current, export_debug, p_path); + } } else if (p_path.ends_with(".pck")) { - platform->export_pack(current, export_debug, p_path); + if (export_as_patch) { + platform->export_pack_patch(current, export_debug, p_path); + } else { + platform->export_pack(current, export_debug, p_path); + } } else { ERR_FAIL_MSG("Path must end with .pck or .zip"); } @@ -1386,6 +1534,40 @@ ProjectExportDialog::ProjectExportDialog() { exclude_filters); exclude_filters->connect(SceneStringName(text_changed), callable_mp(this, &ProjectExportDialog::_filter_changed)); + // Patch packages. + + VBoxContainer *patch_vb = memnew(VBoxContainer); + sections->add_child(patch_vb); + patch_vb->set_name(TTR("Patches")); + + patches = memnew(Tree); + patches->set_v_size_flags(Control::SIZE_EXPAND_FILL); + patches->set_hide_root(true); + patches->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); + patches->connect("button_clicked", callable_mp(this, &ProjectExportDialog::_patch_tree_button_clicked)); + patches->connect("item_edited", callable_mp(this, &ProjectExportDialog::_patch_tree_item_edited)); + SET_DRAG_FORWARDING_GCD(patches, ProjectExportDialog); + patches->set_edit_checkbox_cell_only_when_checkbox_is_pressed(true); + patch_vb->add_margin_child(TTR("Base Packs:"), patches, true); + + patch_dialog = memnew(EditorFileDialog); + patch_dialog->add_filter("*.pck", TTR("Godot Project Pack")); + patch_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM); + patch_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE); + patch_dialog->connect("file_selected", callable_mp(this, &ProjectExportDialog::_patch_file_selected)); + add_child(patch_dialog); + + patch_erase = memnew(ConfirmationDialog); + patch_erase->set_ok_button_text(TTR("Delete")); + patch_erase->connect(SceneStringName(confirmed), callable_mp(this, &ProjectExportDialog::_patch_delete_confirmed)); + add_child(patch_erase); + + patch_add_btn = memnew(Button); + patch_add_btn->set_text(TTR("Add Pack")); + patch_add_btn->set_h_size_flags(Control::SIZE_SHRINK_CENTER); + patch_add_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectExportDialog::_patch_add_pack_pressed)); + patch_vb->add_child(patch_add_btn); + // Feature tags. VBoxContainer *feature_vb = memnew(VBoxContainer); @@ -1569,6 +1751,7 @@ ProjectExportDialog::ProjectExportDialog() { export_project->add_option(TTR("Export With Debug"), Vector(), true); export_pck_zip->add_option(TTR("Export With Debug"), Vector(), true); + export_pck_zip->add_option(TTR("Export As Patch"), Vector(), true); set_hide_on_ok(false); diff --git a/editor/export/project_export.h b/editor/export/project_export.h index c3499177f31..e360596be6e 100644 --- a/editor/export/project_export.h +++ b/editor/export/project_export.h @@ -105,6 +105,13 @@ class ProjectExportDialog : public ConfirmationDialog { AcceptDialog *export_all_dialog = nullptr; RBSet feature_set; + + Tree *patches = nullptr; + int patch_index = -1; + EditorFileDialog *patch_dialog = nullptr; + ConfirmationDialog *patch_erase = nullptr; + Button *patch_add_btn = nullptr; + LineEdit *custom_features = nullptr; RichTextLabel *custom_feature_display = nullptr; @@ -148,6 +155,12 @@ class ProjectExportDialog : public ConfirmationDialog { void _tree_popup_edited(bool p_arrow_clicked); void _set_file_export_mode(int p_id); + void _patch_tree_button_clicked(Object *p_item, int p_column, int p_id, int p_mouse_button_index); + void _patch_tree_item_edited(); + void _patch_file_selected(const String &p_path); + void _patch_delete_confirmed(); + void _patch_add_pack_pressed(); + Variant get_drag_data_fw(const Point2 &p_point, Control *p_from); bool can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const; void drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from); diff --git a/main/main.cpp b/main/main.cpp index 439cd385c02..9d350e60e74 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -650,6 +650,8 @@ void Main::print_help(const char *p_binary) { print_help_option("", "The target directory must exist.\n"); print_help_option("--export-debug ", "Export the project in debug mode using the given preset and output path. See --export-release description for other considerations.\n", CLI_OPTION_AVAILABILITY_EDITOR); print_help_option("--export-pack ", "Export the project data only using the given preset and output path. The extension determines whether it will be in PCK or ZIP format.\n", CLI_OPTION_AVAILABILITY_EDITOR); + print_help_option("--export-patch ", "Export pack with changed files only. See --export-pack description for other considerations.\n", CLI_OPTION_AVAILABILITY_EDITOR); + print_help_option("--patches ", "List of patches to use with --export-patch. The list is comma-separated.\n", CLI_OPTION_AVAILABILITY_EDITOR); print_help_option("--install-android-build-template", "Install the Android build template. Used in conjunction with --export-release or --export-debug.\n", CLI_OPTION_AVAILABILITY_EDITOR); #ifndef DISABLE_DEPRECATED // Commands are long; split the description to a second line. @@ -1469,12 +1471,23 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph wait_for_import = true; quit_after = 1; } else if (arg == "--export-release" || arg == "--export-debug" || - arg == "--export-pack") { // Export project + arg == "--export-pack" || arg == "--export-patch") { // Export project // Actually handling is done in start(). editor = true; cmdline_tool = true; wait_for_import = true; main_args.push_back(arg); + } else if (arg == "--patches") { + if (N) { + // Actually handling is done in start(). + main_args.push_back(arg); + main_args.push_back(N->get()); + + N = N->next(); + } else { + OS::get_singleton()->print("Missing comma-separated list of patches after --patches, aborting.\n"); + goto error; + } #ifndef DISABLE_DEPRECATED } else if (arg == "--export") { // For users used to 3.x syntax. OS::get_singleton()->print("The Godot 3 --export option was changed to more explicit --export-release / --export-debug / --export-pack options.\nSee the --help output for details.\n"); @@ -3480,9 +3493,11 @@ int Main::start() { bool doc_tool_implicit_cwd = false; BitField gen_flags; String _export_preset; + Vector patches; bool export_debug = false; bool export_pack_only = false; bool install_android_build_template = false; + bool export_patch = false; #ifdef MODULE_GDSCRIPT_ENABLED String gdscript_docs_path; #endif @@ -3572,6 +3587,14 @@ int Main::start() { editor = true; _export_preset = E->next()->get(); export_pack_only = true; + } else if (E->get() == "--export-patch") { + ERR_FAIL_COND_V_MSG(!editor && !found_project, EXIT_FAILURE, "Please provide a valid project path when exporting, aborting."); + editor = true; + _export_preset = E->next()->get(); + export_pack_only = true; + export_patch = true; + } else if (E->get() == "--patches") { + patches = E->next()->get().split(",", false); #endif } else { // The parameter does not match anything known, don't skip the next argument @@ -3975,7 +3998,7 @@ int Main::start() { sml->get_root()->add_child(editor_node); if (!_export_preset.is_empty()) { - editor_node->export_preset(_export_preset, positional_arg, export_debug, export_pack_only, install_android_build_template); + editor_node->export_preset(_export_preset, positional_arg, export_debug, export_pack_only, install_android_build_template, export_patch, patches); game_path = ""; // Do not load anything. }