mirror of
https://github.com/godotengine/godot.git
synced 2026-04-18 09:51:15 +00:00
1788 lines
64 KiB
C++
1788 lines
64 KiB
C++
/**************************************************************************/
|
|
/* export_template_manager.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 "export_template_manager.h"
|
|
|
|
#include "core/config/engine.h"
|
|
#include "core/error/error_list.h"
|
|
#include "core/io/dir_access.h"
|
|
#include "core/io/json.h"
|
|
#include "core/io/marshalls.h"
|
|
#include "core/io/zip_io.h"
|
|
#include "core/object/callable_mp.h"
|
|
#include "core/os/os.h"
|
|
#include "core/version.h"
|
|
#include "editor/editor_node.h"
|
|
#include "editor/editor_string_names.h"
|
|
#include "editor/export/editor_export.h"
|
|
#include "editor/file_system/editor_file_system.h"
|
|
#include "editor/file_system/editor_paths.h"
|
|
#include "editor/gui/editor_bottom_panel.h"
|
|
#include "editor/gui/progress_dialog.h"
|
|
#include "editor/settings/editor_settings.h"
|
|
#include "editor/themes/editor_scale.h"
|
|
#include "scene/gui/box_container.h"
|
|
#include "scene/gui/button.h"
|
|
#include "scene/gui/item_list.h"
|
|
#include "scene/gui/label.h"
|
|
#include "scene/gui/link_button.h"
|
|
#include "scene/gui/option_button.h"
|
|
#include "scene/gui/split_container.h"
|
|
#include "scene/gui/tree.h"
|
|
#include "scene/resources/style_box.h"
|
|
#include "scene/resources/texture.h"
|
|
#include "servers/display/display_server.h"
|
|
|
|
void ExportTemplateManager::_request_mirrors() {
|
|
mirrors_list->clear();
|
|
mirrors_empty = true;
|
|
_update_install_button();
|
|
|
|
// Downloadable export templates are only available for stable and official alpha/beta/RC builds
|
|
// (which always have a number following their status, e.g. "alpha1").
|
|
// Therefore, don't display download-related features when using a development version
|
|
// (whose builds aren't numbered).
|
|
if (!strcmp(GODOT_VERSION_STATUS, "dev") || !strcmp(GODOT_VERSION_STATUS, "beta") || !strcmp(GODOT_VERSION_STATUS, "rc")) {
|
|
_set_empty_mirror_list();
|
|
mirrors_list->set_tooltip_text(TTRC("Official export templates aren't available for development builds."));
|
|
#ifdef REAL_T_IS_DOUBLE
|
|
} else if (true) {
|
|
_set_empty_mirror_list();
|
|
mirrors_list->set_tooltip_text(TTRC("Official export templates aren't available for double-precision builds."));
|
|
#endif
|
|
} else if (!_is_online()) {
|
|
mirrors_list->set_tooltip_text(TTRC("Template downloading is disabled in offline mode."));
|
|
} else {
|
|
mirrors_list->set_tooltip_text(String());
|
|
}
|
|
|
|
if (mirrors_list->get_tooltip_text().is_empty()) {
|
|
const String mirrors_metadata_url = vformat("https://godotengine.org/mirrorlist/%s.json", GODOT_VERSION_FULL_CONFIG);
|
|
mirrors_requester->request(mirrors_metadata_url);
|
|
}
|
|
}
|
|
|
|
void ExportTemplateManager::_mirrors_request_completed(int p_result, int p_response_code, const PackedStringArray &p_headers, const PackedByteArray &p_body) {
|
|
mirrors_list->clear();
|
|
|
|
if (p_result != HTTPRequest::RESULT_SUCCESS || p_response_code != HTTPClient::RESPONSE_OK) {
|
|
String error = TTR("Error getting the list of mirrors.") + "\n";
|
|
if (p_result == HTTPRequest::RESULT_SUCCESS && p_response_code == HTTPClient::RESPONSE_NOT_FOUND) {
|
|
// Response successful, but wrong address.
|
|
error += TTR("No mirrors found for this version. Template download is only available for official releases.");
|
|
} else {
|
|
error += vformat(TTR("Result: %d\nResponse code: %d"), p_result, p_response_code);
|
|
}
|
|
EditorNode::get_singleton()->show_warning(error);
|
|
_set_empty_mirror_list();
|
|
return;
|
|
}
|
|
|
|
String response_json = String::utf8((const char *)p_body.ptr(), p_body.size());
|
|
|
|
JSON json;
|
|
Error err = json.parse(response_json);
|
|
if (err != OK) {
|
|
EditorNode::get_singleton()->show_warning(TTR("Error parsing JSON with the list of mirrors. Please report this issue!"));
|
|
_set_empty_mirror_list();
|
|
return;
|
|
}
|
|
|
|
bool mirrors_available = false;
|
|
|
|
Dictionary mirror_data = json.get_data();
|
|
if (mirror_data.has("mirrors")) {
|
|
Array mirrors = mirror_data["mirrors"];
|
|
for (const Variant &mirror : mirrors) {
|
|
Dictionary m = mirror;
|
|
ERR_CONTINUE(!m.has("url") || !m.has("name"));
|
|
|
|
mirrors_list->add_item(m["name"]);
|
|
mirrors_list->set_item_metadata(-1, m["url"]);
|
|
|
|
mirrors_available = true;
|
|
}
|
|
// Hard-coded for translation. Should match the up-to-date list of mirrors.
|
|
// TTR("Official Releases mirror")
|
|
}
|
|
if (!mirrors_available) {
|
|
_set_empty_mirror_list();
|
|
} else {
|
|
mirrors_list->set_disabled(false);
|
|
open_mirror->set_disabled(false);
|
|
mirrors_empty = false;
|
|
|
|
_update_install_button();
|
|
if (!is_downloading()) {
|
|
// Some tree buttons won't show until mirrors are loaded.
|
|
_update_template_tree();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ExportTemplateManager::_set_empty_mirror_list() {
|
|
mirrors_list->add_item(TTRC("No mirrors"));
|
|
mirrors_list->set_disabled(true);
|
|
open_mirror->set_disabled(true);
|
|
mirrors_empty = true;
|
|
_update_install_button();
|
|
}
|
|
|
|
String ExportTemplateManager::_get_current_mirror_url() const {
|
|
return mirrors_list->get_item_metadata(mirrors_list->get_selected());
|
|
}
|
|
|
|
void ExportTemplateManager::_update_online_mode() {
|
|
offline_container->set_visible((int)EDITOR_GET("network/connection/network_mode") == EditorSettings::NETWORK_OFFLINE);
|
|
|
|
if (_is_online()) {
|
|
_update_install_button();
|
|
} else {
|
|
mirrors_list->clear();
|
|
_set_empty_mirror_list();
|
|
}
|
|
}
|
|
|
|
bool ExportTemplateManager::_is_online() const {
|
|
return !offline_container->is_visible();
|
|
}
|
|
|
|
void ExportTemplateManager::_force_online_mode() {
|
|
EditorSettings::get_singleton()->set_setting("network/connection/network_mode", EditorSettings::NETWORK_ONLINE);
|
|
EditorSettings::get_singleton()->notify_changes();
|
|
EditorSettings::get_singleton()->save();
|
|
|
|
_update_online_mode();
|
|
_request_mirrors();
|
|
}
|
|
|
|
void ExportTemplateManager::_open_mirror() {
|
|
OS::get_singleton()->shell_open(_get_current_mirror_url());
|
|
}
|
|
|
|
void ExportTemplateManager::_delete_confirmed() {
|
|
const String selected_version = version_list->get_item_text(version_list->get_current());
|
|
const String template_directory = _get_template_folder_path(selected_version);
|
|
|
|
if (_item_is_file(item_to_delete)) {
|
|
OS::get_singleton()->move_to_trash(template_directory.path_join(item_to_delete->get_text(0)));
|
|
file_metadata.erase(item_to_delete->get_text(0));
|
|
} else {
|
|
for (TreeItem *child = item_to_delete->get_first_child(); child; child = child->get_next()) {
|
|
if (!_get_file_metadata(child)->is_missing) {
|
|
OS::get_singleton()->move_to_trash(template_directory.path_join(child->get_text(0)));
|
|
}
|
|
file_metadata.erase(child->get_text(0));
|
|
}
|
|
}
|
|
_update_template_tree();
|
|
}
|
|
|
|
void ExportTemplateManager::_initialize_template_data() {
|
|
// Base templates.
|
|
{
|
|
TemplateInfo info;
|
|
info.name = "Windows x86_32";
|
|
info.description = TTRC("32-bit build for Microsoft Windows, including console wrapper.");
|
|
info.file_list = { "windows_debug_x86_32.exe", "windows_debug_x86_32_console.exe", "windows_release_x86_32.exe", "windows_release_x86_32_console.exe" };
|
|
template_data[TemplateID::WINDOWS_X86_32] = info;
|
|
}
|
|
{
|
|
TemplateInfo info;
|
|
info.name = "Windows x86_64";
|
|
info.description = TTRC("64-bit build for Microsoft Windows, including console wrapper.");
|
|
info.file_list = { "windows_debug_x86_64.exe", "windows_debug_x86_64_console.exe", "windows_release_x86_64.exe", "windows_release_x86_64_console.exe" };
|
|
template_data[TemplateID::WINDOWS_X86_64] = info;
|
|
}
|
|
{
|
|
TemplateInfo info;
|
|
info.name = "Windows arm64";
|
|
info.description = TTRC("64-bit build for Microsoft Windows on ARM architecture, including console wrapper.");
|
|
info.file_list = { "windows_debug_arm64.exe", "windows_debug_arm64_console.exe", "windows_release_arm64.exe", "windows_release_arm64_console.exe" };
|
|
template_data[TemplateID::WINDOWS_ARM64] = info;
|
|
}
|
|
|
|
{
|
|
TemplateInfo info;
|
|
info.name = "Linux x86_32";
|
|
info.description = TTRC("32-bit build for Linux systems.");
|
|
info.file_list = { "linux_debug.x86_32", "linux_release.x86_32" };
|
|
template_data[TemplateID::LINUX_X86_32] = info;
|
|
}
|
|
{
|
|
TemplateInfo info;
|
|
info.name = "Linux x86_64";
|
|
info.description = TTRC("64-bit build for Linux systems.");
|
|
info.file_list = { "linux_debug.x86_64", "linux_release.x86_64" };
|
|
template_data[TemplateID::LINUX_X86_64] = info;
|
|
}
|
|
{
|
|
TemplateInfo info;
|
|
info.name = "Linux arm32";
|
|
info.description = TTRC("32-bit build for Linux systems on ARM architecture.");
|
|
info.file_list = { "linux_debug.arm32", "linux_release.arm32" };
|
|
template_data[TemplateID::LINUX_ARM32] = info;
|
|
}
|
|
{
|
|
TemplateInfo info;
|
|
info.name = "Linux arm64";
|
|
info.description = TTRC("64-bit build for Linux systems on ARM architecture.");
|
|
info.file_list = { "linux_debug.arm64", "linux_release.arm64" };
|
|
template_data[TemplateID::LINUX_ARM64] = info;
|
|
}
|
|
|
|
{
|
|
TemplateInfo info;
|
|
info.name = "macOS";
|
|
info.description = TTRC("Universal build for macOS.");
|
|
info.file_list = { "macos.zip" };
|
|
template_data[TemplateID::MACOS] = info;
|
|
}
|
|
|
|
{
|
|
TemplateInfo info;
|
|
info.name = "Web";
|
|
info.description = TTRC("Regular web build with threading support. Threads improve performance, but require \"cross-origin isolated\" website to run.");
|
|
info.file_list = { "web_debug.zip", "web_release.zip" };
|
|
template_data[TemplateID::WEB] = info;
|
|
}
|
|
{
|
|
TemplateInfo info;
|
|
info.name = TTR("Web with Extensions");
|
|
info.description = TTRC("Web build with support for GDExtextensions. Only useful if you use GDExtensions, otherwise it only increases build size.");
|
|
info.file_list = { "web_dlink_debug.zip", "web_dlink_release.zip" };
|
|
template_data[TemplateID::WEB_EXTENSIONS] = info;
|
|
}
|
|
{
|
|
TemplateInfo info;
|
|
info.name = TTR("Web Single-Threaded");
|
|
info.description = TTRC("Web build without threading support.");
|
|
info.file_list = { "web_nothreads_debug.zip", "web_nothreads_release.zip" };
|
|
template_data[TemplateID::WEB_NOTHREADS] = info;
|
|
}
|
|
{
|
|
TemplateInfo info;
|
|
info.name = TTR("Web with Extensions Single-Threaded");
|
|
info.description = TTRC("Web build with GDExtension support and no threading support.");
|
|
info.file_list = { "web_dlink_nothreads_debug.zip", "web_dlink_nothreads_release.zip" };
|
|
template_data[TemplateID::WEB_EXTENSIONS_NOTHREADS] = info;
|
|
}
|
|
|
|
{
|
|
TemplateInfo info;
|
|
info.name = "Android";
|
|
info.description = TTRC("Basic Android APK template.");
|
|
info.file_list = { "android_debug.apk", "android_release.apk" };
|
|
template_data[TemplateID::ANDROID] = info;
|
|
}
|
|
{
|
|
TemplateInfo info;
|
|
info.name = TTR("Android Source");
|
|
info.description = TTRC("Template for Gradle builds for Android.");
|
|
info.file_list = { "android_source.zip" };
|
|
template_data[TemplateID::ANDROID_SOURCE] = info;
|
|
}
|
|
|
|
{
|
|
TemplateInfo info;
|
|
info.name = "iOS";
|
|
info.description = TTRC("Build for Apple's iOS.");
|
|
info.file_list = { "ios.zip" };
|
|
template_data[TemplateID::IOS] = info;
|
|
}
|
|
|
|
{
|
|
TemplateInfo info;
|
|
info.name = TTR("ICU Data");
|
|
info.description = TTRC("Line breaking dictionaries for TextServer, used by certain languages.");
|
|
info.file_list = { "icudt_godot.dat" };
|
|
template_data[TemplateID::ICU_DATA] = info;
|
|
}
|
|
|
|
// Platforms.
|
|
{
|
|
PlatformInfo info;
|
|
info.name = "Windows";
|
|
info.icon = _get_platform_icon("Windows Desktop");
|
|
info.templates = { TemplateID::WINDOWS_X86_32, TemplateID::WINDOWS_X86_64, TemplateID::WINDOWS_ARM64 };
|
|
info.group = TTR("Desktop", "Platform Group");
|
|
platform_map[PlatformID::WINDOWS] = info;
|
|
}
|
|
{
|
|
PlatformInfo info;
|
|
info.name = "Linux";
|
|
info.icon = _get_platform_icon("Linux");
|
|
info.templates = { TemplateID::LINUX_X86_32, TemplateID::LINUX_X86_64, TemplateID::LINUX_ARM32, TemplateID::LINUX_ARM64 };
|
|
info.group = TTR("Desktop", "Platform Group");
|
|
platform_map[PlatformID::LINUX] = info;
|
|
}
|
|
{
|
|
PlatformInfo info;
|
|
info.name = "macOS";
|
|
info.icon = _get_platform_icon("macOS");
|
|
info.templates = { TemplateID::MACOS };
|
|
info.group = TTR("Desktop", "Platform Group");
|
|
platform_map[PlatformID::MACOS] = info;
|
|
}
|
|
{
|
|
PlatformInfo info;
|
|
info.name = "Android";
|
|
info.icon = _get_platform_icon("Android");
|
|
info.templates = { TemplateID::ANDROID, TemplateID::ANDROID_SOURCE };
|
|
info.group = TTR("Mobile", "Platform Group");
|
|
platform_map[PlatformID::ANDROID] = info;
|
|
}
|
|
{
|
|
PlatformInfo info;
|
|
info.name = "iOS";
|
|
info.icon = _get_platform_icon("iOS");
|
|
info.templates = { TemplateID::IOS };
|
|
info.group = TTR("Mobile", "Platform Group");
|
|
platform_map[PlatformID::IOS] = info;
|
|
}
|
|
{
|
|
PlatformInfo info;
|
|
info.name = "Web";
|
|
info.icon = _get_platform_icon("Web");
|
|
info.templates = { TemplateID::WEB, TemplateID::WEB_EXTENSIONS, TemplateID::WEB_NOTHREADS, TemplateID::WEB_EXTENSIONS_NOTHREADS };
|
|
info.group = TTR("Web", "Platform Group");
|
|
platform_map[PlatformID::WEB] = info;
|
|
}
|
|
{
|
|
PlatformInfo info;
|
|
info.name = TTR("Common");
|
|
info.templates = { TemplateID::ICU_DATA };
|
|
platform_map[PlatformID::COMMON] = info;
|
|
}
|
|
|
|
// Template directory status.
|
|
DirAccess::make_dir_recursive_absolute(_get_template_folder_path(VERSION_FULL_CONFIG));
|
|
Ref<DirAccess> templates_dir = DirAccess::open(EditorPaths::get_singleton()->get_export_templates_dir());
|
|
ERR_FAIL_COND(templates_dir.is_null());
|
|
|
|
for (const String &dir : templates_dir->get_directories()) {
|
|
if (dir == GODOT_VERSION_FULL_CONFIG) {
|
|
version_list->add_item(dir);
|
|
version_list->set_item_custom_fg_color(-1, theme_cache.current_version_color);
|
|
version_list->select(version_list->get_item_count() - 1);
|
|
} else {
|
|
version_list->add_item(dir);
|
|
}
|
|
version_list->set_item_metadata(-1, dir);
|
|
}
|
|
}
|
|
|
|
void ExportTemplateManager::_update_template_tree() {
|
|
downloading_items.clear();
|
|
|
|
const String selected_version = version_list->get_item_text(version_list->get_current());
|
|
Ref<DirAccess> template_directory = DirAccess::open(_get_template_folder_path(selected_version));
|
|
ERR_FAIL_COND(template_directory.is_null());
|
|
|
|
bool is_current_version = (selected_version == GODOT_VERSION_FULL_CONFIG);
|
|
HashMap<TemplateID, LocalVector<String>> installed_template_files;
|
|
|
|
for (const KeyValue<PlatformID, PlatformInfo> &KV : platform_map) {
|
|
for (TemplateID id : KV.value.templates) {
|
|
for (const String &file : template_data[id].file_list) {
|
|
if (template_directory->file_exists(file)) {
|
|
installed_template_files[id].push_back(file);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_fill_template_tree(available_templates_tree, installed_template_files, is_current_version);
|
|
_fill_template_tree(installed_templates_tree, installed_template_files, is_current_version);
|
|
}
|
|
|
|
void ExportTemplateManager::_update_template_tree_theme(Tree *p_tree) {
|
|
if (is_downloading()) {
|
|
// Prevents hiding progress bar.
|
|
Ref<StyleBoxEmpty> empty_style;
|
|
empty_style.instantiate();
|
|
|
|
p_tree->add_theme_style_override(SNAME("hovered"), empty_style);
|
|
p_tree->add_theme_style_override(SNAME("hovered_dimmed"), empty_style);
|
|
p_tree->add_theme_style_override(SNAME("selected"), empty_style);
|
|
p_tree->add_theme_style_override(SNAME("selected_focus"), empty_style);
|
|
p_tree->add_theme_style_override(SNAME("hovered_selected"), empty_style);
|
|
p_tree->add_theme_style_override(SNAME("hovered_selected_focus"), empty_style);
|
|
} else {
|
|
p_tree->remove_theme_style_override(SNAME("hovered"));
|
|
p_tree->remove_theme_style_override(SNAME("hovered_dimmed"));
|
|
p_tree->remove_theme_style_override(SNAME("selected"));
|
|
p_tree->remove_theme_style_override(SNAME("selected_focus"));
|
|
p_tree->remove_theme_style_override(SNAME("hovered_selected"));
|
|
p_tree->remove_theme_style_override(SNAME("hovered_selected_focus"));
|
|
}
|
|
}
|
|
|
|
void ExportTemplateManager::_fill_template_tree(Tree *p_tree, const HashMap<TemplateID, LocalVector<String>> &p_installed_template_files, bool p_is_current_version) {
|
|
bool is_installed_tree = (p_tree == installed_templates_tree);
|
|
bool is_available_tree = !is_installed_tree; // For readability.
|
|
const LocalVector<String> empty_vector;
|
|
|
|
if (p_tree->get_root()) {
|
|
_update_folding_cache(p_tree->get_root());
|
|
p_tree->clear();
|
|
}
|
|
|
|
TreeItem *platform_parent = p_tree->create_item();
|
|
_setup_item_text(platform_parent, String());
|
|
|
|
if (is_available_tree && !p_is_current_version) {
|
|
TreeItem *nodownloadsforyou = platform_parent->create_child();
|
|
nodownloadsforyou->set_text(0, TTR("Downloads are only available for the current Godot version."));
|
|
nodownloadsforyou->set_custom_color(0, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
|
|
return;
|
|
}
|
|
|
|
String current_group;
|
|
for (const KeyValue<PlatformID, PlatformInfo> &KV : platform_map) {
|
|
const PlatformInfo &template_platform = KV.value;
|
|
|
|
bool all_installed = true;
|
|
bool any_installed = false;
|
|
for (TemplateID id : template_platform.templates) {
|
|
if (p_installed_template_files.has(id) && !queued_templates.has(template_data[id].name)) {
|
|
any_installed = true;
|
|
} else {
|
|
all_installed = false;
|
|
}
|
|
|
|
if (any_installed && !all_installed) {
|
|
// Not going to change anymore.
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ((is_available_tree && all_installed) || (is_installed_tree && !any_installed)) {
|
|
continue;
|
|
}
|
|
|
|
if (is_available_tree && template_platform.group != current_group) {
|
|
// Use platform groups only for available templates.
|
|
_apply_item_folding(platform_parent);
|
|
current_group = template_platform.group;
|
|
|
|
if (current_group.is_empty()) {
|
|
platform_parent = p_tree->get_root();
|
|
} else {
|
|
platform_parent = p_tree->create_item();
|
|
if (!is_downloading()) {
|
|
_set_item_type(platform_parent, TreeItem::CELL_MODE_CHECK);
|
|
}
|
|
_setup_item_text(platform_parent, current_group);
|
|
}
|
|
}
|
|
|
|
TreeItem *platform_item = platform_parent->create_child();
|
|
if (is_available_tree && !is_downloading()) {
|
|
_set_item_type(platform_item, TreeItem::CELL_MODE_CHECK);
|
|
}
|
|
_setup_item_text(platform_item, template_platform.name);
|
|
platform_item->set_icon(0, template_platform.icon);
|
|
platform_item->set_icon_max_width(0, theme_cache.icon_width);
|
|
|
|
for (TemplateID id : template_platform.templates) {
|
|
TemplateInfo &template_info = template_data[id];
|
|
|
|
bool is_template_installed = p_installed_template_files.has(id);
|
|
if (!queued_templates.has(template_info.name)) {
|
|
if (is_template_installed == is_available_tree) {
|
|
continue;
|
|
}
|
|
} else if (is_installed_tree) {
|
|
continue;
|
|
}
|
|
|
|
const LocalVector<String> &installed_files = is_template_installed ? p_installed_template_files[id] : empty_vector;
|
|
|
|
TreeItem *template_item;
|
|
if (template_platform.templates.size() == 1 && template_info.name == template_platform.name) {
|
|
// Single template with the same name as platform, so it can be skipped.
|
|
template_item = platform_item;
|
|
} else {
|
|
template_item = platform_item->create_child();
|
|
}
|
|
|
|
if (is_available_tree) {
|
|
if (queued_templates.has(template_info.name)) {
|
|
_set_item_type(template_item, TreeItem::CELL_MODE_CUSTOM);
|
|
template_item->add_button(0, theme_cache.cancel_icon, (int)ButtonID::CANCEL);
|
|
template_item->set_button_tooltip_text(0, -1, TTR("Cancel downloading this template."));
|
|
} else if (!is_downloading()) {
|
|
_set_item_type(template_item, TreeItem::CELL_MODE_CHECK);
|
|
}
|
|
}
|
|
_setup_item_text(template_item, template_info.name);
|
|
template_item->set_tooltip_text(0, TTR(template_info.description));
|
|
|
|
bool any_missing = false;
|
|
bool any_failed = false;
|
|
for (const String &file : template_info.file_list) {
|
|
FileMetadata *meta = _get_file_metadata(file);
|
|
|
|
TreeItem *file_item = template_item->create_child();
|
|
file_item->set_meta(FILE_META, true);
|
|
|
|
if (meta->download_status == DownloadStatus::FAILED) {
|
|
_add_fail_reason_button(file_item, file);
|
|
any_failed = true;
|
|
}
|
|
|
|
if (is_available_tree && !is_downloading()) {
|
|
_set_item_type(file_item, TreeItem::CELL_MODE_CHECK);
|
|
} else if (meta->download_status != DownloadStatus::NONE || queued_files.has(file)) {
|
|
if (!_status_is_finished(meta->download_status)) {
|
|
_set_item_type(file_item, TreeItem::CELL_MODE_CUSTOM);
|
|
|
|
file_item->add_button(0, theme_cache.cancel_icon, (int)ButtonID::CANCEL);
|
|
file_item->set_button_tooltip_text(0, -1, TTRC("Cancel downloading this file."));
|
|
downloading_items.push_back(file_item);
|
|
|
|
if (meta->download_status == DownloadStatus::NONE) {
|
|
meta->download_status = DownloadStatus::PENDING;
|
|
}
|
|
}
|
|
}
|
|
_setup_item_text(file_item, file);
|
|
|
|
if (is_installed_tree) {
|
|
if (installed_files.has(file)) {
|
|
file_item->add_button(0, theme_cache.remove_icon, (int)ButtonID::REMOVE);
|
|
file_item->set_button_tooltip_text(0, -1, TTR("Remove this file."));
|
|
} else {
|
|
file_item->set_custom_color(0, theme_cache.missing_file_color);
|
|
if (p_is_current_version && !is_downloading() && _can_download_templates()) {
|
|
file_item->add_button(0, theme_cache.install_icon, (int)ButtonID::DOWNLOAD);
|
|
file_item->set_button_tooltip_text(0, -1, TTR("Download this missing file."));
|
|
}
|
|
meta->is_missing = true;
|
|
any_missing = true;
|
|
}
|
|
}
|
|
}
|
|
if (any_failed || any_missing) {
|
|
template_item->set_custom_color(0, theme_cache.incomplete_template_color);
|
|
if (any_failed) {
|
|
template_item->add_button(0, theme_cache.failure_icon, (int)ButtonID::NONE);
|
|
template_item->set_button_tooltip_text(0, -1, TTR("Some files have failed to download."));
|
|
}
|
|
|
|
if (any_missing && p_is_current_version && !is_downloading() && _can_download_templates()) {
|
|
template_item->add_button(0, theme_cache.repair_icon, (int)ButtonID::REPAIR);
|
|
template_item->set_button_tooltip_text(0, -1, TTR("Download missing template files."));
|
|
}
|
|
}
|
|
if (is_installed_tree) {
|
|
template_item->add_button(0, theme_cache.remove_icon, (int)ButtonID::REMOVE);
|
|
template_item->set_button_tooltip_text(0, -1, TTR("Remove this template."));
|
|
}
|
|
_apply_item_folding(template_item, true);
|
|
}
|
|
_apply_item_folding(platform_item);
|
|
}
|
|
|
|
if (p_tree->get_root()->get_child_count() == 0) {
|
|
TreeItem *empty = p_tree->create_item();
|
|
empty->set_text(0, is_available_tree ? TTR("All templates installed.") : TTR("No templates installed."));
|
|
empty->set_custom_color(0, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
|
|
}
|
|
}
|
|
|
|
void ExportTemplateManager::_update_install_button() {
|
|
if (is_downloading()) {
|
|
install_button->set_text(TTRC("Downloading templates..."));
|
|
install_button->set_disabled(true);
|
|
install_button->set_tooltip_text(String());
|
|
return;
|
|
}
|
|
|
|
download_all_enabled = true;
|
|
for (TreeItem *item = available_templates_tree->get_root(); item; item = item->get_next_in_tree()) {
|
|
if (item->is_checked(0)) {
|
|
download_all_enabled = false;
|
|
break;
|
|
}
|
|
}
|
|
if (download_all_enabled) {
|
|
install_button->set_text(TTRC("Install All Templates"));
|
|
} else {
|
|
install_button->set_text(TTRC("Install Selected Templates"));
|
|
}
|
|
|
|
install_button->set_disabled(!_can_download_templates());
|
|
if (install_button->is_disabled()) {
|
|
if (mirrors_empty) {
|
|
install_button->set_tooltip_text(TTRC("No mirrors available for download."));
|
|
} else if (!_is_online()) {
|
|
install_button->set_tooltip_text(TTRC("Download not available in offline mode."));
|
|
} else {
|
|
install_button->set_tooltip_text(TTRC("Downloads are only available for the current Godot version."));
|
|
}
|
|
} else {
|
|
install_button->set_tooltip_text(String());
|
|
}
|
|
}
|
|
|
|
bool ExportTemplateManager::_can_download_templates() {
|
|
const String selected_version = version_list->get_item_text(version_list->get_current());
|
|
return !mirrors_empty && _is_online() && selected_version == GODOT_VERSION_FULL_CONFIG;
|
|
}
|
|
|
|
void ExportTemplateManager::_update_folding_cache(TreeItem *p_item) {
|
|
folding_cache[_get_item_path(p_item)] = p_item->is_collapsed();
|
|
if (p_item->get_cell_mode(0) == TreeItem::CELL_MODE_CHECK) {
|
|
if (p_item->is_indeterminate(0)) {
|
|
checked_cache[_get_item_path(p_item)] = 1;
|
|
} else {
|
|
checked_cache[_get_item_path(p_item)] = p_item->is_checked(0) ? 2 : 0;
|
|
}
|
|
}
|
|
for (TreeItem *child = p_item->get_first_child(); child; child = child->get_next()) {
|
|
_update_folding_cache(child);
|
|
}
|
|
}
|
|
|
|
String ExportTemplateManager::_get_template_folder_path(const String &p_version) const {
|
|
return EditorPaths::get_singleton()->get_export_templates_dir().path_join(p_version);
|
|
}
|
|
|
|
Ref<Texture2D> ExportTemplateManager::_get_platform_icon(const String &p_platform_name) {
|
|
for (int i = 0; i < EditorExport::get_singleton()->get_export_platform_count(); i++) {
|
|
Ref<EditorExportPlatform> platform = EditorExport::get_singleton()->get_export_platform(i);
|
|
if (platform->get_name() == p_platform_name) {
|
|
return platform->get_logo();
|
|
}
|
|
}
|
|
return Ref<Texture2D>();
|
|
}
|
|
|
|
void ExportTemplateManager::_version_selected() {
|
|
if (!is_downloading()) {
|
|
file_metadata.clear();
|
|
_update_template_tree();
|
|
}
|
|
_update_install_button();
|
|
}
|
|
|
|
void ExportTemplateManager::_tree_button_clicked(TreeItem *p_item, int p_column, int p_id, MouseButton p_button) {
|
|
switch ((ButtonID)p_id) {
|
|
case ButtonID::DOWNLOAD: {
|
|
_install_templates(p_item);
|
|
} break;
|
|
|
|
case ButtonID::REPAIR: {
|
|
p_item->set_collapsed(false);
|
|
_install_templates(p_item);
|
|
} break;
|
|
|
|
case ButtonID::REMOVE: {
|
|
item_to_delete = p_item;
|
|
confirm_delete->popup_centered();
|
|
} break;
|
|
|
|
case ButtonID::CANCEL: {
|
|
if (_item_is_file(p_item)) {
|
|
_cancel_item_download(p_item);
|
|
if (_is_template_download_finished(p_item->get_parent())) {
|
|
queued_templates.erase(p_item->get_parent()->get_text(0));
|
|
}
|
|
} else {
|
|
queued_templates.erase(p_item->get_text(0));
|
|
for (TreeItem *child = p_item->get_first_child(); child; child = child->get_next()) {
|
|
if (_get_file_metadata(child)->download_status != DownloadStatus::NONE) {
|
|
_cancel_item_download(child);
|
|
}
|
|
}
|
|
}
|
|
_process_download_queue();
|
|
_update_template_tree();
|
|
} break;
|
|
|
|
case ButtonID::FAIL: {
|
|
FileMetadata *meta = _get_file_metadata(p_item);
|
|
EditorNode::get_singleton()->show_warning(meta->fail_reason + ".", TTR("Download Failed"));
|
|
} break;
|
|
|
|
case ButtonID::NONE: {
|
|
} break;
|
|
}
|
|
}
|
|
|
|
void ExportTemplateManager::_tree_item_edited() {
|
|
TreeItem *edited = available_templates_tree->get_edited();
|
|
ERR_FAIL_NULL(edited);
|
|
|
|
edited->propagate_check(0, false);
|
|
_update_install_button();
|
|
}
|
|
|
|
void ExportTemplateManager::_install_templates(TreeItem *p_files) {
|
|
_queue_download_tree_item(p_files ? p_files : available_templates_tree->get_root());
|
|
download_count = queued_files.size();
|
|
|
|
file_metadata.clear();
|
|
_update_template_tree();
|
|
_process_download_queue();
|
|
_update_install_button();
|
|
_update_template_tree_theme(installed_templates_tree);
|
|
_update_template_tree_theme(available_templates_tree);
|
|
|
|
ProgressIndicator *indicator = EditorNode::get_bottom_panel()->get_progress_indicator();
|
|
indicator->set_tooltip_text(TTRC("Downloading export templates..."));
|
|
indicator->set_value(0);
|
|
indicator->show();
|
|
}
|
|
|
|
void ExportTemplateManager::_open_template_directory() {
|
|
const String selected_version = version_list->get_item_text(version_list->get_current());
|
|
OS::get_singleton()->shell_show_in_file_manager(_get_template_folder_path(selected_version), true);
|
|
}
|
|
|
|
void ExportTemplateManager::_queue_download_tree_item(TreeItem *p_item) {
|
|
if (_item_is_file(p_item)) {
|
|
bool valid;
|
|
bool is_installed_tree = p_item->get_tree() == installed_templates_tree;
|
|
if (is_installed_tree) {
|
|
FileMetadata *meta = _get_file_metadata(p_item);
|
|
valid = meta->is_missing;
|
|
} else {
|
|
valid = download_all_enabled || p_item->is_checked(0);
|
|
}
|
|
|
|
if (valid) {
|
|
queued_files.insert(p_item->get_text(0));
|
|
if (!is_installed_tree) {
|
|
queued_templates.insert(p_item->get_parent()->get_text(0));
|
|
}
|
|
}
|
|
} else {
|
|
for (TreeItem *child = p_item->get_first_child(); child; child = child->get_next()) {
|
|
_queue_download_tree_item(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ExportTemplateManager::_process_download_queue() {
|
|
queue_update_pending = false;
|
|
|
|
int downloader_index = 0;
|
|
bool is_finished = true;
|
|
for (TreeItem *item : downloading_items) {
|
|
FileMetadata *meta = _get_file_metadata(item);
|
|
|
|
is_finished = is_finished && _status_is_finished(meta->download_status);
|
|
if (meta->download_status != DownloadStatus::PENDING) {
|
|
continue;
|
|
}
|
|
|
|
TemplateDownloader *downloader = _get_available_downloader(&downloader_index);
|
|
if (!downloader) {
|
|
break;
|
|
}
|
|
downloader_index++;
|
|
|
|
Error err = downloader->download_template(item->get_text(0), _get_current_mirror_url());
|
|
if (err == OK) {
|
|
meta->download_status = DownloadStatus::IN_PROGRESS;
|
|
meta->downloader = downloader;
|
|
} else {
|
|
_item_download_failed(item, vformat(TTR("Download request failed: %s."), TTR(error_names[err])));
|
|
}
|
|
}
|
|
|
|
if (is_finished) {
|
|
// Exit "downloading mode".
|
|
queued_templates.clear();
|
|
downloading_items.clear();
|
|
set_process_internal(false);
|
|
_update_template_tree_theme(installed_templates_tree);
|
|
_update_template_tree_theme(available_templates_tree);
|
|
_update_install_button();
|
|
EditorNode::get_bottom_panel()->get_progress_indicator()->hide();
|
|
} else {
|
|
set_process_internal(true);
|
|
}
|
|
}
|
|
|
|
void ExportTemplateManager::_queue_process_download_queue() {
|
|
if (queue_update_pending) {
|
|
return;
|
|
}
|
|
callable_mp(this, &ExportTemplateManager::_process_download_queue).call_deferred();
|
|
queue_update_pending = true;
|
|
}
|
|
|
|
TemplateDownloader *ExportTemplateManager::_get_available_downloader(int *r_from_index) {
|
|
int counter = -1;
|
|
for (TemplateDownloader *downloader : downloaders) {
|
|
counter++;
|
|
if (counter < *r_from_index) {
|
|
continue;
|
|
}
|
|
if (!downloader->is_downloading()) {
|
|
*r_from_index = counter;
|
|
return downloader;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void ExportTemplateManager::_download_request_completed(const String &p_filename) {
|
|
bool found = false;
|
|
bool template_finished = false;
|
|
|
|
queued_files.erase(p_filename);
|
|
for (TreeItem *item : downloading_items) {
|
|
if (item->get_text(0) != p_filename) {
|
|
continue;
|
|
}
|
|
item->clear_buttons();
|
|
|
|
FileMetadata *meta = _get_file_metadata(p_filename);
|
|
meta->downloader = nullptr;
|
|
meta->download_status = DownloadStatus::COMPLETED;
|
|
meta->is_missing = false;
|
|
|
|
found = true;
|
|
template_finished = _is_template_download_finished(item->get_parent());
|
|
if (template_finished) {
|
|
queued_templates.erase(item->get_parent()->get_text(0));
|
|
}
|
|
break;
|
|
}
|
|
if (!found) {
|
|
ERR_FAIL_COND(!found);
|
|
}
|
|
_queue_process_download_queue();
|
|
|
|
if (template_finished) {
|
|
_update_template_tree();
|
|
}
|
|
}
|
|
|
|
void ExportTemplateManager::_download_request_failed(const String &p_filename, const String &p_reason) {
|
|
bool found = false;
|
|
bool template_finished = false;
|
|
|
|
queued_files.erase(p_filename);
|
|
for (TreeItem *item : downloading_items) {
|
|
if (item->get_text(0) != p_filename) {
|
|
continue;
|
|
}
|
|
|
|
FileMetadata *meta = _get_file_metadata(p_filename);
|
|
meta->downloader = nullptr;
|
|
|
|
_item_download_failed(item, p_reason);
|
|
|
|
found = true;
|
|
template_finished = _is_template_download_finished(item->get_parent());
|
|
if (template_finished) {
|
|
queued_templates.erase(item->get_parent()->get_text(0));
|
|
}
|
|
break;
|
|
}
|
|
if (!found) {
|
|
ERR_FAIL_COND(!found);
|
|
}
|
|
_queue_process_download_queue();
|
|
|
|
if (template_finished) {
|
|
_update_template_tree();
|
|
}
|
|
}
|
|
|
|
bool ExportTemplateManager::_is_template_download_finished(TreeItem *p_template) {
|
|
for (TreeItem *child = p_template->get_first_child(); child; child = child->get_next()) {
|
|
if (!downloading_items.has(child)) {
|
|
continue;
|
|
}
|
|
FileMetadata *meta = _get_file_metadata(child);
|
|
if (!_status_is_finished(meta->download_status)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ExportTemplateManager::_apply_item_folding(TreeItem *p_item, bool p_default) {
|
|
if (folding_cache.is_empty()) {
|
|
if (p_default) {
|
|
p_item->set_collapsed(true);
|
|
}
|
|
} else {
|
|
bool *cached = folding_cache.getptr(_get_item_path(p_item));
|
|
if (cached) {
|
|
p_item->set_collapsed(*cached);
|
|
} else if (p_default) {
|
|
p_item->set_collapsed(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ExportTemplateManager::_cancel_item_download(TreeItem *p_item) {
|
|
_item_download_failed(p_item, TTR("Canceled by the user"));
|
|
queued_files.erase(p_item->get_text(0));
|
|
|
|
FileMetadata *meta = _get_file_metadata(p_item);
|
|
if (meta->downloader) {
|
|
meta->downloader->cancel_download();
|
|
meta->downloader = nullptr;
|
|
}
|
|
}
|
|
|
|
void ExportTemplateManager::_item_download_failed(TreeItem *p_item, const String &p_reason) {
|
|
FileMetadata *meta = _get_file_metadata(p_item);
|
|
meta->fail_reason = p_reason;
|
|
meta->download_status = DownloadStatus::FAILED;
|
|
|
|
p_item->clear_buttons();
|
|
_add_fail_reason_button(p_item);
|
|
}
|
|
|
|
void ExportTemplateManager::_add_fail_reason_button(TreeItem *p_item, const String &p_filename) {
|
|
FileMetadata *meta = _get_file_metadata(p_filename.is_empty() ? p_item->get_text(0) : p_filename);
|
|
p_item->add_button(0, theme_cache.failure_icon, (int)ButtonID::FAIL);
|
|
p_item->set_button_tooltip_text(0, -1, vformat(TTR("Download failed.\nReason: %s."), meta->fail_reason));
|
|
}
|
|
|
|
void ExportTemplateManager::_set_item_type(TreeItem *p_item, int p_type) {
|
|
switch (p_type) {
|
|
case TreeItem::CELL_MODE_CHECK: {
|
|
p_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
|
|
p_item->set_editable(0, true);
|
|
} break;
|
|
|
|
case TreeItem::CELL_MODE_CUSTOM: {
|
|
p_item->set_cell_mode(0, TreeItem::CELL_MODE_CUSTOM);
|
|
p_item->set_custom_draw_callback(0, callable_mp(this, &ExportTemplateManager::_draw_item_progress));
|
|
} break;
|
|
}
|
|
}
|
|
|
|
void ExportTemplateManager::_setup_item_text(TreeItem *p_item, const String &p_text) {
|
|
if (p_item == p_item->get_tree()->get_root()) {
|
|
if (p_item->get_tree() == installed_templates_tree) {
|
|
p_item->set_meta(PATH_META, "installed/");
|
|
} else {
|
|
p_item->set_meta(PATH_META, "available/");
|
|
}
|
|
} else {
|
|
p_item->set_text(0, p_text);
|
|
const String path = p_item->get_parent()->get_meta(PATH_META).operator String() + p_text;
|
|
p_item->set_meta(PATH_META, path);
|
|
|
|
if (p_item->get_cell_mode(0) == TreeItem::CELL_MODE_CHECK) {
|
|
int *checked = checked_cache.getptr(path);
|
|
if (checked) {
|
|
if (*checked == 1) {
|
|
p_item->set_indeterminate(0, true);
|
|
} else {
|
|
p_item->set_checked(0, *checked == 2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ExportTemplateManager::FileMetadata *ExportTemplateManager::_get_file_metadata(const String &p_text) const {
|
|
FileMetadata *meta = file_metadata.getptr(p_text);
|
|
if (likely(meta)) {
|
|
return meta;
|
|
}
|
|
HashMap<String, FileMetadata>::Iterator it = file_metadata.insert(p_text, FileMetadata());
|
|
return &it->value;
|
|
}
|
|
|
|
ExportTemplateManager::FileMetadata *ExportTemplateManager::_get_file_metadata(const TreeItem *p_item) const {
|
|
return _get_file_metadata(p_item->get_text(0));
|
|
}
|
|
|
|
String ExportTemplateManager::_get_item_path(TreeItem *p_item) const {
|
|
return p_item->get_meta(PATH_META, String());
|
|
}
|
|
|
|
bool ExportTemplateManager::_item_is_file(TreeItem *p_item) const {
|
|
return p_item->get_meta(FILE_META, false).operator bool();
|
|
}
|
|
|
|
float ExportTemplateManager::_get_download_progress(const TreeItem *p_item) const {
|
|
FileMetadata *meta = _get_file_metadata(p_item);
|
|
switch (meta->download_status) {
|
|
case DownloadStatus::NONE:
|
|
case DownloadStatus::PENDING: {
|
|
return 0.0;
|
|
}
|
|
|
|
case DownloadStatus::IN_PROGRESS: {
|
|
if (!meta->downloader) {
|
|
return 0.0;
|
|
}
|
|
return meta->downloader->get_download_progress();
|
|
}
|
|
|
|
case DownloadStatus::COMPLETED: {
|
|
return 1.0;
|
|
}
|
|
|
|
case DownloadStatus::FAILED: {
|
|
return meta->progress_cache;
|
|
}
|
|
}
|
|
return 0.0;
|
|
}
|
|
|
|
void ExportTemplateManager::_draw_item_progress(TreeItem *p_item, const Rect2 &p_rect) {
|
|
Tree *owning_tree = p_item->get_tree();
|
|
owning_tree->draw_rect(p_rect, Color(0, 0, 0, 0.5));
|
|
|
|
if (!_item_is_file(p_item)) {
|
|
float progress = 0.0;
|
|
int item_count = 0;
|
|
|
|
bool has_fail = false;
|
|
for (TreeItem *child = p_item->get_first_child(); child; child = child->get_next()) {
|
|
if (!downloading_items.has(child)) {
|
|
continue;
|
|
}
|
|
item_count++;
|
|
progress += _get_download_progress(child);
|
|
|
|
FileMetadata *meta = _get_file_metadata(child);
|
|
has_fail = has_fail || meta->download_status == DownloadStatus::FAILED;
|
|
}
|
|
progress /= item_count;
|
|
owning_tree->draw_rect(Rect2(p_rect.position, Vector2(p_rect.size.x * progress, p_rect.size.y)), has_fail ? theme_cache.download_failed_color : theme_cache.download_progress_color);
|
|
return;
|
|
}
|
|
|
|
FileMetadata *meta = _get_file_metadata(p_item);
|
|
switch (meta->download_status) {
|
|
case DownloadStatus::NONE: {
|
|
} break;
|
|
|
|
case DownloadStatus::PENDING: {
|
|
uint64_t frame = Engine::get_singleton()->get_frames_drawn();
|
|
const Ref<Texture2D> progress_texture = theme_cache.progress_icons[frame / 4 % 8];
|
|
owning_tree->draw_texture(progress_texture, Vector2(p_rect.get_end().x - progress_texture->get_width(), p_rect.position.y + p_rect.size.y * 0.5 - progress_texture->get_height() * 0.5));
|
|
} break;
|
|
|
|
case DownloadStatus::IN_PROGRESS: {
|
|
float progress = _get_download_progress(p_item);
|
|
meta->progress_cache = progress;
|
|
owning_tree->draw_rect(Rect2(p_rect.position, Vector2(p_rect.size.x * progress, p_rect.size.y)), theme_cache.download_progress_color);
|
|
} break;
|
|
|
|
case DownloadStatus::COMPLETED: {
|
|
owning_tree->draw_rect(p_rect, theme_cache.download_progress_color);
|
|
} break;
|
|
|
|
case DownloadStatus::FAILED: {
|
|
owning_tree->draw_rect(Rect2(p_rect.position, Vector2(p_rect.size.x * _get_download_progress(p_item), p_rect.size.y)), theme_cache.download_failed_color);
|
|
} break;
|
|
}
|
|
}
|
|
|
|
void ExportTemplateManager::_notification(int p_what) {
|
|
switch (p_what) {
|
|
case NOTIFICATION_READY: {
|
|
EditorNode::get_bottom_panel()->get_progress_indicator()->connect("clicked", callable_mp(this, &ExportTemplateManager::popup_manager));
|
|
} break;
|
|
|
|
case NOTIFICATION_TRANSLATION_CHANGED: {
|
|
if (template_data.is_empty()) {
|
|
break;
|
|
}
|
|
platform_map[PlatformID::WINDOWS].group = TTR("Desktop", "Platform Group");
|
|
platform_map[PlatformID::LINUX].group = TTR("Desktop", "Platform Group");
|
|
platform_map[PlatformID::MACOS].group = TTR("Desktop", "Platform Group");
|
|
platform_map[PlatformID::WEB].group = TTR("Web", "Platform Group");
|
|
platform_map[PlatformID::ANDROID].group = TTR("Mobile", "Platform Group");
|
|
platform_map[PlatformID::IOS].group = TTR("Mobile", "Platform Group");
|
|
platform_map[PlatformID::COMMON].name = TTR("Common");
|
|
template_data[TemplateID::WEB_EXTENSIONS].name = TTR("Web with Extensions");
|
|
template_data[TemplateID::WEB_NOTHREADS].name = TTR("Web Single-Threaded");
|
|
template_data[TemplateID::WEB_EXTENSIONS_NOTHREADS].name = TTR("Web with Extensions Single-Threaded");
|
|
template_data[TemplateID::ANDROID_SOURCE].name = TTR("Android Source");
|
|
template_data[TemplateID::ANDROID_SOURCE].name = TTR("ICU Data");
|
|
} break;
|
|
|
|
case NOTIFICATION_THEME_CHANGED: {
|
|
open_folder_button->set_button_icon(get_editor_theme_icon("Folder"));
|
|
install_button->set_button_icon(get_editor_theme_icon("AssetStore"));
|
|
open_mirror->set_button_icon(get_editor_theme_icon("ExternalLink"));
|
|
|
|
theme_cache.install_icon = get_editor_theme_icon("AssetStore");
|
|
theme_cache.remove_icon = get_editor_theme_icon("Remove");
|
|
theme_cache.repair_icon = get_editor_theme_icon("Tools");
|
|
theme_cache.failure_icon = get_editor_theme_icon("NodeWarning");
|
|
theme_cache.cancel_icon = get_editor_theme_icon("Close");
|
|
for (int i = 0; i < 8; i++) {
|
|
theme_cache.progress_icons[i] = get_editor_theme_icon("Progress" + itos(i + 1));
|
|
}
|
|
|
|
theme_cache.current_version_color = get_theme_color("accent_color", EditorStringName(Editor));
|
|
theme_cache.incomplete_template_color = get_theme_color("warning_color", EditorStringName(Editor));
|
|
theme_cache.missing_file_color = get_theme_color("error_color", EditorStringName(Editor));
|
|
theme_cache.download_progress_color = Color(get_theme_color("success_color", EditorStringName(Editor)), 0.5);
|
|
theme_cache.download_failed_color = Color(theme_cache.missing_file_color, 0.5);
|
|
|
|
theme_cache.icon_width = get_theme_constant("class_icon_size", EditorStringName(Editor));
|
|
} break;
|
|
|
|
case NOTIFICATION_INTERNAL_PROCESS: {
|
|
available_templates_tree->queue_redraw();
|
|
installed_templates_tree->queue_redraw();
|
|
|
|
float progress = 0.0;
|
|
int indeterminate_count = download_count;
|
|
for (const TreeItem *item : downloading_items) {
|
|
progress += _get_download_progress(item);
|
|
indeterminate_count--;
|
|
}
|
|
progress += indeterminate_count;
|
|
EditorNode::get_bottom_panel()->get_progress_indicator()->set_value(progress / download_count);
|
|
}
|
|
}
|
|
}
|
|
|
|
String ExportTemplateManager::get_android_build_directory(const Ref<EditorExportPreset> &p_preset) {
|
|
if (p_preset.is_valid()) {
|
|
String gradle_build_dir = p_preset->get("gradle_build/gradle_build_directory");
|
|
if (!gradle_build_dir.is_empty()) {
|
|
return gradle_build_dir.path_join("build");
|
|
}
|
|
}
|
|
return "res://android/build";
|
|
}
|
|
|
|
String ExportTemplateManager::get_android_source_zip(const Ref<EditorExportPreset> &p_preset) {
|
|
if (p_preset.is_valid()) {
|
|
String android_source_zip = p_preset->get("gradle_build/android_source_template");
|
|
if (!android_source_zip.is_empty()) {
|
|
return android_source_zip;
|
|
}
|
|
}
|
|
|
|
const String templates_dir = EditorPaths::get_singleton()->get_export_templates_dir().path_join(GODOT_VERSION_FULL_CONFIG);
|
|
return templates_dir.path_join("android_source.zip");
|
|
}
|
|
|
|
String ExportTemplateManager::get_android_template_identifier(const Ref<EditorExportPreset> &p_preset) {
|
|
// The template identifier is the Godot version for the default template, and the full path plus md5 hash for custom templates.
|
|
if (p_preset.is_valid()) {
|
|
String android_source_zip = p_preset->get("gradle_build/android_source_template");
|
|
if (!android_source_zip.is_empty()) {
|
|
return android_source_zip + String(" [") + FileAccess::get_md5(android_source_zip) + String("]");
|
|
}
|
|
}
|
|
return GODOT_VERSION_FULL_CONFIG;
|
|
}
|
|
|
|
bool ExportTemplateManager::is_android_template_installed(const Ref<EditorExportPreset> &p_preset) {
|
|
return DirAccess::exists(get_android_build_directory(p_preset));
|
|
}
|
|
|
|
bool ExportTemplateManager::can_install_android_template(const Ref<EditorExportPreset> &p_preset) {
|
|
return FileAccess::exists(get_android_source_zip(p_preset));
|
|
}
|
|
|
|
Error ExportTemplateManager::install_android_template(const Ref<EditorExportPreset> &p_preset) {
|
|
const String source_zip = get_android_source_zip(p_preset);
|
|
ERR_FAIL_COND_V(!FileAccess::exists(source_zip), ERR_CANT_OPEN);
|
|
return install_android_template_from_file(source_zip, p_preset);
|
|
}
|
|
|
|
Error ExportTemplateManager::install_android_template_from_file(const String &p_file, const Ref<EditorExportPreset> &p_preset) {
|
|
// To support custom Android builds, we install the Java source code and buildsystem
|
|
// from android_source.zip to the project's res://android folder.
|
|
|
|
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
|
|
ERR_FAIL_COND_V(da.is_null(), ERR_CANT_CREATE);
|
|
|
|
String build_dir = get_android_build_directory(p_preset);
|
|
String parent_dir = build_dir.get_base_dir();
|
|
|
|
// Make parent of the build dir (if it does not exist).
|
|
da->make_dir_recursive(parent_dir);
|
|
{
|
|
// Add identifier, to ensure building won't work if the current template doesn't match.
|
|
Ref<FileAccess> f = FileAccess::open(parent_dir.path_join(".build_version"), FileAccess::WRITE);
|
|
ERR_FAIL_COND_V(f.is_null(), ERR_CANT_CREATE);
|
|
f->store_line(get_android_template_identifier(p_preset));
|
|
}
|
|
|
|
// Create the android build directory.
|
|
Error err = da->make_dir_recursive(build_dir);
|
|
ERR_FAIL_COND_V(err != OK, err);
|
|
{
|
|
// Add an empty .gdignore file to avoid scan.
|
|
Ref<FileAccess> f = FileAccess::open(build_dir.path_join(".gdignore"), FileAccess::WRITE);
|
|
ERR_FAIL_COND_V(f.is_null(), ERR_CANT_CREATE);
|
|
f->store_line("");
|
|
}
|
|
|
|
// Uncompress source template.
|
|
|
|
Ref<FileAccess> io_fa;
|
|
zlib_filefunc_def io = zipio_create_io(&io_fa);
|
|
|
|
unzFile pkg = unzOpen2(p_file.utf8().get_data(), &io);
|
|
ERR_FAIL_NULL_V_MSG(pkg, ERR_CANT_OPEN, "Android sources not in ZIP format.");
|
|
|
|
int ret = unzGoToFirstFile(pkg);
|
|
int total_files = 0;
|
|
// Count files to unzip.
|
|
while (ret == UNZ_OK) {
|
|
total_files++;
|
|
ret = unzGoToNextFile(pkg);
|
|
}
|
|
ret = unzGoToFirstFile(pkg);
|
|
|
|
ProgressDialog::get_singleton()->add_task("uncompress_src", TTR("Uncompressing Android Build Sources"), total_files);
|
|
|
|
HashSet<String> dirs_tested;
|
|
int idx = 0;
|
|
while (ret == UNZ_OK) {
|
|
// Get file path.
|
|
unz_file_info info;
|
|
char fpath[16384];
|
|
ret = unzGetCurrentFileInfo(pkg, &info, fpath, 16384, nullptr, 0, nullptr, 0);
|
|
if (ret != UNZ_OK) {
|
|
break;
|
|
}
|
|
|
|
String path = String::utf8(fpath);
|
|
String base_dir = path.get_base_dir();
|
|
|
|
if (!path.ends_with("/")) {
|
|
Vector<uint8_t> uncomp_data;
|
|
uncomp_data.resize(info.uncompressed_size);
|
|
|
|
// Read.
|
|
unzOpenCurrentFile(pkg);
|
|
unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size());
|
|
unzCloseCurrentFile(pkg);
|
|
|
|
if (!dirs_tested.has(base_dir)) {
|
|
da->make_dir_recursive(build_dir.path_join(base_dir));
|
|
dirs_tested.insert(base_dir);
|
|
}
|
|
|
|
String to_write = build_dir.path_join(path);
|
|
Ref<FileAccess> f = FileAccess::open(to_write, FileAccess::WRITE);
|
|
if (f.is_valid()) {
|
|
f->store_buffer(uncomp_data.ptr(), uncomp_data.size());
|
|
f.unref(); // close file.
|
|
#ifndef WINDOWS_ENABLED
|
|
FileAccess::set_unix_permissions(to_write, (info.external_fa >> 16) & 0x01FF);
|
|
#endif
|
|
} else {
|
|
ERR_PRINT("Can't uncompress file: " + to_write);
|
|
}
|
|
}
|
|
|
|
ProgressDialog::get_singleton()->task_step("uncompress_src", path, idx);
|
|
|
|
idx++;
|
|
ret = unzGoToNextFile(pkg);
|
|
}
|
|
|
|
ProgressDialog::get_singleton()->end_task("uncompress_src");
|
|
unzClose(pkg);
|
|
EditorFileSystem::get_singleton()->scan_changes();
|
|
return OK;
|
|
}
|
|
|
|
void ExportTemplateManager::popup_manager() {
|
|
if (template_data.is_empty()) {
|
|
_initialize_template_data();
|
|
}
|
|
_update_online_mode();
|
|
|
|
if (!is_downloading()) {
|
|
_update_template_tree();
|
|
_request_mirrors();
|
|
}
|
|
popup_centered_clamped(Vector2i(640, 700) * EDSCALE);
|
|
}
|
|
|
|
bool ExportTemplateManager::is_downloading() const {
|
|
return !queued_files.is_empty();
|
|
}
|
|
|
|
void ExportTemplateManager::stop_download() {
|
|
for (TreeItem *item : downloading_items) {
|
|
FileMetadata *meta = _get_file_metadata(item);
|
|
if (meta && !_status_is_finished(meta->download_status)) {
|
|
_cancel_item_download(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
ExportTemplateManager::ExportTemplateManager() {
|
|
set_title(TTRC("Export Template Manager"));
|
|
set_ok_button_text(TTRC("Close"));
|
|
|
|
VBoxContainer *main_vb = memnew(VBoxContainer);
|
|
add_child(main_vb);
|
|
|
|
HBoxContainer *download_header = memnew(HBoxContainer);
|
|
download_header->set_alignment(BoxContainer::ALIGNMENT_BEGIN);
|
|
main_vb->add_child(download_header);
|
|
|
|
download_header->add_child(memnew(Label(TTRC("Download from:"))));
|
|
|
|
mirrors_list = memnew(OptionButton);
|
|
mirrors_list->set_accessibility_name(TTRC("Mirror"));
|
|
download_header->add_child(mirrors_list);
|
|
|
|
open_mirror = memnew(Button);
|
|
open_mirror->set_tooltip_text(TTRC("Open in Web Browser"));
|
|
download_header->add_child(open_mirror);
|
|
open_mirror->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_open_mirror));
|
|
|
|
install_button = memnew(Button);
|
|
install_button->set_h_size_flags(Control::SIZE_SHRINK_END | Control::SIZE_EXPAND);
|
|
download_header->add_child(install_button);
|
|
install_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_install_templates).bind((TreeItem *)nullptr));
|
|
|
|
HSplitContainer *main_split = memnew(HSplitContainer);
|
|
main_split->set_v_size_flags(Control::SIZE_EXPAND_FILL);
|
|
main_vb->add_child(main_split);
|
|
|
|
VBoxContainer *side_vb = memnew(VBoxContainer);
|
|
main_split->add_child(side_vb);
|
|
|
|
Label *version_header = memnew(Label(TTRC("Godot Version")));
|
|
version_header->set_theme_type_variation("HeaderSmall");
|
|
side_vb->add_child(version_header);
|
|
|
|
version_list = memnew(ItemList);
|
|
version_list->set_accessibility_name(TTRC("Godot Version List"));
|
|
version_list->set_theme_type_variation("ItemListSecondary");
|
|
version_list->set_v_size_flags(Control::SIZE_EXPAND_FILL);
|
|
side_vb->add_child(version_list);
|
|
version_list->connect(SceneStringName(item_selected), callable_mp(this, &ExportTemplateManager::_version_selected).unbind(1));
|
|
|
|
open_folder_button = memnew(Button);
|
|
open_folder_button->set_tooltip_text(TTRC("Open templates directory."));
|
|
open_folder_button->set_h_size_flags(Control::SIZE_SHRINK_BEGIN);
|
|
side_vb->add_child(open_folder_button);
|
|
open_folder_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_open_template_directory));
|
|
|
|
VSplitContainer *center_split = memnew(VSplitContainer);
|
|
center_split->set_h_size_flags(Control::SIZE_EXPAND_FILL);
|
|
main_split->add_child(center_split);
|
|
|
|
VBoxContainer *available_templates_container = memnew(VBoxContainer);
|
|
available_templates_container->set_v_size_flags(Control::SIZE_EXPAND_FILL);
|
|
center_split->add_child(available_templates_container);
|
|
|
|
Label *template_header2 = memnew(Label(TTRC("Available Templates")));
|
|
template_header2->set_theme_type_variation("HeaderSmall");
|
|
template_header2->set_h_size_flags(Control::SIZE_EXPAND_FILL);
|
|
available_templates_container->add_child(template_header2);
|
|
|
|
available_templates_tree = memnew(Tree);
|
|
available_templates_tree->set_accessibility_name(TTRC("Available Templates"));
|
|
available_templates_tree->set_hide_root(true);
|
|
available_templates_tree->set_theme_type_variation("TreeSecondary");
|
|
available_templates_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
|
|
available_templates_tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
|
|
available_templates_container->add_child(available_templates_tree);
|
|
available_templates_tree->connect("button_clicked", callable_mp(this, &ExportTemplateManager::_tree_button_clicked));
|
|
available_templates_tree->connect("item_edited", callable_mp(this, &ExportTemplateManager::_tree_item_edited));
|
|
|
|
VBoxContainer *installed_templates_container = memnew(VBoxContainer);
|
|
installed_templates_container->set_v_size_flags(Control::SIZE_EXPAND_FILL);
|
|
center_split->add_child(installed_templates_container);
|
|
|
|
Label *template_header = memnew(Label(TTRC("Installed Templates")));
|
|
template_header->set_theme_type_variation("HeaderSmall");
|
|
installed_templates_container->add_child(template_header);
|
|
|
|
installed_templates_tree = memnew(Tree);
|
|
installed_templates_tree->set_accessibility_name(TTRC("Installed Templates"));
|
|
installed_templates_tree->set_hide_root(true);
|
|
installed_templates_tree->set_theme_type_variation("TreeSecondary");
|
|
installed_templates_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
|
|
installed_templates_tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
|
|
installed_templates_container->add_child(installed_templates_tree);
|
|
installed_templates_tree->connect("button_clicked", callable_mp(this, &ExportTemplateManager::_tree_button_clicked));
|
|
|
|
offline_container = memnew(HBoxContainer);
|
|
offline_container->set_alignment(BoxContainer::ALIGNMENT_CENTER);
|
|
offline_container->hide();
|
|
main_vb->add_child(offline_container);
|
|
|
|
Label *offline_mode_label = memnew(Label(TTRC("Offline mode, some functionality is not available.")));
|
|
offline_container->add_child(offline_mode_label);
|
|
|
|
LinkButton *enable_online_button = memnew(LinkButton);
|
|
enable_online_button->set_text(TTRC("Go Online"));
|
|
offline_container->add_child(enable_online_button);
|
|
enable_online_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_force_online_mode));
|
|
|
|
confirm_delete = memnew(ConfirmationDialog);
|
|
confirm_delete->set_text(TTRC("Remove the selected template files? (Cannot be undone.)\nDepending on your filesystem configuration, the files will either be moved to the system trash or deleted permanently."));
|
|
add_child(confirm_delete);
|
|
confirm_delete->connect(SceneStringName(confirmed), callable_mp(this, &ExportTemplateManager::_delete_confirmed));
|
|
|
|
mirrors_requester = memnew(HTTPRequest);
|
|
mirrors_requester->connect("request_completed", callable_mp(this, &ExportTemplateManager::_mirrors_request_completed));
|
|
add_child(mirrors_requester);
|
|
|
|
const String template_directory = _get_template_folder_path(GODOT_VERSION_FULL_CONFIG);
|
|
for (int i = 0; i < 5; i++) {
|
|
TemplateDownloader *downloader = memnew(TemplateDownloader(template_directory));
|
|
downloader->set_use_threads(true);
|
|
add_child(downloader);
|
|
downloaders.push_back(downloader);
|
|
downloader->connect("download_completed", callable_mp(this, &ExportTemplateManager::_download_request_completed));
|
|
downloader->connect("download_failed", callable_mp(this, &ExportTemplateManager::_download_request_failed));
|
|
}
|
|
}
|
|
|
|
int TemplateDownloader::_find_sequence_backwards(const PackedByteArray &p_source, const PackedByteArray &p_target) const {
|
|
const int64_t source_size = p_source.size();
|
|
const int64_t target_size = p_target.size();
|
|
|
|
if (target_size == 0) {
|
|
return -1;
|
|
}
|
|
if (target_size > source_size) {
|
|
return -1;
|
|
}
|
|
const uint8_t *src_ptr = p_source.ptr();
|
|
const uint8_t *tgt_ptr = p_target.ptr();
|
|
|
|
for (int64_t i = source_size - target_size; i >= 0; i--) {
|
|
if (memcmp(&src_ptr[i], tgt_ptr, target_size) == 0) {
|
|
return (int)i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
String TemplateDownloader::_get_download_error(int p_result, int p_response_code) const {
|
|
switch (p_result) {
|
|
case HTTPRequest::RESULT_CANT_RESOLVE:
|
|
return TTR("Can't resolve the requested address");
|
|
case HTTPRequest::RESULT_BODY_SIZE_LIMIT_EXCEEDED:
|
|
case HTTPRequest::RESULT_CONNECTION_ERROR:
|
|
case HTTPRequest::RESULT_CHUNKED_BODY_SIZE_MISMATCH:
|
|
case HTTPRequest::RESULT_TLS_HANDSHAKE_ERROR:
|
|
case HTTPRequest::RESULT_CANT_CONNECT:
|
|
return TTR("Can't connect to the mirror");
|
|
case HTTPRequest::RESULT_NO_RESPONSE:
|
|
return TTR("No response from the mirror");
|
|
case HTTPRequest::RESULT_REQUEST_FAILED:
|
|
return TTR("Request failed");
|
|
case HTTPRequest::RESULT_REDIRECT_LIMIT_REACHED:
|
|
return TTR("Request ended up in a redirect loop");
|
|
}
|
|
|
|
switch (p_response_code) {
|
|
case HTTPClient::RESPONSE_FORBIDDEN:
|
|
return TTR("Forbidden");
|
|
case HTTPClient::RESPONSE_NOT_FOUND:
|
|
return TTR("Not found");
|
|
default: // Handle only common errors.
|
|
return vformat(TTR("Response code: %d"), p_response_code);
|
|
}
|
|
}
|
|
|
|
void TemplateDownloader::_request_completed(int p_result, int p_response_code, const PackedStringArray &p_headers, const PackedByteArray &p_body) {
|
|
switch (current_step) {
|
|
case Step::WAITING: {
|
|
_download_failed(String()); // Not really possible to happen, so just fail with empty message.
|
|
ERR_FAIL_MSG("Request completed on wrong step.");
|
|
} break;
|
|
|
|
case Step::QUERYING: {
|
|
if (p_result != HTTPRequest::RESULT_SUCCESS || p_response_code != HTTPClient::RESPONSE_OK) {
|
|
_download_failed(_get_download_error(p_result, p_response_code));
|
|
return;
|
|
}
|
|
for (const String &header : p_headers) {
|
|
if (header.to_lower().begins_with("content-length:")) {
|
|
file_size = header.split(":")[1].to_int();
|
|
}
|
|
}
|
|
|
|
current_step = Step::SCANNING;
|
|
// Request the last 64 KB of the file to read the Central Directory.
|
|
const String tail_range = vformat("Range: bytes=%d-%d", MAX(0, file_size - 0x10000), file_size - 1);
|
|
|
|
Error err = request(url, PackedStringArray{ tail_range }, HTTPClient::METHOD_GET);
|
|
if (err != OK) {
|
|
_download_failed(vformat(TTR("Download request failed: %s."), TTR(error_names[err])));
|
|
}
|
|
} break;
|
|
|
|
case Step::SCANNING: {
|
|
if (p_result != HTTPRequest::RESULT_SUCCESS || p_response_code != HTTPClient::RESPONSE_PARTIAL_CONTENT) {
|
|
_download_failed(_get_download_error(p_result, p_response_code));
|
|
return;
|
|
}
|
|
PackedByteArray eocd_sig = { 0x50, 0x4b, 0x05, 0x06 };
|
|
int eocd_pos = _find_sequence_backwards(p_body, eocd_sig);
|
|
if (eocd_pos == -1) {
|
|
_download_failed(TTR("Invalid template archive header."));
|
|
return;
|
|
}
|
|
const uint8_t *tail_data = p_body.ptr();
|
|
|
|
int total_entries = decode_uint16(tail_data + eocd_pos + 10);
|
|
int cd_start_offset = decode_uint32(tail_data + eocd_pos + 16);
|
|
int buffer_start_abs = file_size - p_body.size();
|
|
int current_pos = cd_start_offset - buffer_start_abs;
|
|
const String target_path = "templates/" + filename;
|
|
|
|
for (int i = 0; i < total_entries; i++) {
|
|
if (decode_uint32(tail_data + current_pos) != 0x02014b50) {
|
|
break;
|
|
}
|
|
|
|
int comp_method = decode_uint16(tail_data + current_pos + 10); // 0 = Stored, 8 = Deflated
|
|
int comp_size = decode_uint32(tail_data + current_pos + 20);
|
|
int uncomp_size = decode_uint32(tail_data + current_pos + 24);
|
|
int name_len = decode_uint16(tail_data + current_pos + 28);
|
|
int extra_len = decode_uint16(tail_data + current_pos + 30);
|
|
int comm_len = decode_uint16(tail_data + current_pos + 32);
|
|
int local_offset = decode_uint32(tail_data + current_pos + 42);
|
|
|
|
int full_record_len = 46 + name_len + extra_len + comm_len;
|
|
const PackedByteArray raw_record = p_body.slice(current_pos, current_pos + full_record_len);
|
|
const String file_name = String::utf8((const char *)p_body.slice(current_pos + 46, current_pos + 46 + name_len).ptr(), name_len);
|
|
|
|
if (file_name == target_path) {
|
|
file_info.offset = local_offset;
|
|
file_info.compressed_size = comp_size;
|
|
file_info.uncompressed_size = uncomp_size;
|
|
file_info.raw_record = raw_record;
|
|
file_info.method = comp_method;
|
|
file_info.name = file_name;
|
|
break;
|
|
}
|
|
current_pos += full_record_len;
|
|
}
|
|
|
|
if (file_info.name.is_empty()) {
|
|
_download_failed(TTR("Requested template not found in the archive."));
|
|
return;
|
|
}
|
|
|
|
int start_byte = file_info.offset;
|
|
int end_byte = file_info.offset + file_info.compressed_size + file_info.raw_record.size();
|
|
|
|
current_step = Step::DOWNLOADING;
|
|
const String data_range = vformat("Range: bytes=%d-%d", start_byte, end_byte);
|
|
|
|
Error err = request(url, PackedStringArray{ data_range }, HTTPClient::METHOD_GET);
|
|
if (err != OK) {
|
|
_download_failed(vformat(TTR("Download request failed: %s."), TTR(error_names[err])));
|
|
}
|
|
} break;
|
|
|
|
case Step::DOWNLOADING: {
|
|
if (p_result != HTTPRequest::RESULT_SUCCESS || p_response_code != HTTPClient::RESPONSE_PARTIAL_CONTENT) {
|
|
_download_failed(_get_download_error(p_result, p_response_code));
|
|
return;
|
|
}
|
|
const String mini_zip_path = EditorPaths::get_singleton()->get_temp_dir().path_join(filename + ".zip");
|
|
const uint8_t *fragment = p_body.ptr();
|
|
|
|
int local_name_len = decode_uint16(fragment + 26);
|
|
int local_extra_len = decode_uint16(fragment + 28);
|
|
int full_file_chunk_size = 30 + local_name_len + local_extra_len + file_info.compressed_size;
|
|
|
|
if (p_body.size() < full_file_chunk_size) {
|
|
_download_failed(vformat(TTR("Archive fragment too small. Loaded: %d, required: %d."), p_body.size(), full_file_chunk_size));
|
|
return;
|
|
}
|
|
|
|
const PackedByteArray clean_fragment = p_body.slice(0, full_file_chunk_size);
|
|
|
|
PackedByteArray cd_record = file_info.raw_record.duplicate();
|
|
uint8_t *record_write = cd_record.ptrw();
|
|
encode_uint32(0, record_write + 42); // IMPORTANT: Set the offset to 0, as the file is at the very beginning of the mini-ZIP.
|
|
|
|
// EOCD (End of Central Directory)
|
|
PackedByteArray eocd;
|
|
eocd.resize_initialized(22);
|
|
|
|
uint8_t *eocd_write = eocd.ptrw();
|
|
encode_uint32(0x06054b50, eocd_write); // Signature (4 bytes).
|
|
// Offsets 4-7 remain 0 (disk numbers).
|
|
encode_uint16(1, eocd_write + 8); // Number of entries on this disk (2 bytes).
|
|
encode_uint16(1, eocd_write + 10); // Total number of entries (2 bytes).
|
|
encode_uint32(cd_record.size(), eocd_write + 12); // Central Directory size (4 bytes).
|
|
encode_uint32(clean_fragment.size(), eocd_write + 16); // CD start offset (after file data) (4 bytes).
|
|
|
|
// Write Mini-Zip to a file.
|
|
Ref<FileAccess> f = FileAccess::open(mini_zip_path, FileAccess::WRITE);
|
|
if (f.is_null()) {
|
|
_download_failed(TTR("Failed to open mini-ZIP for writing."));
|
|
return;
|
|
}
|
|
f->store_buffer(clean_fragment);
|
|
f->store_buffer(cd_record);
|
|
f->store_buffer(eocd);
|
|
f.unref();
|
|
|
|
PackedByteArray extracted_data;
|
|
{
|
|
// Read back the mini-ZIP.
|
|
Ref<FileAccess> zip_access;
|
|
zlib_filefunc_def io = zipio_create_io(&zip_access);
|
|
unzFile uzf = unzOpen2(mini_zip_path.utf8().get_data(), &io);
|
|
if (!uzf) {
|
|
_download_failed(TTR("ZIP reader could not open mini-ZIP."));
|
|
return;
|
|
}
|
|
|
|
// IMPORTANT: The path in the ZIP reader must exactly match the one in the CD.
|
|
|
|
int err = UNZ_OK;
|
|
|
|
// Locate and open the file.
|
|
err = godot_unzip_locate_file(uzf, file_info.name, true);
|
|
if (err != UNZ_OK) {
|
|
_download_failed(TTR("File does not exist in zip archive."));
|
|
return;
|
|
}
|
|
|
|
err = unzOpenCurrentFile(uzf);
|
|
if (err != UNZ_OK) {
|
|
_download_failed(TTR("Could not open file within zip archive."));
|
|
return;
|
|
}
|
|
|
|
// Read the file info.
|
|
unz_file_info info;
|
|
err = unzGetCurrentFileInfo(uzf, &info, nullptr, 0, nullptr, 0, nullptr, 0);
|
|
if (err != UNZ_OK) {
|
|
_download_failed(TTR("Unable to read file information from zip archive."));
|
|
return;
|
|
}
|
|
|
|
// Read the file data.
|
|
extracted_data.resize(info.uncompressed_size);
|
|
uint8_t *buffer = extracted_data.ptrw();
|
|
int to_read = extracted_data.size();
|
|
while (to_read > 0) {
|
|
int bytes_read = unzReadCurrentFile(uzf, buffer, to_read);
|
|
if (bytes_read < 0 || (bytes_read == UNZ_EOF && to_read != 0)) {
|
|
_download_failed(TTR("IO/zlib error reading file from zip archive."));
|
|
return;
|
|
}
|
|
buffer += bytes_read;
|
|
to_read -= bytes_read;
|
|
}
|
|
|
|
// Verify the data and return.
|
|
err = unzCloseCurrentFile(uzf);
|
|
if (err != UNZ_OK) {
|
|
_download_failed(TTR("CRC error reading file from zip archive."));
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (extracted_data.is_empty()) {
|
|
_download_failed(TTR("Mini-ZIP data was empty."));
|
|
// The mini-ZIP is not deleted for inspection.
|
|
return;
|
|
} else {
|
|
DirAccess::remove_absolute(mini_zip_path);
|
|
|
|
f = FileAccess::open(target_directory.path_join(filename), FileAccess::WRITE);
|
|
if (f.is_null()) {
|
|
_download_failed(TTR("Failed to template file for writing."));
|
|
return;
|
|
}
|
|
f->store_buffer(extracted_data);
|
|
f.unref();
|
|
|
|
current_step = Step::WAITING;
|
|
emit_signal(SNAME("download_completed"), filename);
|
|
}
|
|
} break;
|
|
}
|
|
}
|
|
|
|
void TemplateDownloader::_download_failed(const String &p_reason) {
|
|
const String failed_file = filename;
|
|
cancel_download();
|
|
emit_signal(SNAME("download_failed"), failed_file, p_reason);
|
|
}
|
|
|
|
void TemplateDownloader::_notification(int p_what) {
|
|
switch (p_what) {
|
|
case NOTIFICATION_POSTINITIALIZE: {
|
|
connect(SNAME("request_completed"), callable_mp(this, &TemplateDownloader::_request_completed), CONNECT_DEFERRED);
|
|
} break;
|
|
}
|
|
}
|
|
|
|
void TemplateDownloader::_bind_methods() {
|
|
ADD_SIGNAL(MethodInfo("download_completed"));
|
|
ADD_SIGNAL(MethodInfo("download_failed"));
|
|
}
|
|
|
|
Error TemplateDownloader::download_template(const String &p_file_name, const String &p_source) {
|
|
url = p_source;
|
|
filename = p_file_name;
|
|
|
|
current_step = Step::QUERYING;
|
|
return request(p_source, PackedStringArray(), HTTPClient::METHOD_HEAD);
|
|
}
|
|
|
|
void TemplateDownloader::cancel_download() {
|
|
cancel_request();
|
|
|
|
current_step = Step::WAITING;
|
|
filename = String();
|
|
url = String();
|
|
file_size = 0;
|
|
file_info = FileInfo();
|
|
}
|
|
|
|
float TemplateDownloader::get_download_progress() const {
|
|
if (current_step == Step::DOWNLOADING) {
|
|
return (float)get_downloaded_bytes() / get_body_size();
|
|
}
|
|
return 0.0f;
|
|
}
|