Merge pull request #75916 from KoBeWi/hot_new_version_in_your_area

Add automatic checking for engine updates
This commit is contained in:
Rémi Verschelde 2024-04-15 18:14:35 +02:00
commit 49dd453ca7
No known key found for this signature in database
GPG key ID: C3336907360768E1
7 changed files with 483 additions and 2 deletions

View file

@ -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

View file

@ -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.
</member>
<member name="network/connection/engine_version_update_mode" type="int" setter="" getter="">
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 -&gt; Godot 5).
</member>
<member name="network/connection/network_mode" type="int" setter="" getter="">
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.
</member>
<member name="network/debug/remote_host" type="String" setter="" getter="">
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]).

View file

@ -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<ConfigFile> 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.

View file

@ -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"], &current_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<String, VersionType> 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<String, VersionType> &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));
}

View file

@ -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

View file

@ -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"

View file

@ -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) {