Add ability to export patch packs

Co-authored-by: Poq Xert <poqxert@poqxert.ru>
This commit is contained in:
Mikael Hermansson 2024-09-17 12:48:10 +02:00
parent 4254946de9
commit d3be030ea6
18 changed files with 619 additions and 20 deletions

View File

@ -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<PathMD5, PackedFile, PathMD5>::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() {

View File

@ -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<FileAccess> try_open_path(const String &p_path);
_FORCE_INLINE_ bool has_path(const String &p_path);

View File

@ -42,6 +42,18 @@
Creates a PCK archive at [param path] for the specified [param preset].
</description>
</method>
<method name="export_pack_patch">
<return type="int" enum="Error" />
<param index="0" name="preset" type="EditorExportPreset" />
<param index="1" name="debug" type="bool" />
<param index="2" name="path" type="String" />
<param index="3" name="patches" type="PackedStringArray" default="PackedStringArray()" />
<param index="4" name="flags" type="int" enum="EditorExportPlatform.DebugFlags" is_bitfield="true" default="0" />
<description>
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.
</description>
</method>
<method name="export_project">
<return type="int" enum="Error" />
<param index="0" name="preset" type="EditorExportPreset" />
@ -75,6 +87,18 @@
Create a ZIP archive at [param path] for the specified [param preset].
</description>
</method>
<method name="export_zip_patch">
<return type="int" enum="Error" />
<param index="0" name="preset" type="EditorExportPreset" />
<param index="1" name="debug" type="bool" />
<param index="2" name="path" type="String" />
<param index="3" name="patches" type="PackedStringArray" default="PackedStringArray()" />
<param index="4" name="flags" type="int" enum="EditorExportPlatform.DebugFlags" is_bitfield="true" default="0" />
<description>
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.
</description>
</method>
<method name="find_export_template" qualifiers="const">
<return type="Dictionary" />
<param index="0" name="template_file_name" type="String" />
@ -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).
</description>
</method>
<method name="save_pack_patch">
<return type="Dictionary" />
<param index="0" name="preset" type="EditorExportPreset" />
<param index="1" name="debug" type="bool" />
<param index="2" name="path" type="String" />
<description>
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]).
</description>
</method>
<method name="save_zip">
<return type="Dictionary" />
<param index="0" name="preset" type="EditorExportPreset" />
@ -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]).
</description>
</method>
<method name="save_zip_patch">
<return type="Dictionary" />
<param index="0" name="preset" type="EditorExportPreset" />
<param index="1" name="debug" type="bool" />
<param index="2" name="path" type="String" />
<description>
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]).
</description>
</method>
<method name="ssh_push_to_remote" qualifiers="const">
<return type="int" enum="Error" />
<param index="0" name="host" type="String" />

View File

@ -36,7 +36,21 @@
<description>
[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.
</description>
</method>
<method name="_export_pack_patch" qualifiers="virtual">
<return type="int" enum="Error" />
<param index="0" name="preset" type="EditorExportPreset" />
<param index="1" name="debug" type="bool" />
<param index="2" name="path" type="String" />
<param index="3" name="patches" type="PackedStringArray" />
<param index="4" name="flags" type="int" enum="EditorExportPlatform.DebugFlags" is_bitfield="true" />
<description>
[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.
</description>
</method>
<method name="_export_project" qualifiers="virtual">
@ -61,7 +75,21 @@
<description>
[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.
</description>
</method>
<method name="_export_zip_patch" qualifiers="virtual">
<return type="int" enum="Error" />
<param index="0" name="preset" type="EditorExportPreset" />
<param index="1" name="debug" type="bool" />
<param index="2" name="path" type="String" />
<param index="3" name="patches" type="PackedStringArray" />
<param index="4" name="flags" type="int" enum="EditorExportPlatform.DebugFlags" is_bitfield="true" />
<description>
[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.
</description>
</method>
<method name="_get_binary_extensions" qualifiers="virtual const">

View File

@ -109,6 +109,12 @@
Returns export option value or value of environment variable if it is set.
</description>
</method>
<method name="get_patches" qualifiers="const">
<return type="PackedStringArray" />
<description>
Returns the list of packs on which to base a patch export on.
</description>
</method>
<method name="get_preset_name" qualifiers="const">
<return type="String" />
<description>

View File

@ -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<String> &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;
}

View File

@ -246,6 +246,8 @@ private:
bool debug = false;
bool pack_only = false;
bool android_build_template = false;
bool patch = false;
Vector<String> 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<String> &p_patches);
bool is_project_exporting() const;
Control *get_gui_base() { return gui_base; }

View File

@ -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<String>()));
if (config->has_section_key(section, "encrypt_pck")) {
preset->set_enc_pck(config->get_value(section, "encrypt_pck"));

View File

@ -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<uint8_t> &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<String> &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<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &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<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &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<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &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<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &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<ImageTexture> EditorExportPlatform::get_option_icon(int p_index) const {
Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
ERR_FAIL_COND_V(theme.is_null(), Ref<ImageTexture>());
@ -1561,7 +1617,7 @@ Dictionary EditorExportPlatform::_save_pack(const Ref<EditorExportPreset> &p_pre
Vector<SharedObject> 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<EditorExportPreset> &p_pres
return ret;
}
Error EditorExportPlatform::save_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *p_so_files, bool p_embed, int64_t *r_embedded_start, int64_t *r_embedded_size) {
Dictionary EditorExportPlatform::_save_pack_patch(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) {
Vector<SharedObject> 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<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) {
Vector<SharedObject> 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<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *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<DirAccess> 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<EditorExportPreset> &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<EditorExportPreset> &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<FileAccess> f;
@ -1831,28 +1939,56 @@ Error EditorExportPlatform::save_pack(const Ref<EditorExportPreset> &p_preset, b
return OK;
}
Error EditorExportPlatform::save_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *p_so_files) {
Error EditorExportPlatform::save_pack_patch(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *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<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *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<FileAccess> 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<DirAccess> 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<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *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<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> 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<EditorExportPreset> &p_preset,
return save_zip(p_preset, p_debug, p_path);
}
Error EditorExportPlatform::export_pack_patch(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, const Vector<String> &p_patches, BitField<EditorExportPlatform::DebugFlags> 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<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, const Vector<String> &p_patches, BitField<EditorExportPlatform::DebugFlags> 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<String> EditorExportPlatform::gen_export_flags(BitField<EditorExportPlatform::DebugFlags> p_flags) {
Vector<String> 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);

View File

@ -101,6 +101,7 @@ private:
void *zip = nullptr;
EditorProgress *ep = nullptr;
Vector<SharedObject> *so_files = nullptr;
int file_count = 0;
};
Vector<ExportMessage> messages;
@ -109,10 +110,14 @@ private:
void _export_find_customized_resources(const Ref<EditorExportPreset> &p_preset, EditorFileSystemDirectory *p_dir, EditorExportPreset::FileExportMode p_mode, HashSet<String> &p_paths);
void _export_find_dependencies(const String &p_path, HashSet<String> &p_paths);
static bool _check_hash(const uint8_t *p_hash, const Vector<uint8_t> &p_data);
static Error _save_pack_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
static Error _save_pack_patch_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &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<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
static Error _save_zip_patch_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &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<String> &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<String> &p_scp_args, const String &p_src_file, const String &p_dst_file) const;
Error _load_patches(const Vector<String> &p_patches);
void _unload_patches();
public:
virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const = 0;
@ -284,8 +292,14 @@ public:
Dictionary _save_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, bool p_embed = false);
Dictionary _save_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path);
Error save_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *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<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *p_so_files = nullptr);
Dictionary _save_pack_patch(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path);
Dictionary _save_zip_patch(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path);
Error save_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *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<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *p_so_files = nullptr, EditorExportSaveFunction p_save_func = nullptr);
Error save_pack_patch(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *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<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *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<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) = 0;
virtual Error export_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0);
virtual Error export_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0);
virtual Error export_pack_patch(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, const Vector<String> &p_patches = Vector<String>(), BitField<EditorExportPlatform::DebugFlags> p_flags = 0);
virtual Error export_zip_patch(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, const Vector<String> &p_patches = Vector<String>(), BitField<EditorExportPlatform::DebugFlags> p_flags = 0);
virtual void get_platform_features(List<String> *r_features) const = 0;
virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) {}
virtual String get_debug_protocol() const { return "tcp://"; }

View File

@ -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<EditorExportPreset> &p
return save_zip(p_preset, p_debug, p_path);
}
Error EditorExportPlatformExtension::export_pack_patch(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, const Vector<String> &p_patches, BitField<EditorExportPlatform::DebugFlags> 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<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, const Vector<String> &p_patches, BitField<EditorExportPlatform::DebugFlags> 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<String> *r_features) const {
Vector<String> ret;
if (GDVIRTUAL_REQUIRED_CALL(_get_platform_features, ret) && r_features) {

View File

@ -136,6 +136,12 @@ public:
virtual Error export_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override;
GDVIRTUAL4R(Error, _export_zip, Ref<EditorExportPreset>, bool, const String &, BitField<EditorExportPlatform::DebugFlags>);
virtual Error export_pack_patch(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, const Vector<String> &p_patches = Vector<String>(), BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override;
GDVIRTUAL5R(Error, _export_pack_patch, Ref<EditorExportPreset>, bool, const String &, const Vector<String> &, BitField<EditorExportPlatform::DebugFlags>);
virtual Error export_zip_patch(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, const Vector<String> &p_patches = Vector<String>(), BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override;
GDVIRTUAL5R(Error, _export_zip_patch, Ref<EditorExportPreset>, bool, const String &, const Vector<String> &, BitField<EditorExportPlatform::DebugFlags>);
virtual void get_platform_features(List<String> *r_features) const override;
GDVIRTUAL0RC(Vector<String>, _get_platform_features);

View File

@ -194,7 +194,7 @@ Error EditorExportPlatformPC::export_project_data(const Ref<EditorExportPreset>
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."));

View File

@ -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<String> &p_patches) {
patches = p_patches;
}
Vector<String> 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();

View File

@ -74,6 +74,8 @@ private:
bool advanced_options_enabled = false;
bool dedicated_server = false;
Vector<String> 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<String> &p_patches);
Vector<String> get_patches() const;
void set_custom_features(const String &p_custom_features);
String get_custom_features() const;

View File

@ -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<String> 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<StringName, Variant> &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<EditorExportPreset> 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<TreeItem>(p_item);
patch_index = ti->get_metadata(0);
Ref<EditorExportPreset> current = get_current_preset();
ERR_FAIL_COND(current.is_null());
if (p_id == 0) {
Vector<String> 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<EditorExportPreset> 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<EditorExportPreset> 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<String> 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<EditorExportPreset> current = get_current_preset();
ERR_FAIL_COND(current.is_null());
Vector<String> 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<EditorExportPreset> 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<EditorExportPreset> 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<String>(), true);
export_pck_zip->add_option(TTR("Export With Debug"), Vector<String>(), true);
export_pck_zip->add_option(TTR("Export As Patch"), Vector<String>(), true);
set_hide_on_ok(false);

View File

@ -105,6 +105,13 @@ class ProjectExportDialog : public ConfirmationDialog {
AcceptDialog *export_all_dialog = nullptr;
RBSet<String> 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);

View File

@ -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 <preset> <path>", "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 <preset> <path>", "Export the project data only using the given preset and output path. The <path> extension determines whether it will be in PCK or ZIP format.\n", CLI_OPTION_AVAILABILITY_EDITOR);
print_help_option("--export-patch <preset> <path>", "Export pack with changed files only. See --export-pack description for other considerations.\n", CLI_OPTION_AVAILABILITY_EDITOR);
print_help_option("--patches <paths>", "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<DocTools::GenerateFlags> gen_flags;
String _export_preset;
Vector<String> 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.
}