From 0cc88f34dabb27af61ef77a680653a0290e31055 Mon Sep 17 00:00:00 2001 From: Mikael Hermansson Date: Fri, 24 Oct 2025 16:10:45 +0200 Subject: [PATCH] Add support for delta encoding to patch PCKs --- core/io/delta_encoding.cpp | 110 ++++++++++ core/io/delta_encoding.h | 39 ++++ core/io/file_access_pack.cpp | 49 ++++- core/io/file_access_pack.h | 11 +- core/io/file_access_patched.cpp | 203 ++++++++++++++++++ core/io/file_access_patched.h | 84 ++++++++ editor/export/editor_export.cpp | 22 ++ editor/export/editor_export_platform.cpp | 152 ++++++++----- editor/export/editor_export_platform.h | 27 ++- editor/export/editor_export_preset.cpp | 48 ++++- editor/export/editor_export_preset.h | 22 ++ editor/export/project_export.cpp | 135 +++++++++++- editor/export/project_export.h | 12 ++ platform/android/export/export_plugin.cpp | 10 +- platform/android/export/export_plugin.h | 8 +- .../android/export/gradle_export_util.cpp | 7 +- platform/android/export/gradle_export_util.h | 4 +- 17 files changed, 851 insertions(+), 92 deletions(-) create mode 100644 core/io/delta_encoding.cpp create mode 100644 core/io/delta_encoding.h create mode 100644 core/io/file_access_patched.cpp create mode 100644 core/io/file_access_patched.h diff --git a/core/io/delta_encoding.cpp b/core/io/delta_encoding.cpp new file mode 100644 index 00000000000..d74af768561 --- /dev/null +++ b/core/io/delta_encoding.cpp @@ -0,0 +1,110 @@ +/**************************************************************************/ +/* delta_encoding.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "delta_encoding.h" + +#include + +#define ERR_FAIL_ZSTD_V_MSG(m_result, m_retval, m_msg) \ + ERR_FAIL_COND_V_MSG(ZSTD_isError(m_result), m_retval, vformat("%s Zstandard reported error code %d: \"%s\".", m_msg, ZSTD_getErrorCode(m_result), ZSTD_getErrorString(ZSTD_getErrorCode(m_result)))) + +struct ZstdCompressionContext { + ZSTD_CCtx *context = ZSTD_createCCtx(); + ~ZstdCompressionContext() { ZSTD_freeCCtx(context); } + operator ZSTD_CCtx *() { return context; } +}; + +struct ZstdDecompressionContext { + ZSTD_DCtx *context = ZSTD_createDCtx(); + ~ZstdDecompressionContext() { ZSTD_freeDCtx(context); } + operator ZSTD_DCtx *() { return context; } +}; + +static constexpr uint8_t DELTA_MAGIC[4] = { 'G', 'D', 'D', 'L' }; +static constexpr uint8_t DELTA_VERSION_NUMBER = 1; +static constexpr size_t DELTA_HEADER_SIZE = 5; + +Error DeltaEncoding::encode_delta(Span p_old_data, Span p_new_data, Vector &r_delta, int p_compression_level) { + size_t zstd_result = ZSTD_compressBound(p_new_data.size()); + ERR_FAIL_ZSTD_V_MSG(zstd_result, FAILED, "Failed to encode delta. Calculating compression bounds failed."); + + r_delta.reserve_exact(DELTA_HEADER_SIZE + zstd_result); + r_delta.resize(DELTA_HEADER_SIZE + zstd_result); + + memcpy(r_delta.ptrw(), DELTA_MAGIC, 4); + r_delta.write[4] = DELTA_VERSION_NUMBER; + + ZstdCompressionContext zstd_context; + + ZSTD_parameters zstd_params = ZSTD_getParams(p_compression_level, p_new_data.size(), p_old_data.size()); + zstd_params.fParams.contentSizeFlag = 1; + zstd_params.fParams.checksumFlag = 1; + + zstd_result = ZSTD_CCtx_setParams(zstd_context, zstd_params); + ERR_FAIL_ZSTD_V_MSG(zstd_result, FAILED, "Failed to encode delta. Setting compression parameters failed."); + + zstd_result = ZSTD_CCtx_refPrefix(zstd_context, p_old_data.ptr(), p_old_data.size()); + ERR_FAIL_ZSTD_V_MSG(zstd_result, FAILED, "Failed to encode delta. Setting prefix dictionary failed."); + + zstd_result = ZSTD_compress2(zstd_context, r_delta.ptrw() + DELTA_HEADER_SIZE, r_delta.size() - DELTA_HEADER_SIZE, p_new_data.ptr(), p_new_data.size()); + ERR_FAIL_ZSTD_V_MSG(zstd_result, FAILED, "Failed to encode delta. Compression failed."); + + r_delta.resize(DELTA_HEADER_SIZE + zstd_result); + + return OK; +} + +Error DeltaEncoding::decode_delta(Span p_old_data, Span p_delta, Vector &r_new_data) { + ERR_FAIL_COND_V_MSG(p_delta.size() < DELTA_HEADER_SIZE, ERR_INVALID_DATA, vformat("Failed to decode delta. File size (%d) is too small.", p_delta.size())); + + uint8_t magic[4]; + memcpy(magic, p_delta.ptr(), sizeof(magic)); + uint8_t version = p_delta[4]; + + ERR_FAIL_COND_V_MSG(memcmp(magic, DELTA_MAGIC, 4) != 0, ERR_FILE_CORRUPT, "Failed to decode delta. Header is invalid."); + ERR_FAIL_COND_V_MSG(version != DELTA_VERSION_NUMBER, ERR_FILE_UNRECOGNIZED, vformat("Failed to decode delta. Expected version %d but found %d.", DELTA_VERSION_NUMBER, version)); + + size_t zstd_result = ZSTD_findDecompressedSize(p_delta.ptr() + DELTA_HEADER_SIZE, p_delta.size() - DELTA_HEADER_SIZE); + ERR_FAIL_ZSTD_V_MSG(zstd_result, FAILED, "Failed to decode delta. Unable to find decompressed size."); + + r_new_data.reserve_exact(zstd_result); + r_new_data.resize(zstd_result); + + ZstdDecompressionContext zstd_context; + + zstd_result = ZSTD_DCtx_refPrefix(zstd_context, p_old_data.ptr(), p_old_data.size()); + ERR_FAIL_ZSTD_V_MSG(zstd_result, FAILED, "Failed to decode delta. Setting prefix dictionary failed."); + + zstd_result = ZSTD_decompressDCtx(zstd_context, r_new_data.ptrw(), r_new_data.size(), p_delta.ptr() + DELTA_HEADER_SIZE, p_delta.size() - DELTA_HEADER_SIZE); + ERR_FAIL_ZSTD_V_MSG(zstd_result, FAILED, "Failed to decode delta. Decompression failed."); + ERR_FAIL_COND_V(zstd_result != (size_t)r_new_data.size(), ERR_FILE_CORRUPT); + + return OK; +} diff --git a/core/io/delta_encoding.h b/core/io/delta_encoding.h new file mode 100644 index 00000000000..835f1e84091 --- /dev/null +++ b/core/io/delta_encoding.h @@ -0,0 +1,39 @@ +/**************************************************************************/ +/* delta_encoding.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "core/io/file_access.h" + +class DeltaEncoding { +public: + static Error encode_delta(Span p_old_data, Span p_new_data, Vector &r_delta, int p_compression_level = 19); + static Error decode_delta(Span p_old_data, Span p_delta, Vector &r_new_data); +}; diff --git a/core/io/file_access_pack.cpp b/core/io/file_access_pack.cpp index 6f46769e42b..89967c15722 100644 --- a/core/io/file_access_pack.cpp +++ b/core/io/file_access_pack.cpp @@ -31,6 +31,7 @@ #include "file_access_pack.h" #include "core/io/file_access_encrypted.h" +#include "core/io/file_access_patched.h" #include "core/object/script_language.h" #include "core/os/os.h" #include "core/version.h" @@ -45,7 +46,7 @@ Error PackedData::add_pack(const String &p_path, bool p_replace_files, uint64_t return ERR_FILE_UNRECOGNIZED; } -void PackedData::add_path(const String &p_pkg_path, const String &p_path, uint64_t p_ofs, uint64_t p_size, const uint8_t *p_md5, PackSource *p_src, bool p_replace_files, bool p_encrypted, bool p_bundle) { +void PackedData::add_path(const String &p_pkg_path, const String &p_path, uint64_t p_ofs, uint64_t p_size, const uint8_t *p_md5, PackSource *p_src, bool p_replace_files, bool p_encrypted, bool p_bundle, bool p_delta) { String simplified_path = p_path.simplify_path().trim_prefix("res://"); PathMD5 pmd5(simplified_path.md5_buffer()); @@ -54,6 +55,7 @@ void PackedData::add_path(const String &p_pkg_path, const String &p_path, uint64 PackedFile pf; pf.encrypted = p_encrypted; pf.bundle = p_bundle; + pf.delta = p_delta; pf.pack = p_pkg_path; pf.offset = p_ofs; pf.size = p_size; @@ -62,8 +64,11 @@ void PackedData::add_path(const String &p_pkg_path, const String &p_path, uint64 } pf.src = p_src; - if (!exists || p_replace_files) { + if (p_delta) { + delta_patches[pmd5].push_back(pf); + } else if (!exists || p_replace_files) { files[pmd5] = pf; + delta_patches[pmd5].clear(); } if (!exists) { @@ -137,6 +142,28 @@ uint8_t *PackedData::get_file_hash(const String &p_path) { return E->value.md5; } +Vector PackedData::get_delta_patches(const String &p_path) const { + String simplified_path = p_path.simplify_path().trim_prefix("res://"); + PathMD5 pmd5(simplified_path.md5_buffer()); + HashMap, PathMD5>::ConstIterator E = delta_patches.find(pmd5); + if (!E) { + return Vector(); + } + + return E->value; +} + +bool PackedData::has_delta_patches(const String &p_path) const { + String simplified_path = p_path.simplify_path().trim_prefix("res://"); + PathMD5 pmd5(simplified_path.md5_buffer()); + HashMap, PathMD5>::ConstIterator E = delta_patches.find(pmd5); + if (!E) { + return false; + } + + return !E->value.is_empty(); +} + HashSet PackedData::get_file_paths() const { HashSet file_paths; _get_file_paths(root, root->name, file_paths); @@ -155,6 +182,7 @@ void PackedData::_get_file_paths(PackedDir *p_dir, const String &p_parent_dir, H void PackedData::clear() { files.clear(); + delta_patches.clear(); _free_packed_dirs(root); root = memnew(PackedDir); } @@ -322,7 +350,7 @@ bool PackedSourcePCK::try_open_pack(const String &p_path, bool p_replace_files, if (flags & PACK_FILE_REMOVAL) { // The file was removed. PackedData::get_singleton()->remove_path(path); } else { - PackedData::get_singleton()->add_path(p_path, path, file_base + ofs, size, md5, this, p_replace_files, (flags & PACK_FILE_ENCRYPTED), sparse_bundle); + PackedData::get_singleton()->add_path(p_path, path, file_base + ofs, size, md5, this, p_replace_files, (flags & PACK_FILE_ENCRYPTED), sparse_bundle, (flags & PACK_FILE_DELTA)); } } @@ -330,7 +358,17 @@ bool PackedSourcePCK::try_open_pack(const String &p_path, bool p_replace_files, } Ref PackedSourcePCK::get_file(const String &p_path, PackedData::PackedFile *p_file) { - return memnew(FileAccessPack(p_path, *p_file)); + Ref file(memnew(FileAccessPack(p_path, *p_file))); + + if (PackedData::get_singleton()->has_delta_patches(p_path)) { + Ref file_patched; + file_patched.instantiate(); + Error err = file_patched->open_custom(file); + ERR_FAIL_COND_V(err != OK, Ref()); + file = file_patched; + } + + return file; } ////////////////////////////////////////////////////////////////// @@ -362,7 +400,7 @@ void PackedSourceDirectory::add_directory(const String &p_path, bool p_replace_f for (const String &file_name : da->get_files()) { String file_path = p_path.path_join(file_name); uint8_t md5[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - PackedData::get_singleton()->add_path(p_path, file_path, 0, 0, md5, this, p_replace_files, false, false); + PackedData::get_singleton()->add_path(p_path, file_path, 0, 0, md5, this, p_replace_files, false, false, false); } for (const String &sub_dir_name : da->get_directories()) { @@ -470,6 +508,7 @@ void FileAccessPack::close() { } FileAccessPack::FileAccessPack(const String &p_path, const PackedData::PackedFile &p_file) { + path = p_path; pf = p_file; if (pf.bundle) { String simplified_path = p_path.simplify_path(); diff --git a/core/io/file_access_pack.h b/core/io/file_access_pack.h index c2c7575542c..121372b51ef 100644 --- a/core/io/file_access_pack.h +++ b/core/io/file_access_pack.h @@ -54,6 +54,7 @@ enum PackFlags { enum PackFileFlags { PACK_FILE_ENCRYPTED = 1 << 0, PACK_FILE_REMOVAL = 1 << 1, + PACK_FILE_DELTA = 1 << 2, }; class PackSource; @@ -72,6 +73,7 @@ public: PackSource *src = nullptr; bool encrypted; bool bundle; + bool delta; }; private: @@ -103,6 +105,7 @@ private: }; HashMap files; + HashMap, PathMD5> delta_patches; Vector sources; @@ -116,9 +119,11 @@ 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, bool p_bundle = false); // for PackSource + void add_path(const String &p_pkg_path, const String &p_path, uint64_t p_ofs, uint64_t p_size, const uint8_t *p_md5, PackSource *p_src, bool p_replace_files, bool p_encrypted = false, bool p_bundle = false, bool p_delta = false); // for PackSource void remove_path(const String &p_path); uint8_t *get_file_hash(const String &p_path); + Vector get_delta_patches(const String &p_path) const; + bool has_delta_patches(const String &p_path) const; HashSet get_file_paths() const; void set_disabled(bool p_disabled) { disabled = p_disabled; } @@ -166,6 +171,7 @@ class FileAccessPack : public FileAccess { GDSOFTCLASS(FileAccessPack, FileAccess); PackedData::PackedFile pf; + String path; mutable uint64_t pos; mutable bool eof; uint64_t off; @@ -186,6 +192,9 @@ class FileAccessPack : public FileAccess { public: virtual bool is_open() const override; + virtual String get_path() const override { return path; } + virtual String get_path_absolute() const override { return path; } + virtual void seek(uint64_t p_position) override; virtual void seek_end(int64_t p_position = 0) override; virtual uint64_t get_position() const override; diff --git a/core/io/file_access_patched.cpp b/core/io/file_access_patched.cpp new file mode 100644 index 00000000000..8c520b68769 --- /dev/null +++ b/core/io/file_access_patched.cpp @@ -0,0 +1,203 @@ +/**************************************************************************/ +/* file_access_patched.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "file_access_patched.h" + +#include "file_access_pack.h" + +#include "core/io/delta_encoding.h" +#include "core/os/os.h" + +Error FileAccessPatched::_apply_patch() const { + ERR_FAIL_COND_V(!is_open(), FAILED); + + String path = old_file->get_path(); + Vector delta_patches = PackedData::get_singleton()->get_delta_patches(path); + Vector old_file_data = old_file->get_buffer(old_file->get_length()); + + for (int i = 0; i < delta_patches.size(); ++i) { + const PackedData::PackedFile &delta_patch = delta_patches[i]; + ERR_FAIL_COND_V(delta_patch.bundle, FAILED); + + Error err = OK; + + uint64_t total_usec_start = OS::get_singleton()->get_ticks_usec(); + uint64_t io_usec_start = OS::get_singleton()->get_ticks_usec(); + + Ref patch_file = FileAccess::open(delta_patch.pack, FileAccess::READ, &err); + ERR_FAIL_COND_V(err != OK, err); + + patch_file->seek(delta_patch.offset); + ERR_FAIL_COND_V(patch_file->get_error() != OK, patch_file->get_error()); + + Vector patch_data = patch_file->get_buffer(delta_patch.size); + ERR_FAIL_COND_V(patch_data.is_empty(), ERR_FILE_CANT_READ); + + uint64_t io_usec_end = OS::get_singleton()->get_ticks_usec(); + uint64_t decode_usec_start = OS::get_singleton()->get_ticks_usec(); + + Vector new_file_data; + err = DeltaEncoding::decode_delta(old_file_data, patch_data, new_file_data); + ERR_FAIL_COND_V_MSG(err != OK, err, vformat("Failed to apply delta patch (%d of %d) to \"%s\".", i + 1, delta_patches.size(), path)); + + uint64_t decode_usec_end = OS::get_singleton()->get_ticks_usec(); + + old_file_data = new_file_data; + + uint64_t total_usec_end = OS::get_singleton()->get_ticks_usec(); + + print_verbose(vformat(U"Applied delta patch to \"%s\" from \"%s\" in %d μs (%d μs I/O, %d μs decoding).", path, delta_patch.pack.get_file(), total_usec_end - total_usec_start, io_usec_end - io_usec_start, decode_usec_end - decode_usec_start)); + } + + patched_file_data = old_file_data; + patched_file.instantiate(); + return patched_file->open_custom(patched_file_data.ptr(), patched_file_data.size()); +} + +bool FileAccessPatched::_try_apply_patch() const { + if (last_error != OK) { + return false; + } + + if (patched_file.is_valid()) { + return true; + } + + last_error = _apply_patch(); + return last_error == OK; +} + +Error FileAccessPatched::open_custom(const Ref &p_old_file) { + close(); + + if (!p_old_file->is_open()) { + last_error = ERR_FILE_CANT_OPEN; + return last_error; + } + + old_file = p_old_file; + + return OK; +} + +bool FileAccessPatched::is_open() const { + return old_file.is_valid() && old_file->is_open(); +} + +void FileAccessPatched::seek(uint64_t p_position) { + if (!_try_apply_patch()) { + return; + } + + patched_file->seek(p_position); +} + +void FileAccessPatched::seek_end(int64_t p_position) { + if (!_try_apply_patch()) { + return; + } + + patched_file->seek_end(p_position); +} + +uint64_t FileAccessPatched::get_position() const { + if (!_try_apply_patch()) { + return 0; + } + + return patched_file->get_position(); +} + +uint64_t FileAccessPatched::get_length() const { + if (!_try_apply_patch()) { + return 0; + } + + return patched_file->get_length(); +} + +bool FileAccessPatched::eof_reached() const { + if (!_try_apply_patch()) { + return true; + } + + return patched_file->eof_reached(); +} + +Error FileAccessPatched::get_error() const { + if (last_error != OK) { + return last_error; + } + + if (patched_file.is_valid()) { + Error inner_error = patched_file->get_error(); + if (inner_error != OK) { + return inner_error; + } + } + + return last_error; +} + +bool FileAccessPatched::store_buffer(const uint8_t *p_src, uint64_t p_length) { + if (!_try_apply_patch()) { + return false; + } + + return patched_file->store_buffer(p_src, p_length); +} + +uint64_t FileAccessPatched::get_buffer(uint8_t *p_dst, uint64_t p_length) const { + if (!_try_apply_patch()) { + return 0; + } + + return patched_file->get_buffer(p_dst, p_length); +} + +void FileAccessPatched::flush() { + if (!_try_apply_patch()) { + return; + } + + patched_file->flush(); +} + +void FileAccessPatched::close() { + old_file = Ref(); + patched_file = Ref(); + patched_file_data.clear(); + last_error = OK; +} + +bool FileAccessPatched::file_exists(const String &p_name) { + ERR_FAIL_COND_V(old_file.is_null(), false); + return old_file->file_exists(p_name); +} diff --git a/core/io/file_access_patched.h b/core/io/file_access_patched.h new file mode 100644 index 00000000000..62cfb7b8709 --- /dev/null +++ b/core/io/file_access_patched.h @@ -0,0 +1,84 @@ +/**************************************************************************/ +/* file_access_patched.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "file_access.h" +#include "file_access_memory.h" + +class FileAccessPatched : public FileAccess { + GDSOFTCLASS(FileAccessPatched, FileAccess); + + Ref old_file; + mutable Vector patched_file_data; + mutable Ref patched_file; + mutable Error last_error = OK; + + Error _apply_patch() const; + bool _try_apply_patch() const; + +protected: + virtual BitField _get_unix_permissions(const String &p_file) override { return 0; } + virtual Error _set_unix_permissions(const String &p_file, BitField p_permissions) override { return FAILED; } + + virtual bool _get_hidden_attribute(const String &p_file) override { return false; } + virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; } + + virtual bool _get_read_only_attribute(const String &p_file) override { return false; } + virtual Error _set_read_only_attribute(const String &p_file, bool p_ro) override { return ERR_UNAVAILABLE; } + + virtual uint64_t _get_modified_time(const String &p_file) override { return 0; } + virtual uint64_t _get_access_time(const String &p_file) override { return 0; } + virtual int64_t _get_size(const String &p_file) override { return -1; } + + virtual Error open_internal(const String &p_path, int p_mode_flags) override { return ERR_UNAVAILABLE; } + +public: + Error open_custom(const Ref &p_old_file); + + virtual bool is_open() const override; + + virtual void seek(uint64_t p_position) override; + virtual void seek_end(int64_t p_position = 0) override; + + virtual uint64_t get_position() const override; + virtual uint64_t get_length() const override; + virtual bool eof_reached() const override; + virtual Error get_error() const override; + + virtual bool store_buffer(const uint8_t *p_src, uint64_t p_length) override; + virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; + virtual Error resize(int64_t p_length) override { return ERR_UNAVAILABLE; } + + virtual void flush() override; + virtual void close() override; + + virtual bool file_exists(const String &p_name) override; +}; diff --git a/editor/export/editor_export.cpp b/editor/export/editor_export.cpp index b13b7935c05..fb5c2287d25 100644 --- a/editor/export/editor_export.cpp +++ b/editor/export/editor_export.cpp @@ -83,7 +83,13 @@ 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, "patch_delta_encoding", preset->is_patch_delta_encoding_enabled()); + config->set_value(section, "patch_delta_compression_level_zstd", preset->get_patch_delta_zstd_level()); + config->set_value(section, "patch_delta_min_reduction", preset->get_patch_delta_min_reduction()); + config->set_value(section, "patch_delta_include_filters", preset->get_patch_delta_include_filter()); + config->set_value(section, "patch_delta_exclude_filters", preset->get_patch_delta_exclude_filter()); config->set_value(section, "encryption_include_filters", preset->get_enc_in_filter()); config->set_value(section, "encryption_exclude_filters", preset->get_enc_ex_filter()); @@ -332,6 +338,22 @@ void EditorExport::load_config() { 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, "patch_delta_encoding")) { + preset->set_patch_delta_encoding_enabled(config->get_value(section, "patch_delta_encoding")); + } + if (config->has_section_key(section, "patch_delta_compression_level_zstd")) { + preset->set_patch_delta_zstd_level(config->get_value(section, "patch_delta_compression_level_zstd")); + } + if (config->has_section_key(section, "patch_delta_min_reduction")) { + preset->set_patch_delta_min_reduction(config->get_value(section, "patch_delta_min_reduction")); + } + if (config->has_section_key(section, "patch_delta_include_filters")) { + preset->set_patch_delta_include_filter(config->get_value(section, "patch_delta_include_filters")); + } + if (config->has_section_key(section, "patch_delta_exclude_filters")) { + preset->set_patch_delta_exclude_filter(config->get_value(section, "patch_delta_exclude_filters")); + } + if (config->has_section_key(section, "seed")) { preset->set_seed(config->get_value(section, "seed")); } diff --git a/editor/export/editor_export_platform.cpp b/editor/export/editor_export_platform.cpp index 34fa344060b..84c2e1ac9f7 100644 --- a/editor/export/editor_export_platform.cpp +++ b/editor/export/editor_export_platform.cpp @@ -35,6 +35,7 @@ #include "core/config/project_settings.h" #include "core/crypto/crypto_core.h" #include "core/extension/gdextension.h" +#include "core/io/delta_encoding.h" #include "core/io/dir_access.h" #include "core/io/file_access_encrypted.h" #include "core/io/file_access_pack.h" // PACK_HEADER_MAGIC, PACK_FORMAT_VERSION @@ -66,12 +67,12 @@ class EditorExportSaveProxy { public: bool has_saved(const String &p_path) const { return saved_paths.has(p_path); } - Error save_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, uint64_t p_seed) { + Error save_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta) { if (tracking_saves) { saved_paths.insert(p_path.simplify_path().trim_prefix("res://")); } - return save_func(p_userdata, p_path, p_data, p_file, p_total, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed); + return save_func(p_preset, p_userdata, p_path, p_data, p_file, p_total, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, p_delta); } EditorExportSaveProxy(EditorExportPlatform::EditorExportSaveFunction p_save_func, bool p_track_saves) : @@ -218,26 +219,6 @@ 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()) { @@ -310,7 +291,7 @@ Error EditorExportPlatform::_encrypt_and_store_data(Ref p_fd, const return OK; } -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, uint64_t p_seed) { +Error EditorExportPlatform::_save_pack_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta) { ERR_FAIL_COND_V_MSG(p_total < 1, ERR_PARAMETER_RANGE_ERROR, "Must select at least one file to export."); PackData *pd = (PackData *)p_userdata; @@ -328,6 +309,7 @@ Error EditorExportPlatform::_save_pack_file(void *p_userdata, const String &p_pa sd.path_utf8 = simplified_path.trim_prefix("res://").utf8(); sd.ofs = (pd->use_sparse_pck) ? 0 : pd->f->get_position(); sd.size = p_data.size(); + sd.delta = p_delta; Error err = _encrypt_and_store_data(ftmp, simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, sd.encrypted); if (err != OK) { return err; @@ -363,15 +345,67 @@ 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, uint64_t p_seed) { - if (_check_hash(PackedData::get_singleton()->get_file_hash(p_path), p_data)) { - return OK; +Error EditorExportPlatform::_save_pack_patch_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta) { + Ref old_file = PackedData::get_singleton()->try_open_path(p_path); + if (old_file.is_null()) { + return _save_pack_file(p_preset, p_userdata, p_path, p_data, p_file, p_total, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, false); } - return _save_pack_file(p_userdata, p_path, p_data, p_file, p_total, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed); + Vector old_data = old_file->get_buffer(old_file->get_length()); + + // We can't rely on the MD5 as stored in the PCKs, since delta patches could have made it stale. + if (p_data == old_data) { + return OK; // Do nothing if the file hasn't changed. + } + + if (!p_preset->is_patch_delta_encoding_enabled()) { + return _save_pack_file(p_preset, p_userdata, p_path, p_data, p_file, p_total, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, false); + } + + bool delta = false; + + for (const String &filter : p_preset->get_patch_delta_include_filter().split(",", false)) { + String filter_stripped = filter.strip_edges(); + if (p_path.matchn(filter_stripped) || p_path.trim_prefix("res://").matchn(filter_stripped)) { + delta = true; + break; + } + } + + for (const String &filter : p_preset->get_patch_delta_exclude_filter().split(",", false)) { + String filter_stripped = filter.strip_edges(); + if (p_path.matchn(filter_stripped) || p_path.trim_prefix("res://").matchn(filter_stripped)) { + delta = false; + break; + } + } + + Vector patch_data = p_data; + + if (delta) { + Error err = DeltaEncoding::encode_delta(old_data, p_data, patch_data, p_preset->get_patch_delta_zstd_level()); + if (err != OK) { + return err; + } + + int64_t reduction_bytes = MAX(0, p_data.size() - patch_data.size()); + double reduction_ratio = reduction_bytes / (double)p_data.size(); + + if (reduction_ratio >= p_preset->get_patch_delta_min_reduction()) { + print_verbose(vformat("Used delta encoding for patch of \"%s\", resulting in a patch of %d bytes, which reduced the size by %.1f%% (%d bytes) compared to the actual file.", p_path, patch_data.size(), reduction_ratio * 100, reduction_bytes)); + } else { + print_verbose(vformat("Skipped delta encoding for patch of \"%s\", as it resulted in a patch of %d bytes, which only reduced the size by %.1f%% (%d bytes) compared to the actual file.", p_path, patch_data.size(), reduction_ratio * 100, reduction_bytes)); + patch_data = p_data; + delta = false; + } + } else { + print_verbose(vformat("Skipped delta encoding for patch of \"%s\", due to include/exclude filters.", p_path)); + } + + return _save_pack_file(p_preset, p_userdata, p_path, patch_data, p_file, p_total, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, delta); } -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, uint64_t p_seed) { +Error EditorExportPlatform::_save_zip_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta) { ERR_FAIL_COND_V_MSG(p_total < 1, ERR_PARAMETER_RANGE_ERROR, "Must select at least one file to export."); const String path = simplify_path(p_path).replace_first("res://", ""); @@ -403,12 +437,18 @@ 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, uint64_t p_seed) { - if (_check_hash(PackedData::get_singleton()->get_file_hash(p_path), p_data)) { - return OK; +Error EditorExportPlatform::_save_zip_patch_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta) { + Ref old_file = PackedData::get_singleton()->try_open_path(p_path); + if (old_file.is_valid()) { + Vector old_data = old_file->get_buffer(old_file->get_length()); + + // We can't rely on the MD5 as stored in the PCKs, since delta patches could have made it stale. + if (p_data == old_data) { + return OK; // Do nothing if the file hasn't changed. + } } - return _save_zip_file(p_userdata, p_path, p_data, p_file, p_total, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed); + return _save_zip_file(p_preset, p_userdata, p_path, p_data, p_file, p_total, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, p_delta); } Ref EditorExportPlatform::get_option_icon(int p_index) const { @@ -1036,7 +1076,7 @@ Vector EditorExportPlatform::get_forced_export_files(const Ref &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed) { +Error EditorExportPlatform::_script_save_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta) { Callable cb = ((ScriptCallbackData *)p_userdata)->file_cb; ERR_FAIL_COND_V(!cb.is_valid(), FAILED); @@ -1060,7 +1100,7 @@ Error EditorExportPlatform::_script_save_file(void *p_userdata, const String &p_ return (Error)ret.operator int(); } -Error EditorExportPlatform::_script_add_shared_object(void *p_userdata, const SharedObject &p_so) { +Error EditorExportPlatform::_script_add_shared_object(const Ref &p_preset, void *p_userdata, const SharedObject &p_so) { Callable cb = ((ScriptCallbackData *)p_userdata)->so_cb; if (!cb.is_valid()) { return OK; // Optional. @@ -1216,14 +1256,14 @@ Error EditorExportPlatform::export_project_files(const Ref & for (int i = 0; i < export_plugins.size(); i++) { if (p_so_func) { for (int j = 0; j < export_plugins[i]->shared_objects.size(); j++) { - err = p_so_func(p_udata, export_plugins[i]->shared_objects[j]); + err = p_so_func(p_preset, p_udata, export_plugins[i]->shared_objects[j]); if (err != OK) { return err; } } } for (int j = 0; j < export_plugins[i]->extra_files.size(); j++) { - err = save_proxy.save_file(p_udata, export_plugins[i]->extra_files[j].path, export_plugins[i]->extra_files[j].data, 0, paths.size(), enc_in_filters, enc_ex_filters, key, seed); + err = save_proxy.save_file(p_preset, p_udata, export_plugins[i]->extra_files[j].path, export_plugins[i]->extra_files[j].data, 0, paths.size(), enc_in_filters, enc_ex_filters, key, seed, false); if (err != OK) { return err; } @@ -1315,9 +1355,8 @@ Error EditorExportPlatform::export_project_files(const Ref & // for continue statements without accidentally skipping an increment. int idx = total > 0 ? -1 : 0; - for (const String &E : paths) { + for (const String &path : paths) { idx++; - String path = E; String type = ResourceLoader::get_resource_type(path); bool has_import_file = FileAccess::exists(path + ".import"); @@ -1347,7 +1386,7 @@ Error EditorExportPlatform::export_project_files(const Ref & } if (p_so_func) { for (int j = 0; j < export_plugins[i]->shared_objects.size(); j++) { - err = p_so_func(p_udata, export_plugins[i]->shared_objects[j]); + err = p_so_func(p_preset, p_udata, export_plugins[i]->shared_objects[j]); if (err != OK) { return err; } @@ -1355,7 +1394,7 @@ Error EditorExportPlatform::export_project_files(const Ref & } for (int j = 0; j < export_plugins[i]->extra_files.size(); j++) { - err = save_proxy.save_file(p_udata, export_plugins[i]->extra_files[j].path, export_plugins[i]->extra_files[j].data, idx, total, enc_in_filters, enc_ex_filters, key, seed); + err = save_proxy.save_file(p_preset, p_udata, export_plugins[i]->extra_files[j].path, export_plugins[i]->extra_files[j].data, idx, total, enc_in_filters, enc_ex_filters, key, seed, false); if (err != OK) { return err; } @@ -1385,7 +1424,7 @@ Error EditorExportPlatform::export_project_files(const Ref & if (importer_type == "keep") { // Just keep file as-is. Vector array = FileAccess::get_file_as_bytes(path); - err = save_proxy.save_file(p_udata, path, array, idx, total, enc_in_filters, enc_ex_filters, key, seed); + err = save_proxy.save_file(p_preset, p_udata, path, array, idx, total, enc_in_filters, enc_ex_filters, key, seed, false); if (err != OK) { return err; @@ -1427,13 +1466,13 @@ Error EditorExportPlatform::export_project_files(const Ref & sarr.resize(cs.size()); memcpy(sarr.ptrw(), cs.ptr(), sarr.size()); - err = save_proxy.save_file(p_udata, path + ".import", sarr, idx, total, enc_in_filters, enc_ex_filters, key, seed); + err = save_proxy.save_file(p_preset, p_udata, path + ".import", sarr, idx, total, enc_in_filters, enc_ex_filters, key, seed, false); if (err != OK) { return err; } // Now actual remapped file: sarr = FileAccess::get_file_as_bytes(export_path); - err = save_proxy.save_file(p_udata, export_path, sarr, idx, total, enc_in_filters, enc_ex_filters, key, seed); + err = save_proxy.save_file(p_preset, p_udata, export_path, sarr, idx, total, enc_in_filters, enc_ex_filters, key, seed, false); if (err != OK) { return err; } @@ -1461,14 +1500,14 @@ Error EditorExportPlatform::export_project_files(const Ref & if (remap == "path") { String remapped_path = config->get_value("remap", remap); Vector array = FileAccess::get_file_as_bytes(remapped_path); - err = save_proxy.save_file(p_udata, remapped_path, array, idx, total, enc_in_filters, enc_ex_filters, key, seed); + err = save_proxy.save_file(p_preset, p_udata, remapped_path, array, idx, total, enc_in_filters, enc_ex_filters, key, seed, false); } else if (remap.begins_with("path.")) { String feature = remap.get_slicec('.', 1); if (remap_features.has(feature)) { String remapped_path = config->get_value("remap", remap); Vector array = FileAccess::get_file_as_bytes(remapped_path); - err = save_proxy.save_file(p_udata, remapped_path, array, idx, total, enc_in_filters, enc_ex_filters, key, seed); + err = save_proxy.save_file(p_preset, p_udata, remapped_path, array, idx, total, enc_in_filters, enc_ex_filters, key, seed, false); } else { // Remove paths if feature not enabled. config->erase_section_key("remap", remap); @@ -1494,7 +1533,7 @@ Error EditorExportPlatform::export_project_files(const Ref & sarr.resize(cs.size()); memcpy(sarr.ptrw(), cs.ptr(), sarr.size()); - err = save_proxy.save_file(p_udata, path + ".import", sarr, idx, total, enc_in_filters, enc_ex_filters, key, seed); + err = save_proxy.save_file(p_preset, p_udata, path + ".import", sarr, idx, total, enc_in_filters, enc_ex_filters, key, seed, false); if (err != OK) { return err; @@ -1515,7 +1554,7 @@ Error EditorExportPlatform::export_project_files(const Ref & } Vector array = FileAccess::get_file_as_bytes(export_path); - err = save_proxy.save_file(p_udata, export_path, array, idx, total, enc_in_filters, enc_ex_filters, key, seed); + err = save_proxy.save_file(p_preset, p_udata, export_path, array, idx, total, enc_in_filters, enc_ex_filters, key, seed, false); if (err != OK) { return err; } @@ -1584,7 +1623,7 @@ Error EditorExportPlatform::export_project_files(const Ref & new_file.write[j] = utf8[j]; } - err = save_proxy.save_file(p_udata, from + ".remap", new_file, idx, total, enc_in_filters, enc_ex_filters, key, seed); + err = save_proxy.save_file(p_preset, p_udata, from + ".remap", new_file, idx, total, enc_in_filters, enc_ex_filters, key, seed, false); if (err != OK) { return err; } @@ -1602,7 +1641,7 @@ Error EditorExportPlatform::export_project_files(const Ref & } else { array = FileAccess::get_file_as_bytes(forced_export[i]); } - err = save_proxy.save_file(p_udata, forced_export[i], array, idx, total, enc_in_filters, enc_ex_filters, key, seed); + err = save_proxy.save_file(p_preset, p_udata, forced_export[i], array, idx, total, enc_in_filters, enc_ex_filters, key, seed, false); if (err != OK) { return err; } @@ -1611,7 +1650,7 @@ Error EditorExportPlatform::export_project_files(const Ref & Dictionary int_export = get_internal_export_files(p_preset, p_debug); for (const KeyValue &int_export_kv : int_export) { const PackedByteArray &array = int_export_kv.value; - err = save_proxy.save_file(p_udata, int_export_kv.key, array, idx, total, enc_in_filters, enc_ex_filters, key, seed); + err = save_proxy.save_file(p_preset, p_udata, int_export_kv.key, array, idx, total, enc_in_filters, enc_ex_filters, key, seed, false); if (err != OK) { return err; } @@ -1624,7 +1663,7 @@ Error EditorExportPlatform::export_project_files(const Ref & Vector data = FileAccess::get_file_as_bytes(engine_cfb); DirAccess::remove_file_or_error(engine_cfb); - err = save_proxy.save_file(p_udata, "res://" + config_file, data, idx, total, enc_in_filters, enc_ex_filters, key, seed); + err = save_proxy.save_file(p_preset, p_udata, "res://" + config_file, data, idx, total, enc_in_filters, enc_ex_filters, key, seed, false); if (err != OK) { return err; } @@ -1633,7 +1672,7 @@ Error EditorExportPlatform::export_project_files(const Ref & HashSet currently_loaded_paths = PackedData::get_singleton()->get_file_paths(); for (const String &path : currently_loaded_paths) { if (!save_proxy.has_saved(path)) { - err = p_remove_func(p_udata, path); + err = p_remove_func(p_preset, p_udata, path); if (err != OK) { return err; } @@ -1660,7 +1699,7 @@ Vector EditorExportPlatform::_filter_extension_list_config_file(const S return data; } -Error EditorExportPlatform::_pack_add_shared_object(void *p_userdata, const SharedObject &p_so) { +Error EditorExportPlatform::_pack_add_shared_object(const Ref &p_preset, void *p_userdata, const SharedObject &p_so) { PackData *pack_data = (PackData *)p_userdata; if (pack_data->so_files) { pack_data->so_files->push_back(p_so); @@ -1669,7 +1708,7 @@ Error EditorExportPlatform::_pack_add_shared_object(void *p_userdata, const Shar return OK; } -Error EditorExportPlatform::_remove_pack_file(void *p_userdata, const String &p_path) { +Error EditorExportPlatform::_remove_pack_file(const Ref &p_preset, void *p_userdata, const String &p_path) { PackData *pd = (PackData *)p_userdata; SavedData sd; @@ -1691,7 +1730,7 @@ Error EditorExportPlatform::_remove_pack_file(void *p_userdata, const String &p_ return OK; } -Error EditorExportPlatform::_zip_add_shared_object(void *p_userdata, const SharedObject &p_so) { +Error EditorExportPlatform::_zip_add_shared_object(const Ref &p_preset, void *p_userdata, const SharedObject &p_so) { ZipData *zip_data = (ZipData *)p_userdata; if (zip_data->so_files) { zip_data->so_files->push_back(p_so); @@ -1996,6 +2035,9 @@ bool EditorExportPlatform::_encrypt_and_store_directory(Ref p_fd, Pa if (p_pack_data.file_ofs[i].removal) { flags |= PACK_FILE_REMOVAL; } + if (p_pack_data.file_ofs[i].delta) { + flags |= PACK_FILE_DELTA; + } fhead->store_32(flags); } diff --git a/editor/export/editor_export_platform.h b/editor/export/editor_export_platform.h index 650dbdb7f18..3c4f3c1cc90 100644 --- a/editor/export/editor_export_platform.h +++ b/editor/export/editor_export_platform.h @@ -53,9 +53,9 @@ protected: static void _bind_methods(); public: - typedef Error (*EditorExportSaveFunction)(void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed); - typedef Error (*EditorExportRemoveFunction)(void *p_userdata, const String &p_path); - typedef Error (*EditorExportSaveSharedObject)(void *p_userdata, const SharedObject &p_so); + typedef Error (*EditorExportSaveFunction)(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); + typedef Error (*EditorExportRemoveFunction)(const Ref &p_preset, void *p_userdata, const String &p_path); + typedef Error (*EditorExportSaveSharedObject)(const Ref &p_preset, void *p_userdata, const SharedObject &p_so); enum DebugFlags { DEBUG_FLAG_DUMB_CLIENT = 1, @@ -83,6 +83,7 @@ public: uint64_t size = 0; bool encrypted = false; bool removal = false; + bool delta = false; Vector md5; CharString path_utf8; @@ -119,25 +120,23 @@ 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(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); + static Error _save_pack_patch_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); + static Error _pack_add_shared_object(const Ref &p_preset, void *p_userdata, const SharedObject &p_so); - static 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, uint64_t p_seed); - 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, uint64_t p_seed); - static Error _pack_add_shared_object(void *p_userdata, const SharedObject &p_so); + static Error _remove_pack_file(const Ref &p_preset, void *p_userdata, const String &p_path); - static Error _remove_pack_file(void *p_userdata, const String &p_path); - - 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, uint64_t p_seed); - 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, uint64_t p_seed); - static Error _zip_add_shared_object(void *p_userdata, const SharedObject &p_so); + static Error _save_zip_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); + static Error _save_zip_patch_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); + static Error _zip_add_shared_object(const Ref &p_preset, void *p_userdata, const SharedObject &p_so); struct ScriptCallbackData { Callable file_cb; Callable so_cb; }; - static Error _script_save_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, uint64_t p_seed); - static Error _script_add_shared_object(void *p_userdata, const SharedObject &p_so); + static Error _script_save_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); + static Error _script_add_shared_object(const Ref &p_preset, void *p_userdata, const SharedObject &p_so); void _edit_files_with_filter(Ref &da, const Vector &p_filters, HashSet &r_list, bool exclude); void _edit_filter_list(HashSet &r_list, const String &p_filter, bool exclude); diff --git a/editor/export/editor_export_preset.cpp b/editor/export/editor_export_preset.cpp index 72f33f9fe0f..3c4f8abbac0 100644 --- a/editor/export/editor_export_preset.cpp +++ b/editor/export/editor_export_preset.cpp @@ -28,10 +28,11 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#include "editor_export.h" +#include "editor_export_preset.h" #include "core/config/project_settings.h" #include "core/io/dir_access.h" +#include "editor/export/editor_export.h" #include "editor/settings/editor_settings.h" bool EditorExportPreset::_set(const StringName &p_name, const Variant &p_value) { @@ -440,6 +441,51 @@ Vector EditorExportPreset::get_patches() const { return patches; } +void EditorExportPreset::set_patch_delta_encoding_enabled(bool p_enable) { + patch_delta_encoding_enabled = p_enable; + EditorExport::singleton->save_presets(); +} + +bool EditorExportPreset::is_patch_delta_encoding_enabled() const { + return patch_delta_encoding_enabled; +} + +void EditorExportPreset::set_patch_delta_zstd_level(int p_level) { + patch_delta_zstd_level = p_level; + EditorExport::singleton->save_presets(); +} + +int EditorExportPreset::get_patch_delta_zstd_level() const { + return patch_delta_zstd_level; +} + +void EditorExportPreset::set_patch_delta_min_reduction(double p_ratio) { + patch_delta_min_reduction = p_ratio; + EditorExport::singleton->save_presets(); +} + +double EditorExportPreset::get_patch_delta_min_reduction() const { + return patch_delta_min_reduction; +} + +void EditorExportPreset::set_patch_delta_include_filter(const String &p_filter) { + patch_delta_include_filter = p_filter; + EditorExport::singleton->save_presets(); +} + +String EditorExportPreset::get_patch_delta_include_filter() const { + return patch_delta_include_filter; +} + +void EditorExportPreset::set_patch_delta_exclude_filter(const String &p_filter) { + patch_delta_exclude_filter = p_filter; + EditorExport::singleton->save_presets(); +} + +String EditorExportPreset::get_patch_delta_exclude_filter() const { + return patch_delta_exclude_filter; +} + 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 59ab0465b0f..d79db335178 100644 --- a/editor/export/editor_export_preset.h +++ b/editor/export/editor_export_preset.h @@ -73,6 +73,11 @@ private: bool dedicated_server = false; Vector patches; + bool patch_delta_encoding_enabled = false; + int patch_delta_zstd_level = 19; + double patch_delta_min_reduction = 0.1; + String patch_delta_include_filter = "*"; + String patch_delta_exclude_filter; friend class EditorExport; friend class EditorExportPlatform; @@ -148,11 +153,28 @@ public: 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_patch_delta_encoding_enabled(bool p_enable); + bool is_patch_delta_encoding_enabled() const; + + void set_patch_delta_zstd_level(int p_level); + int get_patch_delta_zstd_level() const; + + void set_patch_delta_min_reduction(double p_ratio); + double get_patch_delta_min_reduction() const; + + void set_patch_delta_include_filter(const String &p_filter); + String get_patch_delta_include_filter() const; + + void set_patch_delta_exclude_filter(const String &p_filter); + String get_patch_delta_exclude_filter() 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 938d0c0ae32..abf7c3ba003 100644 --- a/editor/export/project_export.cpp +++ b/editor/export/project_export.cpp @@ -51,11 +51,14 @@ #include "scene/gui/option_button.h" #include "scene/gui/popup_menu.h" #include "scene/gui/rich_text_label.h" +#include "scene/gui/spin_box.h" #include "scene/gui/split_container.h" #include "scene/gui/tab_container.h" #include "scene/gui/texture_rect.h" #include "scene/gui/tree.h" +#include + void ProjectExportTextureFormatError::_on_fix_texture_format_pressed() { export_dialog->hide(); ProjectSettingsEditor *project_settings = EditorNode::get_singleton()->get_project_settings(); @@ -301,6 +304,19 @@ 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); + bool patch_delta_encoding_enabled = current->is_patch_delta_encoding_enabled(); + patch_delta_encoding->set_pressed(patch_delta_encoding_enabled); + patch_delta_zstd_level->set_editable(patch_delta_encoding_enabled); + patch_delta_zstd_level->set_value(current->get_patch_delta_zstd_level()); + patch_delta_min_reduction->set_editable(patch_delta_encoding_enabled); + patch_delta_min_reduction->set_value(current->get_patch_delta_min_reduction() * 100); + patch_delta_include_filter->set_editable(patch_delta_encoding_enabled); + patch_delta_exclude_filter->set_editable(patch_delta_encoding_enabled); + if (!updating_patch_delta_filters) { + patch_delta_include_filter->set_text(current->get_patch_delta_include_filter()); + patch_delta_exclude_filter->set_text(current->get_patch_delta_exclude_filter()); + } + patches->clear(); TreeItem *patch_root = patches->create_item(); Vector patch_list = current->get_patches(); @@ -745,6 +761,11 @@ void ProjectExportDialog::_duplicate_preset() { preset->set_include_filter(current->get_include_filter()); preset->set_exclude_filter(current->get_exclude_filter()); preset->set_patches(current->get_patches()); + preset->set_patch_delta_encoding_enabled(current->is_patch_delta_encoding_enabled()); + preset->set_patch_delta_zstd_level(current->get_patch_delta_zstd_level()); + preset->set_patch_delta_min_reduction(current->get_patch_delta_min_reduction()); + preset->set_patch_delta_include_filter(current->get_patch_delta_include_filter()); + preset->set_patch_delta_exclude_filter(current->get_patch_delta_exclude_filter()); preset->set_custom_features(current->get_custom_features()); preset->set_enc_in_filter(current->get_enc_in_filter()); preset->set_enc_ex_filter(current->get_enc_ex_filter()); @@ -1201,6 +1222,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_delta_encoding_changed(bool p_pressed) { + if (updating) { + return; + } + + Ref current = get_current_preset(); + ERR_FAIL_COND(current.is_null()); + + current->set_patch_delta_encoding_enabled(p_pressed); + + _update_current_preset(); +} + +void ProjectExportDialog::_patch_delta_include_filter_changed(const String &p_filter) { + if (updating) { + return; + } + + Ref current = get_current_preset(); + ERR_FAIL_COND(current.is_null()); + + current->set_patch_delta_include_filter(patch_delta_include_filter->get_text()); + + updating_patch_delta_filters = true; + _update_current_preset(); + updating_patch_delta_filters = false; +} + +void ProjectExportDialog::_patch_delta_exclude_filter_changed(const String &p_filter) { + if (updating) { + return; + } + + Ref current = get_current_preset(); + ERR_FAIL_COND(current.is_null()); + + current->set_patch_delta_exclude_filter(patch_delta_exclude_filter->get_text()); + + updating_patch_delta_filters = true; + _update_current_preset(); + updating_patch_delta_filters = false; +} + +void ProjectExportDialog::_patch_delta_zstd_level_changed(double p_value) { + if (updating) { + return; + } + + Ref current = get_current_preset(); + ERR_FAIL_COND(current.is_null()); + + current->set_patch_delta_zstd_level((int)p_value); + + _update_current_preset(); +} + +void ProjectExportDialog::_patch_delta_min_reduction_changed(double p_value) { + if (updating) { + return; + } + + Ref current = get_current_preset(); + ERR_FAIL_COND(current.is_null()); + + current->set_patch_delta_min_reduction(p_value / 100.0); + + _update_current_preset(); +} + 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); @@ -1657,11 +1747,52 @@ ProjectExportDialog::ProjectExportDialog() { exclude_filters); exclude_filters->connect(SceneStringName(text_changed), callable_mp(this, &ProjectExportDialog::_filter_changed)); - // Patch packages. + // Patching. VBoxContainer *patch_vb = memnew(VBoxContainer); sections->add_child(patch_vb); - patch_vb->set_name(TTR("Patches")); + patch_vb->set_name(TTRC("Patching")); + + patch_delta_encoding = memnew(CheckButton); + patch_delta_encoding->connect(SceneStringName(toggled), callable_mp(this, &ProjectExportDialog::_patch_delta_encoding_changed)); + patch_delta_encoding->set_text(TTRC("Enable Delta Encoding")); + patch_delta_encoding->set_tooltip_text(TTRC("If checked, any change to a file already present in the base packs will be exported as the difference between the old file and the new file.\n" + "Enabling this comes at the cost of longer export times as well as longer load times for patched resources.")); + patch_vb->add_child(patch_delta_encoding); + + patch_delta_zstd_level = memnew(SpinBox); + patch_delta_zstd_level->set_min(ZSTD_minCLevel()); + patch_delta_zstd_level->set_max(ZSTD_maxCLevel()); + patch_delta_zstd_level->set_step(1); + patch_delta_zstd_level->set_tooltip_text( + vformat(TTR("The Zstandard compression level to use when generating delta-encoded patches.\n" + "Higher positive levels will reduce patch sizes, at the cost of longer export time, but do not affect the time it takes to apply patches.\n" + "Negative levels will reduce the time it takes to apply patches, at the cost of worse compression.\n" + "Levels above 19 require more memory both during export and when applying patches, usually for very little benefit.\n" + "Level 0 will cause Zstandard to use its default compression level, which is currently level %d."), + ZSTD_CLEVEL_DEFAULT)); + patch_delta_zstd_level->connect(SceneStringName(value_changed), callable_mp(this, &ProjectExportDialog::_patch_delta_zstd_level_changed)); + patch_vb->add_margin_child(TTRC("Delta Encoding Compression Level"), patch_delta_zstd_level); + + patch_delta_min_reduction = memnew(SpinBox); + patch_delta_min_reduction->set_min(0.0); + patch_delta_min_reduction->set_max(100.0); + patch_delta_min_reduction->set_step(1.0); + patch_delta_min_reduction->set_suffix("%"); + patch_delta_min_reduction->set_tooltip_text(TTRC("How much smaller, when compared to the new file, a delta-encoded patch needs to be for it to be exported.\n" + "If the patch is not at least this much smaller, the new file will be exported as-is.")); + patch_delta_min_reduction->connect(SceneStringName(value_changed), callable_mp(this, &ProjectExportDialog::_patch_delta_min_reduction_changed)); + patch_vb->add_margin_child(TTRC("Delta Encoding Minimum Size Reduction"), patch_delta_min_reduction); + + patch_delta_include_filter = memnew(LineEdit); + patch_delta_include_filter->set_accessibility_name(TTRC("Delta Encoding Include Filters")); + patch_delta_include_filter->connect(SceneStringName(text_changed), callable_mp(this, &ProjectExportDialog::_patch_delta_include_filter_changed)); + patch_vb->add_margin_child(TTRC("Filters to include files/folders from being delta-encoded\n(comma-separated, e.g: *.gdc, scripts/*)"), patch_delta_include_filter); + + patch_delta_exclude_filter = memnew(LineEdit); + patch_delta_exclude_filter->set_accessibility_name(TTRC("Delta Encoding Exclude Filters")); + patch_delta_exclude_filter->connect(SceneStringName(text_changed), callable_mp(this, &ProjectExportDialog::_patch_delta_exclude_filter_changed)); + patch_vb->add_margin_child(TTRC("Filters to exclude files/folders from being delta-encoded\n(comma-separated, e.g: *.ctex, textures/*)"), patch_delta_exclude_filter); patches = memnew(Tree); patches->set_v_size_flags(Control::SIZE_EXPAND_FILL); diff --git a/editor/export/project_export.h b/editor/export/project_export.h index 922634e815e..586635ce747 100644 --- a/editor/export/project_export.h +++ b/editor/export/project_export.h @@ -46,6 +46,7 @@ class OptionButton; class PopupMenu; class ProjectExportDialog; class RichTextLabel; +class SpinBox; class TabContainer; class Tree; class TreeItem; @@ -107,6 +108,11 @@ class ProjectExportDialog : public ConfirmationDialog { RBSet feature_set; + CheckButton *patch_delta_encoding = nullptr; + SpinBox *patch_delta_zstd_level = nullptr; + SpinBox *patch_delta_min_reduction = nullptr; + LineEdit *patch_delta_include_filter = nullptr; + LineEdit *patch_delta_exclude_filter = nullptr; Tree *patches = nullptr; int patch_index = -1; EditorFileDialog *patch_dialog = nullptr; @@ -158,6 +164,12 @@ class ProjectExportDialog : public ConfirmationDialog { void _tree_popup_edited(bool p_arrow_clicked); void _set_file_export_mode(int p_id); + bool updating_patch_delta_filters = false; + void _patch_delta_encoding_changed(bool p_pressed); + void _patch_delta_include_filter_changed(const String &p_filter); + void _patch_delta_exclude_filter_changed(const String &p_filter); + void _patch_delta_zstd_level_changed(double p_value); + void _patch_delta_min_reduction_changed(double p_value); 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); diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 805e82c7cfa..e7444d904ac 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -767,7 +767,7 @@ Error EditorExportPlatformAndroid::store_in_apk(APKExportData *ed, const String return OK; } -Error EditorExportPlatformAndroid::save_apk_so(void *p_userdata, const SharedObject &p_so) { +Error EditorExportPlatformAndroid::save_apk_so(const Ref &p_preset, void *p_userdata, const SharedObject &p_so) { if (!p_so.path.get_file().begins_with("lib")) { String err = "Android .so file names must start with \"lib\", but got: " + p_so.path; ERR_PRINT(err); @@ -801,14 +801,14 @@ Error EditorExportPlatformAndroid::save_apk_so(void *p_userdata, const SharedObj return OK; } -Error EditorExportPlatformAndroid::save_apk_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, uint64_t p_seed) { +Error EditorExportPlatformAndroid::save_apk_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta) { APKExportData *ed = static_cast(p_userdata); const String simplified_path = simplify_path(p_path); Vector enc_data; EditorExportPlatform::SavedData sd; - Error err = _store_temp_file(simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, enc_data, sd); + Error err = _store_temp_file(simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, p_delta, enc_data, sd); if (err != OK) { return err; } @@ -822,11 +822,11 @@ Error EditorExportPlatformAndroid::save_apk_file(void *p_userdata, const String return OK; } -Error EditorExportPlatformAndroid::ignore_apk_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, uint64_t p_seed) { +Error EditorExportPlatformAndroid::ignore_apk_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta) { return OK; } -Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const SharedObject &p_so) { +Error EditorExportPlatformAndroid::copy_gradle_so(const Ref &p_preset, void *p_userdata, const SharedObject &p_so) { ERR_FAIL_COND_V_MSG(!p_so.path.get_file().begins_with("lib"), FAILED, "Android .so file names must start with \"lib\", but got: " + p_so.path); Vector abis = get_abis(); diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h index 541bc417bdf..8a95613d012 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -151,13 +151,13 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { static Error store_in_apk(APKExportData *ed, const String &p_path, const Vector &p_data, int compression_method = Z_DEFLATED); - static Error save_apk_so(void *p_userdata, const SharedObject &p_so); + static Error save_apk_so(const Ref &p_preset, void *p_userdata, const SharedObject &p_so); - static Error save_apk_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, uint64_t p_seed); + static Error save_apk_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); - static Error ignore_apk_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, uint64_t p_seed); + static Error ignore_apk_file(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); - static Error copy_gradle_so(void *p_userdata, const SharedObject &p_so); + static Error copy_gradle_so(const Ref &p_preset, void *p_userdata, const SharedObject &p_so); bool _has_read_write_storage_permission(const Vector &p_permissions); diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index 289321f72cc..420c36e3959 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -170,14 +170,14 @@ Error store_string_at_path(const String &p_path, const String &p_data) { // It is used by the export_project_files method to save all the asset files into the gradle project. // It's functionality mirrors that of the method save_apk_file. // This method will be called ONLY when gradle build is enabled. -Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed) { +Error rename_and_store_file_in_gradle_project(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta) { CustomExportData *export_data = static_cast(p_userdata); const String simplified_path = EditorExportPlatform::simplify_path(p_path); Vector enc_data; EditorExportPlatform::SavedData sd; - Error err = _store_temp_file(simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, enc_data, sd); + Error err = _store_temp_file(simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, p_delta, enc_data, sd); if (err != OK) { return err; } @@ -398,7 +398,7 @@ String _get_application_tag(const Ref &p_export_platform, return manifest_application_text; } -Error _store_temp_file(const String &p_simplified_path, const Vector &p_data, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, Vector &r_enc_data, EditorExportPlatform::SavedData &r_sd) { +Error _store_temp_file(const String &p_simplified_path, const Vector &p_data, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta, Vector &r_enc_data, EditorExportPlatform::SavedData &r_sd) { Error err = OK; Ref ftmp = FileAccess::create_temp(FileAccess::WRITE_READ, "export", "tmp", false, &err); if (err != OK) { @@ -407,6 +407,7 @@ Error _store_temp_file(const String &p_simplified_path, const Vector &p r_sd.path_utf8 = p_simplified_path.trim_prefix("res://").utf8(); r_sd.ofs = 0; r_sd.size = p_data.size(); + r_sd.delta = p_delta; err = EditorExportPlatform::_encrypt_and_store_data(ftmp, p_simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, r_sd.encrypted); if (err != OK) { return err; diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index 96fddd223ba..c7216887927 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -85,7 +85,7 @@ int _get_app_category_value(int category_index); String _get_app_category_label(int category_index); -Error _store_temp_file(const String &p_simplified_path, const Vector &p_data, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, Vector &r_enc_data, EditorExportPlatform::SavedData &r_sd); +Error _store_temp_file(const String &p_simplified_path, const Vector &p_data, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta, Vector &r_enc_data, EditorExportPlatform::SavedData &r_sd); // Utility method used to create a directory. Error create_directory(const String &p_dir); @@ -103,7 +103,7 @@ Error store_string_at_path(const String &p_path, const String &p_data); // It is used by the export_project_files method to save all the asset files into the gradle project. // It's functionality mirrors that of the method save_apk_file. // This method will be called ONLY when gradle build is enabled. -Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed); +Error rename_and_store_file_in_gradle_project(const Ref &p_preset, void *p_userdata, const String &p_path, const Vector &p_data, int p_file, int p_total, const Vector &p_enc_in_filters, const Vector &p_enc_ex_filters, const Vector &p_key, uint64_t p_seed, bool p_delta); // Creates strings.xml files inside the gradle project for different locales. Error _create_project_name_strings_files(const Ref &p_preset, const String &p_project_name, const String &p_gradle_build_dir, const Dictionary &p_appnames);