diff --git a/SConstruct b/SConstruct index 6a99662c489..f6c3b33fc63 100644 --- a/SConstruct +++ b/SConstruct @@ -228,6 +228,7 @@ opts.Add( opts.Add(BoolVariable("use_precise_math_checks", "Math checks use very precise epsilon (debug option)", False)) opts.Add(BoolVariable("scu_build", "Use single compilation unit build", False)) opts.Add("scu_limit", "Max includes per SCU file when using scu_build (determines RAM use)", "0") +opts.Add(BoolVariable("engine_update_check", "Enable engine update checks in the Project Manager", True)) # Thirdparty libraries opts.Add(BoolVariable("builtin_brotli", "Use the built-in Brotli library", True)) @@ -474,6 +475,9 @@ if methods.get_cmdline_bool("fast_unsafe", env_base.dev_build): if env_base["use_precise_math_checks"]: env_base.Append(CPPDEFINES=["PRECISE_MATH_CHECKS"]) +if env_base["engine_update_check"]: + env_base.Append(CPPDEFINES=["ENGINE_UPDATE_CHECK_ENABLED"]) + if not env_base.File("#main/splash_editor.png").exists(): # Force disabling editor splash if missing. env_base["no_editor_splash"] = True diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index 87ca0536b84..7dcee579819 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -866,8 +866,16 @@ Specify the multiplier to apply to the scale for the editor gizmo handles to improve usability on touchscreen devices. [b]Note:[/b] Defaults to [code]1[/code] on non-touchscreen devices. + + Specifies how the engine should check for updates. + - [b]Disable Update Checks[/b] will block the engine from checking updates (see also [member network/connection/network_mode]). + - [b]Check Newest Preview[/b] (default for preview versions) will check for the newest available development snapshot. + - [b]Check Newest Stable[/b] (default for stable versions) will check for the newest available stable version. + - [b]Check Newest Patch[/b] will check for the latest available stable version, but only within the same minor version. E.g. if your version is [code]4.3.stable[/code], you will be notified about [code]4.3.1.stable[/code], but not [code]4.4.stable[/code]. + All update modes will ignore builds with different major versions (e.g. Godot 4 -> Godot 5). + - Determines whether online features are enabled in the editor, such as the Asset Library. Setting this property to "Offline" is recommended to limit editor's internet activity, especially if privacy is a concern. + Determines whether online features are enabled in the editor, such as the Asset Library or update checks. Disabling these online features helps alleviate privacy concerns by preventing the editor from making HTTP requests to the Godot website, GitHub, or third-party platforms hosting assets from the Asset Library. The address to listen to when starting the remote debugger. This can be set to [code]0.0.0.0[/code] to allow external clients to connect to the remote debugger (instead of restricting the remote debugger to connections from [code]localhost[/code]). diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index 32d581a26e9..30587b257ce 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -49,6 +49,7 @@ #include "editor/editor_paths.h" #include "editor/editor_property_name_processor.h" #include "editor/editor_translation.h" +#include "editor/engine_update_label.h" #include "scene/gui/color_picker.h" #include "scene/main/node.h" #include "scene/main/scene_tree.h" @@ -413,6 +414,14 @@ void EditorSettings::_load_defaults(Ref p_extra_config) { EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/editor_screen", -2, ed_screen_hints) EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/project_manager_screen", -2, ed_screen_hints) + { + EngineUpdateLabel::UpdateMode default_update_mode = EngineUpdateLabel::UpdateMode::NEWEST_UNSTABLE; + if (String(VERSION_STATUS) == String("stable")) { + default_update_mode = EngineUpdateLabel::UpdateMode::NEWEST_STABLE; + } + EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "network/connection/engine_version_update_mode", int(default_update_mode), "Disable Update Checks,Check Newest Preview,Check Newest Stable,Check Newest Patch"); // Uses EngineUpdateLabel::UpdateMode. + } + _initial_set("interface/editor/debug/enable_pseudolocalization", false); set_restart_if_changed("interface/editor/debug/enable_pseudolocalization", true); // Use pseudolocalization in editor. diff --git a/editor/engine_update_label.cpp b/editor/engine_update_label.cpp new file mode 100644 index 00000000000..0b20738e994 --- /dev/null +++ b/editor/engine_update_label.cpp @@ -0,0 +1,344 @@ +/**************************************************************************/ +/* engine_update_label.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 "engine_update_label.h" + +#include "core/os/time.h" +#include "editor/editor_settings.h" +#include "editor/editor_string_names.h" +#include "editor/themes/editor_scale.h" +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/main/http_request.h" + +bool EngineUpdateLabel::_can_check_updates() const { + return int(EDITOR_GET("network/connection/network_mode")) == EditorSettings::NETWORK_ONLINE && + UpdateMode(int(EDITOR_GET("network/connection/engine_version_update_mode"))) != UpdateMode::DISABLED; +} + +void EngineUpdateLabel::_check_update() { + checked_update = true; + _set_status(UpdateStatus::BUSY); + http->request("https://raw.githubusercontent.com/godotengine/godot-website/master/_data/versions.yml"); +} + +void EngineUpdateLabel::_http_request_completed(int p_result, int p_response_code, const PackedStringArray &p_headers, const PackedByteArray &p_body) { + if (p_result != OK) { + _set_status(UpdateStatus::ERROR); + _set_message(vformat(TTR("Failed to check for updates. Error: %d."), p_result), theme_cache.error_color); + return; + } + + if (p_response_code != 200) { + _set_status(UpdateStatus::ERROR); + _set_message(vformat(TTR("Failed to check for updates. Response code: %d."), p_response_code), theme_cache.error_color); + return; + } + + PackedStringArray lines; + { + String s; + const uint8_t *r = p_body.ptr(); + s.parse_utf8((const char *)r, p_body.size()); + lines = s.split("\n"); + } + + UpdateMode update_mode = UpdateMode(int(EDITOR_GET("network/connection/engine_version_update_mode"))); + bool stable_only = update_mode == UpdateMode::NEWEST_STABLE || update_mode == UpdateMode::NEWEST_PATCH; + + const Dictionary version_info = Engine::get_singleton()->get_version_info(); + int current_major = version_info["major"]; + int current_minor = version_info["minor"]; + int current_patch = version_info["patch"]; + + int current_version_line = -1; + for (int i = 0; i < lines.size(); i++) { + const String &line = lines[i]; + if (!line.begins_with("- name")) { + continue; + } + + const String version_string = _extract_sub_string(line); + const PackedStringArray version_bits = version_string.split("."); + + if (version_bits.size() < 2) { + continue; + } + + int minor = version_bits[1].to_int(); + if (version_bits[0].to_int() != current_major || minor < current_minor) { + continue; + } + + int patch = 0; + if (version_bits.size() >= 3) { + patch = version_bits[2].to_int(); + } + + if (minor == current_minor && patch < current_patch) { + continue; + } + + if (update_mode == UpdateMode::NEWEST_PATCH && minor > current_minor) { + continue; + } + + if (minor > current_minor || patch > current_patch) { + String version_type = _extract_sub_string(lines[i + 1]); + if (stable_only && _get_version_type(version_type, nullptr) != VersionType::STABLE) { + continue; + } + + found_version = version_string; + found_version += "-" + version_type; + break; + } else if (minor == current_minor && patch == current_patch) { + current_version_line = i; + found_version = version_string; + break; + } + } + + if (current_version_line == -1 && !found_version.is_empty()) { + _set_status(UpdateStatus::UPDATE_AVAILABLE); + _set_message(vformat(TTR("Update available: %s."), found_version), theme_cache.update_color); + return; + } else if (current_version_line == -1 || stable_only) { + _set_status(UpdateStatus::UP_TO_DATE); + return; + } + + int current_version_index; + VersionType current_version_type = _get_version_type(version_info["status"], ¤t_version_index); + + for (int i = current_version_line + 1; i < lines.size(); i++) { + const String &line = lines[i]; + if (line.begins_with("- name")) { + break; + } + + if (!line.begins_with(" - name") && !line.begins_with(" flavor")) { + continue; + } + + const String version_string = _extract_sub_string(line); + int version_index; + VersionType version_type = _get_version_type(version_string, &version_index); + + if (int(version_type) < int(current_version_type) || version_index > current_version_index) { + found_version += "-" + version_string; + + _set_status(UpdateStatus::UPDATE_AVAILABLE); + _set_message(vformat(TTR("Update available: %s."), found_version), theme_cache.update_color); + return; + } + } + + if (current_version_index == DEV_VERSION) { + // Since version index can't be determined and no strictly newer version exists, display a different status. + _set_status(UpdateStatus::DEV); + } else { + _set_status(UpdateStatus::UP_TO_DATE); + } +} + +void EngineUpdateLabel::_set_message(const String &p_message, const Color &p_color) { + if (is_disabled()) { + add_theme_color_override("font_disabled_color", p_color); + } else { + add_theme_color_override("font_color", p_color); + } + set_text(p_message); +} + +void EngineUpdateLabel::_set_status(UpdateStatus p_status) { + status = p_status; + if (compact_mode) { + if (status != UpdateStatus::BUSY && status != UpdateStatus::UPDATE_AVAILABLE) { + hide(); + return; + } else { + show(); + } + } + + switch (status) { + case UpdateStatus::DEV: { + set_disabled(true); + _set_message(TTR("Running a development build."), theme_cache.disabled_color); + set_tooltip_text(TTR("Exact version can't be determined for update checking.")); + break; + } + case UpdateStatus::OFFLINE: { + set_disabled(false); + if (int(EDITOR_GET("network/connection/network_mode")) == EditorSettings::NETWORK_OFFLINE) { + _set_message(TTR("Offline mode, update checks disabled."), theme_cache.disabled_color); + } else { + _set_message(TTR("Update checks disabled."), theme_cache.disabled_color); + } + set_tooltip_text(""); + break; + } + case UpdateStatus::BUSY: { + set_disabled(true); + _set_message(TTR("Checking for updates..."), theme_cache.default_color); + set_tooltip_text(""); + } break; + + case UpdateStatus::ERROR: { + set_disabled(false); + set_tooltip_text(TTR("An error has occurred. Click to try again.")); + } break; + + case UpdateStatus::UP_TO_DATE: { + set_disabled(false); + _set_message(TTR("Current version up to date."), theme_cache.disabled_color); + set_tooltip_text(TTR("Click to check again.")); + } break; + + case UpdateStatus::UPDATE_AVAILABLE: { + set_disabled(false); + set_tooltip_text(TTR("Click to open download page.")); + } break; + + default: { + } + } +} + +EngineUpdateLabel::VersionType EngineUpdateLabel::_get_version_type(const String &p_string, int *r_index) const { + VersionType type = VersionType::UNKNOWN; + String index_string; + + static HashMap type_map; + if (type_map.is_empty()) { + type_map["stable"] = VersionType::STABLE; + type_map["rc"] = VersionType::RC; + type_map["beta"] = VersionType::BETA; + type_map["alpha"] = VersionType::ALPHA; + type_map["dev"] = VersionType::DEV; + } + + for (const KeyValue &kv : type_map) { + if (p_string.begins_with(kv.key)) { + index_string = p_string.trim_prefix(kv.key); + type = kv.value; + break; + } + } + + if (r_index) { + if (index_string.is_empty()) { + *r_index = DEV_VERSION; + } else { + *r_index = index_string.to_int(); + } + } + return type; +} + +String EngineUpdateLabel::_extract_sub_string(const String &p_line) const { + int j = p_line.find("\"") + 1; + return p_line.substr(j, p_line.find("\"", j) - j); +} + +void EngineUpdateLabel::_notification(int p_what) { + switch (p_what) { + case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { + if (!EditorSettings::get_singleton()->check_changed_settings_in_group("network/connection")) { + break; + } + + if (_can_check_updates()) { + if (!checked_update) { + _check_update(); + } else { + // This will be wrong when user toggles online mode twice when update is available, but it's not worth handling. + _set_status(UpdateStatus::UP_TO_DATE); + } + } else { + _set_status(UpdateStatus::OFFLINE); + } + } break; + + case NOTIFICATION_THEME_CHANGED: { + theme_cache.default_color = get_theme_color("font_color", "Button"); + theme_cache.disabled_color = get_theme_color("font_disabled_color", "Button"); + theme_cache.error_color = get_theme_color("error_color", EditorStringName(Editor)); + theme_cache.update_color = get_theme_color("warning_color", EditorStringName(Editor)); + } break; + + case NOTIFICATION_READY: { + if (_can_check_updates()) { + _check_update(); + } else { + _set_status(UpdateStatus::OFFLINE); + } + } break; + } +} + +void EngineUpdateLabel::_bind_methods() { + ADD_SIGNAL(MethodInfo("offline_clicked")); +} + +void EngineUpdateLabel::pressed() { + switch (status) { + case UpdateStatus::OFFLINE: { + emit_signal("offline_clicked"); + } break; + + case UpdateStatus::ERROR: + case UpdateStatus::UP_TO_DATE: { + _check_update(); + } break; + + case UpdateStatus::UPDATE_AVAILABLE: { + OS::get_singleton()->shell_open("https://godotengine.org/download/archive/" + found_version); + } break; + + default: { + } + } +} + +void EngineUpdateLabel::enable_compact_mode() { + compact_mode = true; +} + +EngineUpdateLabel::EngineUpdateLabel() { + set_underline_mode(UNDERLINE_MODE_ON_HOVER); + + http = memnew(HTTPRequest); + http->set_https_proxy(EDITOR_GET("network/http_proxy/host"), EDITOR_GET("network/http_proxy/port")); + http->set_timeout(10.0); + add_child(http); + http->connect("request_completed", callable_mp(this, &EngineUpdateLabel::_http_request_completed)); +} diff --git a/editor/engine_update_label.h b/editor/engine_update_label.h new file mode 100644 index 00000000000..d00fe53e1ee --- /dev/null +++ b/editor/engine_update_label.h @@ -0,0 +1,107 @@ +/**************************************************************************/ +/* engine_update_label.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef ENGINE_UPDATE_LABEL_H +#define ENGINE_UPDATE_LABEL_H + +#include "scene/gui/link_button.h" + +class HTTPRequest; + +class EngineUpdateLabel : public LinkButton { + GDCLASS(EngineUpdateLabel, LinkButton); + +public: + enum class UpdateMode { + DISABLED, + NEWEST_UNSTABLE, + NEWEST_STABLE, + NEWEST_PATCH, + }; + +private: + static constexpr int DEV_VERSION = 9999; // Version index for unnumbered builds (assumed to always be newest). + + enum class VersionType { + STABLE, + RC, + BETA, + ALPHA, + DEV, + UNKNOWN, + }; + + enum class UpdateStatus { + NONE, + DEV, + OFFLINE, + BUSY, + ERROR, + UPDATE_AVAILABLE, + UP_TO_DATE, + }; + + struct ThemeCache { + Color default_color; + Color disabled_color; + Color error_color; + Color update_color; + } theme_cache; + + HTTPRequest *http = nullptr; + bool compact_mode = false; + + UpdateStatus status = UpdateStatus::NONE; + bool checked_update = false; + String found_version; + + bool _can_check_updates() const; + void _check_update(); + void _http_request_completed(int p_result, int p_response_code, const PackedStringArray &p_headers, const PackedByteArray &p_body); + + void _set_message(const String &p_message, const Color &p_color); + void _set_status(UpdateStatus p_status); + + VersionType _get_version_type(const String &p_string, int *r_index) const; + String _extract_sub_string(const String &p_line) const; + +protected: + void _notification(int p_what); + static void _bind_methods(); + + virtual void pressed() override; + +public: + void enable_compact_mode(); + + EngineUpdateLabel(); +}; + +#endif // ENGINE_UPDATE_LABEL_H diff --git a/editor/gui/editor_bottom_panel.cpp b/editor/gui/editor_bottom_panel.cpp index 1c95b546f47..b575910976e 100644 --- a/editor/gui/editor_bottom_panel.cpp +++ b/editor/gui/editor_bottom_panel.cpp @@ -37,6 +37,7 @@ #include "editor/editor_command_palette.h" #include "editor/editor_node.h" #include "editor/editor_string_names.h" +#include "editor/engine_update_label.h" #include "editor/gui/editor_toaster.h" #include "editor/themes/editor_scale.h" #include "scene/gui/box_container.h" diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 86c59ec99eb..a16767d9167 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -43,6 +43,7 @@ #include "editor/editor_about.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" +#include "editor/engine_update_label.h" #include "editor/gui/editor_file_dialog.h" #include "editor/gui/editor_title_bar.h" #include "editor/plugins/asset_library_editor_plugin.h" @@ -526,7 +527,7 @@ void ProjectManager::_open_selected_projects_ask() { return; } - const Size2i popup_min_size = Size2i(600.0 * EDSCALE, 0); + const Size2i popup_min_size = Size2i(400.0 * EDSCALE, 0); if (selected_list.size() > 1) { multi_open_ask->set_text(vformat(TTR("You requested to open %d projects in parallel. Do you confirm?\nNote that usual checks for engine version compatibility will be bypassed."), selected_list.size())); @@ -1397,8 +1398,15 @@ ProjectManager::ProjectManager() { { HBoxContainer *footer_bar = memnew(HBoxContainer); footer_bar->set_alignment(BoxContainer::ALIGNMENT_END); + footer_bar->add_theme_constant_override("separation", 20 * EDSCALE); main_vbox->add_child(footer_bar); +#ifdef ENGINE_UPDATE_CHECK_ENABLED + EngineUpdateLabel *update_label = memnew(EngineUpdateLabel); + footer_bar->add_child(update_label); + update_label->connect("offline_clicked", callable_mp(this, &ProjectManager::_show_quick_settings)); +#endif + version_btn = memnew(LinkButton); String hash = String(VERSION_HASH); if (hash.length() != 0) {