godot/editor/export/export_template_manager.cpp
kobewi 2f079a1360 Extract individual templates from remote TPZ
Co-authored-by: Alexander Hartmann <alex.hart.278@gmail.com>
2026-04-16 13:17:59 +02:00

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;
}