From 23ecaabc09ff6726badd7a85cf340ea76e97f72e Mon Sep 17 00:00:00 2001 From: Ricardo Subtil Date: Tue, 30 Apr 2024 21:13:10 +0100 Subject: [PATCH] Implement a "Safe Mode" for recovering crashing/hanging projects during initialization --- core/config/engine.h | 7 ++ core/extension/gdextension_manager.cpp | 27 +++++ core/os/os.cpp | 23 ++++- core/os/os.h | 4 + drivers/unix/os_unix.cpp | 9 +- drivers/unix/os_unix.h | 2 +- editor/debugger/editor_debugger_node.cpp | 8 ++ editor/editor_node.cpp | 14 +++ editor/editor_node.h | 29 +++--- editor/gui/editor_run_bar.cpp | 89 +++++++++++++++++ editor/gui/editor_run_bar.h | 9 ++ editor/gui/scene_tree_editor.cpp | 9 +- .../plugins/asset_library_editor_plugin.cpp | 2 +- editor/plugins/editor_plugin_settings.cpp | 24 +++++ editor/plugins/editor_plugin_settings.h | 1 + editor/project_manager.cpp | 99 +++++++++++++++++-- editor/project_manager.h | 15 ++- editor/project_manager/project_list.cpp | 12 ++- editor/project_manager/project_list.h | 3 + editor/register_editor_types.cpp | 4 +- editor/themes/editor_theme_manager.cpp | 15 +++ main/main.cpp | 23 ++++- modules/gdscript/gdscript.cpp | 2 +- modules/mono/csharp_script.cpp | 2 +- platform/android/java_godot_io_wrapper.cpp | 2 +- platform/android/java_godot_io_wrapper.h | 2 +- platform/android/os_android.cpp | 10 +- platform/android/os_android.h | 2 +- platform/ios/os_ios.h | 2 +- platform/ios/os_ios.mm | 2 +- platform/web/os_web.cpp | 9 +- platform/web/os_web.h | 2 +- platform/windows/os_windows.cpp | 9 +- platform/windows/os_windows.h | 2 +- 34 files changed, 414 insertions(+), 60 deletions(-) diff --git a/core/config/engine.h b/core/config/engine.h index b38412308ae7..82af653c94e8 100644 --- a/core/config/engine.h +++ b/core/config/engine.h @@ -87,6 +87,7 @@ class Engine { bool editor_hint = false; bool project_manager_hint = false; bool extension_reloading = false; + bool safe_mode_hint = false; bool _print_header = true; @@ -162,6 +163,9 @@ class Engine { _FORCE_INLINE_ void set_extension_reloading_enabled(bool p_enabled) { extension_reloading = p_enabled; } _FORCE_INLINE_ bool is_extension_reloading_enabled() const { return extension_reloading; } + + _FORCE_INLINE_ void set_safe_mode_hint(bool p_enabled) { safe_mode_hint = p_enabled; } + _FORCE_INLINE_ bool is_safe_mode_hint() const { return safe_mode_hint; } #else _FORCE_INLINE_ void set_editor_hint(bool p_enabled) {} _FORCE_INLINE_ bool is_editor_hint() const { return false; } @@ -171,6 +175,9 @@ class Engine { _FORCE_INLINE_ void set_extension_reloading_enabled(bool p_enabled) {} _FORCE_INLINE_ bool is_extension_reloading_enabled() const { return false; } + + _FORCE_INLINE_ void set_safe_mode_hint(bool p_enabled) {} + _FORCE_INLINE_ bool is_safe_mode_hint() const { return false; } #endif Dictionary get_version_info() const; diff --git a/core/extension/gdextension_manager.cpp b/core/extension/gdextension_manager.cpp index 0dc13cf3759a..4013c34a4e96 100644 --- a/core/extension/gdextension_manager.cpp +++ b/core/extension/gdextension_manager.cpp @@ -84,6 +84,10 @@ GDExtensionManager::LoadStatus GDExtensionManager::_unload_extension_internal(co } GDExtensionManager::LoadStatus GDExtensionManager::load_extension(const String &p_path) { + if (Engine::get_singleton()->is_safe_mode_hint()) { + return LOAD_STATUS_OK; + } + Ref loader; loader.instantiate(); return GDExtensionManager::get_singleton()->load_extension_with_loader(p_path, loader); @@ -119,6 +123,10 @@ GDExtensionManager::LoadStatus GDExtensionManager::reload_extension(const String #else ERR_FAIL_COND_V_MSG(!Engine::get_singleton()->is_extension_reloading_enabled(), LOAD_STATUS_FAILED, "GDExtension reloading is disabled."); + if (Engine::get_singleton()->is_safe_mode_hint()) { + return LOAD_STATUS_OK; + } + if (!gdextension_map.has(p_path)) { return LOAD_STATUS_NOT_LOADED; } @@ -161,6 +169,10 @@ GDExtensionManager::LoadStatus GDExtensionManager::reload_extension(const String } GDExtensionManager::LoadStatus GDExtensionManager::unload_extension(const String &p_path) { + if (Engine::get_singleton()->is_safe_mode_hint()) { + return LOAD_STATUS_OK; + } + if (!gdextension_map.has(p_path)) { return LOAD_STATUS_NOT_LOADED; } @@ -207,6 +219,10 @@ String GDExtensionManager::class_get_icon_path(const String &p_class) const { } void GDExtensionManager::initialize_extensions(GDExtension::InitializationLevel p_level) { + if (Engine::get_singleton()->is_safe_mode_hint()) { + return; + } + ERR_FAIL_COND(int32_t(p_level) - 1 != level); for (KeyValue> &E : gdextension_map) { E.value->initialize_library(p_level); @@ -215,6 +231,10 @@ void GDExtensionManager::initialize_extensions(GDExtension::InitializationLevel } void GDExtensionManager::deinitialize_extensions(GDExtension::InitializationLevel p_level) { + if (Engine::get_singleton()->is_safe_mode_hint()) { + return; + } + ERR_FAIL_COND(int32_t(p_level) != level); for (KeyValue> &E : gdextension_map) { E.value->deinitialize_library(p_level); @@ -253,6 +273,10 @@ void GDExtensionManager::_reload_all_scripts() { #endif // TOOLS_ENABLED void GDExtensionManager::load_extensions() { + if (Engine::get_singleton()->is_safe_mode_hint()) { + return; + } + Ref f = FileAccess::open(GDExtension::get_extension_list_config_file(), FileAccess::READ); while (f.is_valid() && !f->eof_reached()) { String s = f->get_line().strip_edges(); @@ -267,6 +291,9 @@ void GDExtensionManager::load_extensions() { void GDExtensionManager::reload_extensions() { #ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_safe_mode_hint()) { + return; + } bool reloaded = false; for (const KeyValue> &E : gdextension_map) { if (!E.value->is_reloadable()) { diff --git a/core/os/os.cpp b/core/os/os.cpp index 59a0579ce36b..4757c374a401 100644 --- a/core/os/os.cpp +++ b/core/os/os.cpp @@ -287,10 +287,14 @@ String OS::get_bundle_icon_path() const { } // OS specific path for user:// -String OS::get_user_data_dir() const { +String OS::get_user_data_dir(const String &p_appname) const { return "."; } +String OS::get_user_data_dir() const { + return get_user_data_dir(get_safe_dir_name(GLOBAL_GET("application/config/name"))); +} + // Absolute path to res:// String OS::get_resource_dir() const { return ProjectSettings::get_singleton()->get_resource_path(); @@ -301,6 +305,23 @@ String OS::get_system_dir(SystemDir p_dir, bool p_shared_storage) const { return "."; } +void OS::create_lock_file() { + if (Engine::get_singleton()->is_safe_mode_hint()) { + return; + } + + String lock_file_path = get_user_data_dir().path_join(".safe_mode_lock"); + Ref lock_file = FileAccess::open(lock_file_path, FileAccess::WRITE); + if (lock_file.is_valid()) { + lock_file->close(); + } +} + +void OS::remove_lock_file() { + String lock_file_path = get_user_data_dir().path_join(".safe_mode_lock"); + DirAccess::remove_absolute(lock_file_path); +} + Error OS::shell_open(const String &p_uri) { return ERR_UNAVAILABLE; } diff --git a/core/os/os.h b/core/os/os.h index ffdb905abaf7..f49801ab96ea 100644 --- a/core/os/os.h +++ b/core/os/os.h @@ -290,6 +290,7 @@ class OS { virtual String get_bundle_resource_dir() const; virtual String get_bundle_icon_path() const; + virtual String get_user_data_dir(const String &p_appname) const; virtual String get_user_data_dir() const; virtual String get_resource_dir() const; @@ -308,6 +309,9 @@ class OS { virtual Error move_to_trash(const String &p_path) { return FAILED; } + void create_lock_file(); + void remove_lock_file(); + virtual int get_exit_code() const; // `set_exit_code` should only be used from `SceneTree` (or from a similar // level, e.g. from the `Main::start` if leaving without creating a `SceneTree`). diff --git a/drivers/unix/os_unix.cpp b/drivers/unix/os_unix.cpp index 92855d1a4395..d74bab9fa60b 100644 --- a/drivers/unix/os_unix.cpp +++ b/drivers/unix/os_unix.cpp @@ -951,18 +951,17 @@ void OS_Unix::unset_environment(const String &p_var) const { unsetenv(p_var.utf8().get_data()); } -String OS_Unix::get_user_data_dir() const { - String appname = get_safe_dir_name(GLOBAL_GET("application/config/name")); - if (!appname.is_empty()) { +String OS_Unix::get_user_data_dir(const String &p_appname) const { + if (!p_appname.is_empty()) { bool use_custom_dir = GLOBAL_GET("application/config/use_custom_user_dir"); if (use_custom_dir) { String custom_dir = get_safe_dir_name(GLOBAL_GET("application/config/custom_user_dir_name"), true); if (custom_dir.is_empty()) { - custom_dir = appname; + custom_dir = p_appname; } return get_data_path().path_join(custom_dir); } else { - return get_data_path().path_join(get_godot_dir_name()).path_join("app_userdata").path_join(appname); + return get_data_path().path_join(get_godot_dir_name()).path_join("app_userdata").path_join(p_appname); } } diff --git a/drivers/unix/os_unix.h b/drivers/unix/os_unix.h index 2c7920c14262..2ba11b46f3a6 100644 --- a/drivers/unix/os_unix.h +++ b/drivers/unix/os_unix.h @@ -104,7 +104,7 @@ class OS_Unix : public OS { virtual void initialize_debugging() override; virtual String get_executable_path() const override; - virtual String get_user_data_dir() const override; + virtual String get_user_data_dir(const String &p_appname) const override; }; class UnixTerminalLogger : public StdLogger { diff --git a/editor/debugger/editor_debugger_node.cpp b/editor/debugger/editor_debugger_node.cpp index 0f948b4ed591..82c46b476479 100644 --- a/editor/debugger/editor_debugger_node.cpp +++ b/editor/debugger/editor_debugger_node.cpp @@ -91,6 +91,10 @@ EditorDebuggerNode::EditorDebuggerNode() { remote_scene_tree_timeout = EDITOR_GET("debugger/remote_scene_tree_refresh_interval"); inspect_edited_object_timeout = EDITOR_GET("debugger/remote_inspect_refresh_interval"); + if (Engine::get_singleton()->is_safe_mode_hint()) { + return; + } + EditorRunBar::get_singleton()->get_pause_button()->connect(SceneStringName(pressed), callable_mp(this, &EditorDebuggerNode::_paused)); } @@ -263,6 +267,10 @@ void EditorDebuggerNode::set_keep_open(bool p_keep_open) { } Error EditorDebuggerNode::start(const String &p_uri) { + if (Engine::get_singleton()->is_safe_mode_hint()) { + return OK; + } + ERR_FAIL_COND_V(!p_uri.contains("://"), ERR_INVALID_PARAMETER); if (keep_open && current_uri == p_uri && server.is_valid()) { return OK; diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index f8e23ecc9d81..2b1d513a7347 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -734,6 +734,10 @@ void EditorNode::_notification(int p_what) { CanvasItemEditor::ThemePreviewMode theme_preview_mode = (CanvasItemEditor::ThemePreviewMode)(int)EditorSettings::get_singleton()->get_project_metadata("2d_editor", "theme_preview", CanvasItemEditor::THEME_PREVIEW_PROJECT); update_preview_themes(theme_preview_mode); + if (Engine::get_singleton()->is_safe_mode_hint()) { + EditorToaster::get_singleton()->popup_str(TTR("Safe Mode is enabled. Editor functionality has been restricted."), EditorToaster::SEVERITY_WARNING); + } + /* DO NOT LOAD SCENES HERE, WAIT FOR FILE SCANNING AND REIMPORT TO COMPLETE */ } break; @@ -1154,9 +1158,15 @@ void EditorNode::_sources_changed(bool p_exist) { if (!singleton->cmdline_export_mode) { EditorResourcePreview::get_singleton()->start(); } + + get_tree()->create_timer(1.0f)->connect("timeout", callable_mp(this, &EditorNode::_remove_lock_file)); } } +void EditorNode::_remove_lock_file() { + OS::get_singleton()->remove_lock_file(); +} + void EditorNode::_scan_external_changes() { disk_changed_list->clear(); TreeItem *r = disk_changed_list->create_item(); @@ -5352,6 +5362,10 @@ void EditorNode::_save_window_settings_to_config(Ref p_layout, const } void EditorNode::_load_open_scenes_from_config(Ref p_layout) { + if (Engine::get_singleton()->is_safe_mode_hint()) { + return; + } + if (!bool(EDITOR_GET("interface/scene_tabs/restore_scenes_on_load"))) { return; } diff --git a/editor/editor_node.h b/editor/editor_node.h index 49c1699c2828..a64490cdd45d 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -132,20 +132,6 @@ class EditorNode : public Node { ACTION_ON_STOP_CLOSE_BUTTOM_PANEL, }; - struct ExecuteThreadArgs { - String path; - List args; - String output; - Thread execute_output_thread; - Mutex execute_output_mutex; - int exitcode = 0; - SafeFlag done; - }; - -private: - friend class EditorSceneTabs; - friend class SurfaceUpgradeTool; - enum MenuOptions { FILE_NEW_SCENE, FILE_NEW_INHERITED_SCENE, @@ -235,6 +221,20 @@ class EditorNode : public Node { TOOL_MENU_BASE = 1000 }; + struct ExecuteThreadArgs { + String path; + List args; + String output; + Thread execute_output_thread; + Mutex execute_output_mutex; + int exitcode = 0; + SafeFlag done; + }; + +private: + friend class EditorSceneTabs; + friend class SurfaceUpgradeTool; + enum { MAX_INIT_CALLBACKS = 128, MAX_BUILD_CALLBACKS = 128 @@ -543,6 +543,7 @@ class EditorNode : public Node { void _resources_reimporting(const Vector &p_resources); void _resources_reimported(const Vector &p_resources); void _sources_changed(bool p_exist); + void _remove_lock_file(); void _node_renamed(); void _save_editor_states(const String &p_file, int p_idx = -1); diff --git a/editor/gui/editor_run_bar.cpp b/editor/gui/editor_run_bar.cpp index 64135c8d50ed..ae4f1ac561d7 100644 --- a/editor/gui/editor_run_bar.cpp +++ b/editor/gui/editor_run_bar.cpp @@ -38,6 +38,7 @@ #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/gui/editor_quick_open_dialog.h" +#include "editor/gui/editor_toaster.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/panel_container.h" @@ -50,7 +51,35 @@ void EditorRunBar::_notification(int p_what) { _reset_play_buttons(); } break; + case NOTIFICATION_READY: { + if (Engine::get_singleton()->is_safe_mode_hint()) { + safe_mode_show_dialog(); + } + } break; + case NOTIFICATION_THEME_CHANGED: { + if (Engine::get_singleton()->is_safe_mode_hint()) { + main_panel->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("LaunchPadSafeMode"), EditorStringName(EditorStyles))); + safe_mode_panel->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("SafeModeButton"), EditorStringName(EditorStyles))); + safe_mode_button->add_theme_style_override("hover", get_theme_stylebox(SNAME("SafeModeButton"), EditorStringName(EditorStyles))); + + safe_mode_button->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning"))); + safe_mode_reload_button->set_button_icon(get_editor_theme_icon(SNAME("Reload"))); + + safe_mode_button->begin_bulk_theme_override(); + safe_mode_button->add_theme_color_override("icon_normal_color", Color(0.3, 0.3, 0.3, 1)); + safe_mode_button->add_theme_color_override("icon_pressed_color", Color(0.4, 0.4, 0.4, 1)); + safe_mode_button->add_theme_color_override("icon_hover_color", Color(0.6, 0.6, 0.6, 1)); + Color dark_color = get_theme_color("safe_mode_text_color", EditorStringName(Editor)); + safe_mode_button->add_theme_color_override(SceneStringName(font_color), dark_color); + safe_mode_button->add_theme_color_override("font_pressed_color", dark_color.lightened(0.2)); + safe_mode_button->add_theme_color_override("font_hover_color", dark_color.lightened(0.4)); + safe_mode_button->add_theme_color_override("font_hover_pressed_color", dark_color.lightened(0.2)); + safe_mode_button->end_bulk_theme_override(); + + return; + } + _update_play_buttons(); pause_button->set_button_icon(get_editor_theme_icon(SNAME("Pause"))); stop_button->set_button_icon(get_editor_theme_icon(SNAME("Stop"))); @@ -76,6 +105,10 @@ void EditorRunBar::_notification(int p_what) { } void EditorRunBar::_reset_play_buttons() { + if (Engine::get_singleton()->is_safe_mode_hint()) { + return; + } + play_button->set_pressed(false); play_button->set_button_icon(get_editor_theme_icon(SNAME("MainPlay"))); play_button->set_tooltip_text(TTR("Play the project.")); @@ -90,6 +123,10 @@ void EditorRunBar::_reset_play_buttons() { } void EditorRunBar::_update_play_buttons() { + if (Engine::get_singleton()->is_safe_mode_hint()) { + return; + } + _reset_play_buttons(); if (!is_playing()) { return; @@ -261,7 +298,20 @@ void EditorRunBar::_run_native(const Ref &p_preset) { } } +void EditorRunBar::safe_mode_show_dialog() { + safe_mode_popup->popup_centered(); +} + +void EditorRunBar::safe_mode_reload_project() { + EditorNode::get_singleton()->trigger_menu_option(EditorNode::RELOAD_CURRENT_PROJECT, false); +} + void EditorRunBar::play_main_scene(bool p_from_native) { + if (Engine::get_singleton()->is_safe_mode_hint()) { + EditorToaster::get_singleton()->popup_str(TTR("Safe Mode is enabled. Disable it to run the project."), EditorToaster::SEVERITY_WARNING); + return; + } + if (p_from_native) { run_native->resume_run_native(); } else { @@ -273,6 +323,11 @@ void EditorRunBar::play_main_scene(bool p_from_native) { } void EditorRunBar::play_current_scene(bool p_reload) { + if (Engine::get_singleton()->is_safe_mode_hint()) { + EditorToaster::get_singleton()->popup_str(TTR("Safe Mode is enabled. Disable it to run the project."), EditorToaster::SEVERITY_WARNING); + return; + } + String last_current_scene = run_current_filename; // This is necessary to have a copy of the string. EditorNode::get_singleton()->save_default_environment(); @@ -287,6 +342,11 @@ void EditorRunBar::play_current_scene(bool p_reload) { } void EditorRunBar::play_custom_scene(const String &p_custom) { + if (Engine::get_singleton()->is_safe_mode_hint()) { + EditorToaster::get_singleton()->popup_str(TTR("Safe Mode is enabled. Disable it to run the project."), EditorToaster::SEVERITY_WARNING); + return; + } + stop_playing(); current_mode = RunMode::RUN_CUSTOM; @@ -370,6 +430,35 @@ EditorRunBar::EditorRunBar() { main_hbox = memnew(HBoxContainer); main_panel->add_child(main_hbox); + if (Engine::get_singleton()->is_safe_mode_hint()) { + safe_mode_popup = memnew(AcceptDialog); + safe_mode_popup->set_min_size(Size2(550, 70)); + safe_mode_popup->set_title(TTR("Safe Mode")); + safe_mode_popup->set_text(TTR("Godot opened the project in Safe Mode, which is a special mode that can help recover projects that crash the engine upon initialization. The following features have been temporarily disabled:\n\n- Tool scripts\n- Editor plugins\n- GDExtension addons\n- Automatic scene restoring\n\nIf the project cannot be opened outside of this mode, then it's very likely any of these components is preventing this project from launching. This mode is intended only for basic editing to troubleshoot such issues, and therefore it is not possible to run a project in this mode.\n\nTo disable Safe Mode, reload the project by pressing the Reload button next to the Safe Mode banner, or by reopening the project normally.")); + safe_mode_popup->set_autowrap(true); + add_child(safe_mode_popup); + + safe_mode_reload_button = memnew(Button); + main_hbox->add_child(safe_mode_reload_button); + safe_mode_reload_button->set_theme_type_variation("RunBarButton"); + safe_mode_reload_button->set_focus_mode(Control::FOCUS_NONE); + safe_mode_reload_button->set_tooltip_text(TTR("Disable safe mode and reload the project.")); + safe_mode_reload_button->connect(SceneStringName(pressed), callable_mp(this, &EditorRunBar::safe_mode_reload_project)); + + safe_mode_panel = memnew(PanelContainer); + main_hbox->add_child(safe_mode_panel); + + safe_mode_button = memnew(Button); + safe_mode_panel->add_child(safe_mode_button); + safe_mode_button->set_theme_type_variation("RunBarButton"); + safe_mode_button->set_focus_mode(Control::FOCUS_NONE); + safe_mode_button->set_text(TTR("Safe Mode")); + safe_mode_button->set_tooltip_text(TTR("Safe Mode is enabled. Click for more details.")); + safe_mode_button->connect(SceneStringName(pressed), callable_mp(this, &EditorRunBar::safe_mode_show_dialog)); + + return; + } + play_button = memnew(Button); main_hbox->add_child(play_button); play_button->set_theme_type_variation("RunBarButton"); diff --git a/editor/gui/editor_run_bar.h b/editor/gui/editor_run_bar.h index d8238aa2c541..6c90fe194feb 100644 --- a/editor/gui/editor_run_bar.h +++ b/editor/gui/editor_run_bar.h @@ -39,6 +39,7 @@ class Button; class EditorRunNative; class PanelContainer; class HBoxContainer; +class AcceptDialog; class EditorRunBar : public MarginContainer { GDCLASS(EditorRunBar, MarginContainer); @@ -55,6 +56,11 @@ class EditorRunBar : public MarginContainer { PanelContainer *main_panel = nullptr; HBoxContainer *main_hbox = nullptr; + PanelContainer *safe_mode_panel = nullptr; + Button *safe_mode_button = nullptr; + Button *safe_mode_reload_button = nullptr; + AcceptDialog *safe_mode_popup = nullptr; + Button *play_button = nullptr; Button *pause_button = nullptr; Button *stop_button = nullptr; @@ -90,6 +96,9 @@ class EditorRunBar : public MarginContainer { public: static EditorRunBar *get_singleton() { return singleton; } + void safe_mode_show_dialog(); + void safe_mode_reload_project(); + void play_main_scene(bool p_from_native = false); void play_current_scene(bool p_reload = false); void play_custom_scene(const String &p_custom); diff --git a/editor/gui/scene_tree_editor.cpp b/editor/gui/scene_tree_editor.cpp index c11da5dfdb38..ef0441b72bc2 100644 --- a/editor/gui/scene_tree_editor.cpp +++ b/editor/gui/scene_tree_editor.cpp @@ -413,8 +413,13 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) { Color button_color = Color(1, 1, 1); // Can't set tooltip after adding button, need to do it before. if (scr->is_tool()) { - additional_notes += "\n" + TTR("This script is currently running in the editor."); - button_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); + if (Engine::get_singleton()->is_safe_mode_hint()) { + additional_notes += "\n" + TTR("This script can run in the editor.\nIt is currently disabled due to safe mode."); + button_color = get_theme_color(SNAME("warning_color"), EditorStringName(Editor)); + } else { + additional_notes += "\n" + TTR("This script is currently running in the editor."); + button_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); + } } if (EditorNode::get_singleton()->get_object_custom_type_base(p_node) == scr) { additional_notes += "\n" + TTR("This script is a custom type."); diff --git a/editor/plugins/asset_library_editor_plugin.cpp b/editor/plugins/asset_library_editor_plugin.cpp index 8db106da074e..715a0fb19544 100644 --- a/editor/plugins/asset_library_editor_plugin.cpp +++ b/editor/plugins/asset_library_editor_plugin.cpp @@ -1782,7 +1782,7 @@ bool AssetLibraryEditorPlugin::is_available() { // directly from GitHub which does not set CORS. return false; #else - return StreamPeerTLS::is_available(); + return StreamPeerTLS::is_available() && !Engine::get_singleton()->is_safe_mode_hint(); #endif } diff --git a/editor/plugins/editor_plugin_settings.cpp b/editor/plugins/editor_plugin_settings.cpp index 5949bc141f52..ff11c011ec66 100644 --- a/editor/plugins/editor_plugin_settings.cpp +++ b/editor/plugins/editor_plugin_settings.cpp @@ -39,6 +39,7 @@ #include "editor/editor_string_names.h" #include "editor/themes/editor_scale.h" #include "scene/gui/margin_container.h" +#include "scene/gui/separator.h" #include "scene/gui/tree.h" void EditorPluginSettings::_notification(int p_what) { @@ -51,6 +52,12 @@ void EditorPluginSettings::_notification(int p_what) { plugin_config_dialog->connect("plugin_ready", callable_mp(EditorNode::get_singleton(), &EditorNode::_on_plugin_ready)); plugin_list->connect("button_clicked", callable_mp(this, &EditorPluginSettings::_cell_button_pressed)); } break; + + case NOTIFICATION_THEME_CHANGED: { + if (Engine::get_singleton()->is_safe_mode_hint()) { + safe_mode_icon->set_texture(get_editor_theme_icon(SNAME("NodeWarning"))); + } + } break; } } @@ -206,6 +213,23 @@ EditorPluginSettings::EditorPluginSettings() { plugin_config_dialog->config(""); add_child(plugin_config_dialog); + if (Engine::get_singleton()->is_safe_mode_hint()) { + HBoxContainer *c = memnew(HBoxContainer); + add_child(c); + + safe_mode_icon = memnew(TextureRect); + safe_mode_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED); + c->add_child(safe_mode_icon); + + Label *safe_mode_label = memnew(Label(TTR("Safe mode is enabled. Enabled plugins will not run while this mode is active."))); + safe_mode_label->set_theme_type_variation("HeaderSmall"); + safe_mode_label->set_h_size_flags(SIZE_EXPAND_FILL); + c->add_child(safe_mode_label); + + HSeparator *sep = memnew(HSeparator); + add_child(sep); + } + HBoxContainer *title_hb = memnew(HBoxContainer); Label *label = memnew(Label(TTR("Installed Plugins:"))); label->set_theme_type_variation("HeaderSmall"); diff --git a/editor/plugins/editor_plugin_settings.h b/editor/plugins/editor_plugin_settings.h index ea7d0ea8b31d..41f36b0cafea 100644 --- a/editor/plugins/editor_plugin_settings.h +++ b/editor/plugins/editor_plugin_settings.h @@ -55,6 +55,7 @@ class EditorPluginSettings : public VBoxContainer { }; PluginConfigDialog *plugin_config_dialog = nullptr; + TextureRect *safe_mode_icon = nullptr; Tree *plugin_list = nullptr; bool updating = false; diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 30cf2030bca9..ee2dcb4cbe7e 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -250,6 +250,7 @@ void ProjectManager::_update_theme(bool p_skip_creation) { import_btn->set_button_icon(get_editor_theme_icon(SNAME("Load"))); scan_btn->set_button_icon(get_editor_theme_icon(SNAME("Search"))); open_btn->set_button_icon(get_editor_theme_icon(SNAME("Edit"))); + open_options_btn->set_button_icon(get_editor_theme_icon(SNAME("Collapse"))); run_btn->set_button_icon(get_editor_theme_icon(SNAME("Play"))); rename_btn->set_button_icon(get_editor_theme_icon(SNAME("Rename"))); manage_tags_btn->set_button_icon(get_editor_theme_icon("Script")); @@ -269,6 +270,9 @@ void ProjectManager::_update_theme(bool p_skip_creation) { manage_tags_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager"))); erase_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager"))); erase_missing_btn->add_theme_constant_override("h_separation", get_theme_constant(SNAME("sidebar_button_icon_separation"), SNAME("ProjectManager"))); + + open_btn_container->add_theme_constant_override("separation", 0); + open_options_popup->set_item_icon(0, get_editor_theme_icon(SNAME("NodeWarning"))); } // Asset library popup. @@ -501,6 +505,10 @@ void ProjectManager::_open_selected_projects() { args.push_back("--editor"); + if (open_in_safe_mode) { + args.push_back("--safe-mode"); + } + Error err = OS::get_singleton()->create_instance(args); if (err != OK) { loading_label->hide(); @@ -516,7 +524,7 @@ void ProjectManager::_open_selected_projects() { get_tree()->quit(); } -void ProjectManager::_open_selected_projects_ask() { +void ProjectManager::_open_selected_projects_check_warnings() { const HashSet &selected_list = project_list->get_selected_project_keys(); if (selected_list.size() < 1) { return; @@ -605,6 +613,22 @@ void ProjectManager::_open_selected_projects_ask() { _open_selected_projects(); } +void ProjectManager::_open_selected_projects_check_safe_mode() { + ProjectList::Item project = project_list->get_selected_projects()[0]; + if (project.missing) { + return; + } + + open_in_safe_mode = false; + // Check if the project failed to load during last startup. + if (project.safe_mode) { + open_safe_mode_ask->popup_centered(); + return; + } + + _open_selected_projects_check_warnings(); +} + void ProjectManager::_open_selected_projects_with_migration() { #ifndef DISABLE_DEPRECATED if (project_list->get_selected_projects().size() == 1) { @@ -697,6 +721,7 @@ void ProjectManager::_update_project_buttons() { erase_btn->set_disabled(empty_selection); open_btn->set_disabled(empty_selection || is_missing_project_selected); + open_options_btn->set_disabled(empty_selection || is_missing_project_selected); rename_btn->set_disabled(empty_selection || is_missing_project_selected); manage_tags_btn->set_disabled(empty_selection || is_missing_project_selected || selected_projects.size() > 1); run_btn->set_disabled(empty_selection || is_missing_project_selected); @@ -704,6 +729,19 @@ void ProjectManager::_update_project_buttons() { erase_missing_btn->set_disabled(!project_list->is_any_project_missing()); } +void ProjectManager::_open_options_popup() { + Rect2 rect = open_btn_container->get_screen_rect(); + rect.position.y += rect.size.height; + open_options_popup->set_size(Size2(rect.size.width, 0)); + open_options_popup->set_position(rect.position); + + open_options_popup->popup(); +} + +void ProjectManager::_open_safe_mode_ask() { + open_safe_mode_ask->popup_centered(); +} + void ProjectManager::_on_projects_updated() { Vector selected_projects = project_list->get_selected_projects(); int index = 0; @@ -717,6 +755,25 @@ void ProjectManager::_on_projects_updated() { project_list->update_dock_menu(); } +void ProjectManager::_on_open_options_selected(int p_option) { + switch (p_option) { + case 0: // Edit in safe mode. + _open_safe_mode_ask(); + break; + } +} + +void ProjectManager::_on_safe_mode_popup_open_normal() { + open_safe_mode_ask->hide(); + open_in_safe_mode = false; + _open_selected_projects_check_warnings(); +} + +void ProjectManager::_on_safe_mode_popup_open_safe() { + open_in_safe_mode = true; + _open_selected_projects_check_warnings(); +} + void ProjectManager::_on_project_created(const String &dir) { project_list->add_project(dir, false); project_list->save_config(); @@ -724,7 +781,7 @@ void ProjectManager::_on_project_created(const String &dir) { int i = project_list->refresh_project(dir); project_list->select_project(i); project_list->ensure_project_visible(i); - _open_selected_projects_ask(); + _open_selected_projects_check_warnings(); project_list->update_dock_menu(); } @@ -751,7 +808,7 @@ void ProjectManager::_on_search_term_submitted(const String &p_text) { return; } - _open_selected_projects_ask(); + _open_selected_projects_check_safe_mode(); } LineEdit *ProjectManager::get_search_box() { @@ -968,7 +1025,7 @@ void ProjectManager::shortcut_input(const Ref &p_ev) { switch (k->get_keycode()) { case Key::ENTER: { - _open_selected_projects_ask(); + _open_selected_projects_check_safe_mode(); } break; case Key::HOME: { if (project_list->get_project_count() > 0) { @@ -1327,7 +1384,7 @@ ProjectManager::ProjectManager() { project_list->connect(ProjectList::SIGNAL_LIST_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons)); project_list->connect(ProjectList::SIGNAL_LIST_CHANGED, callable_mp(this, &ProjectManager::_update_list_placeholder)); project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons)); - project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask)); + project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_check_safe_mode)); // Empty project list placeholder. { @@ -1386,11 +1443,30 @@ ProjectManager::ProjectManager() { project_list_sidebar->add_child(memnew(HSeparator)); + open_btn_container = memnew(HBoxContainer); + open_btn_container->set_anchors_preset(Control::PRESET_FULL_RECT); + project_list_sidebar->add_child(open_btn_container); + open_btn = memnew(Button); open_btn->set_text(TTR("Edit")); open_btn->set_shortcut(ED_SHORTCUT("project_manager/edit_project", TTR("Edit Project"), KeyModifierMask::CMD_OR_CTRL | Key::E)); - open_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_open_selected_projects_ask)); - project_list_sidebar->add_child(open_btn); + open_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_open_selected_projects_check_safe_mode)); + open_btn->set_h_size_flags(Control::SIZE_EXPAND_FILL); + open_btn_container->add_child(open_btn); + + open_btn_container->add_child(memnew(VSeparator)); + + open_options_btn = memnew(Button); + open_options_btn->set_icon_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER); + open_options_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_open_options_popup)); + open_btn_container->add_child(open_options_btn); + + open_options_popup = memnew(PopupMenu); + open_options_popup->add_item(TTR("Edit in safe mode")); + open_options_popup->connect(SceneStringName(id_pressed), callable_mp(this, &ProjectManager::_on_open_options_selected)); + open_options_btn->add_child(open_options_popup); + + open_btn_container->set_custom_minimum_size(Size2(120, open_btn->get_combined_minimum_size().y)); run_btn = memnew(Button); run_btn->set_text(TTR("Run")); @@ -1506,6 +1582,15 @@ ProjectManager::ProjectManager() { multi_run_ask->get_ok_button()->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_run_project_confirm)); add_child(multi_run_ask); + open_safe_mode_ask = memnew(ConfirmationDialog); + open_safe_mode_ask->set_min_size(Size2(550, 70)); + open_safe_mode_ask->set_ok_button_text(TTR("Edit in Safe Mode")); + open_safe_mode_ask->set_text(TTR("It looks like Godot crashed when opening this project the last time. If you're having problems editing this project, you can try to open it in Safe Mode.\n\nThis is a special mode that may help to recover projects that crash the engine during initialization. This mode temporarily disables the following features:\n\n- Tool scripts\n- Editor plugins\n- GDExtension addons\n- Automatic scene restoring\n\nThis mode is intended only for basic editing to troubleshoot such issues, and therefore it will not possible to run the project during this mode. It is also a good idea to make a backup of your project before proceeding.\n\nEdit the project in Safe Mode?")); + open_safe_mode_ask->set_autowrap(true); + open_safe_mode_ask->add_button(TTR("Edit normally"))->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_on_safe_mode_popup_open_normal)); + open_safe_mode_ask->get_ok_button()->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_on_safe_mode_popup_open_safe)); + add_child(open_safe_mode_ask); + ask_update_settings = memnew(ConfirmationDialog); ask_update_settings->set_autowrap(true); ask_update_settings->get_ok_button()->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_open_selected_projects_with_migration)); diff --git a/editor/project_manager.h b/editor/project_manager.h index 07da0059c01a..faffd768cb96 100644 --- a/editor/project_manager.h +++ b/editor/project_manager.h @@ -44,6 +44,7 @@ class LineEdit; class MarginContainer; class OptionButton; class PanelContainer; +class PopupMenu; class ProjectDialog; class ProjectList; class QuickSettingsDialog; @@ -145,12 +146,16 @@ class ProjectManager : public Control { Button *import_btn = nullptr; Button *scan_btn = nullptr; Button *open_btn = nullptr; + Button *open_options_btn = nullptr; Button *run_btn = nullptr; Button *rename_btn = nullptr; Button *manage_tags_btn = nullptr; Button *erase_btn = nullptr; Button *erase_missing_btn = nullptr; + HBoxContainer *open_btn_container = nullptr; + PopupMenu *open_options_popup = nullptr; + EditorFileDialog *scan_dir = nullptr; ConfirmationDialog *erase_ask = nullptr; @@ -161,6 +166,7 @@ class ProjectManager : public Control { ConfirmationDialog *erase_missing_ask = nullptr; ConfirmationDialog *multi_open_ask = nullptr; ConfirmationDialog *multi_run_ask = nullptr; + ConfirmationDialog *open_safe_mode_ask = nullptr; ProjectDialog *project_dialog = nullptr; @@ -168,8 +174,9 @@ class ProjectManager : public Control { void _run_project(); void _run_project_confirm(); void _open_selected_projects(); - void _open_selected_projects_ask(); void _open_selected_projects_with_migration(); + void _open_selected_projects_check_warnings(); + void _open_selected_projects_check_safe_mode(); void _install_project(const String &p_zip_path, const String &p_title); void _import_project(); @@ -180,9 +187,14 @@ class ProjectManager : public Control { void _erase_project_confirm(); void _erase_missing_projects_confirm(); void _update_project_buttons(); + void _open_options_popup(); + void _open_safe_mode_ask(); void _on_project_created(const String &dir); void _on_projects_updated(); + void _on_open_options_selected(int p_option); + void _on_safe_mode_popup_open_normal(); + void _on_safe_mode_popup_open_safe(); void _on_order_option_changed(int p_idx); void _on_search_term_changed(const String &p_term); @@ -218,6 +230,7 @@ class ProjectManager : public Control { Button *full_convert_button = nullptr; String version_convert_feature; + bool open_in_safe_mode = false; #ifndef DISABLE_DEPRECATED void _minor_project_migrate(); diff --git a/editor/project_manager/project_list.cpp b/editor/project_manager/project_list.cpp index 27f04c0d0d3d..fa0fbc1c995c 100644 --- a/editor/project_manager/project_list.cpp +++ b/editor/project_manager/project_list.cpp @@ -417,14 +417,16 @@ ProjectList::Item ProjectList::load_project_data(const String &p_path, bool p_fa String conf = p_path.path_join("project.godot"); bool grayed = false; bool missing = false; + bool safe_mode = false; Ref cf = memnew(ConfigFile); Error cf_err = cf->load(conf); int config_version = 0; + String cf_project_name; String project_name = TTR("Unnamed Project"); if (cf_err == OK) { - String cf_project_name = cf->get_value("application", "config/name", ""); + cf_project_name = cf->get_value("application", "config/name", ""); if (!cf_project_name.is_empty()) { project_name = cf_project_name.xml_unescape(); } @@ -486,7 +488,13 @@ ProjectList::Item ProjectList::load_project_data(const String &p_path, bool p_fa ProjectManager::get_singleton()->add_new_tag(tag); } - return Item(project_name, description, project_version, tags, p_path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, config_version); + // We can't use globalize_path because there is no loaded project. So we replicate the behavior of accessing user:// + if (!cf_project_name.is_empty()) { + String safe_mode_lock_file = OS::get_singleton()->get_user_data_dir(cf_project_name).path_join(".safe_mode_lock"); + safe_mode = FileAccess::exists(safe_mode_lock_file); + } + + return Item(project_name, description, project_version, tags, p_path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, safe_mode, config_version); } void ProjectList::_update_icons_async() { diff --git a/editor/project_manager/project_list.h b/editor/project_manager/project_list.h index 6e0f5830ac31..be986d4187d1 100644 --- a/editor/project_manager/project_list.h +++ b/editor/project_manager/project_list.h @@ -115,6 +115,7 @@ class ProjectList : public ScrollContainer { bool favorite = false; bool grayed = false; bool missing = false; + bool safe_mode = false; int version = 0; ProjectListItemControl *control = nullptr; @@ -133,6 +134,7 @@ class ProjectList : public ScrollContainer { bool p_favorite, bool p_grayed, bool p_missing, + bool p_safe_mode, int p_version) { project_name = p_name; description = p_description; @@ -146,6 +148,7 @@ class ProjectList : public ScrollContainer { favorite = p_favorite; grayed = p_grayed; missing = p_missing; + safe_mode = p_safe_mode; version = p_version; control = nullptr; diff --git a/editor/register_editor_types.cpp b/editor/register_editor_types.cpp index 2d3cbfb1e3c5..371eaa1222b7 100644 --- a/editor/register_editor_types.cpp +++ b/editor/register_editor_types.cpp @@ -215,7 +215,9 @@ void register_editor_types() { EditorPlugins::add_by_type(); EditorPlugins::add_by_type(); EditorPlugins::add_by_type(); - EditorPlugins::add_by_type(); + if (!Engine::get_singleton()->is_safe_mode_hint()) { + EditorPlugins::add_by_type(); + } EditorPlugins::add_by_type(); EditorPlugins::add_by_type(); EditorPlugins::add_by_type(); diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp index 32079f3753fa..acd90eecc56f 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -1927,6 +1927,11 @@ void EditorThemeManager::_populate_editor_styles(const Ref &p_theme style_launch_pad_movie->set_border_color(p_config.accent_color); style_launch_pad_movie->set_border_width_all(Math::round(2 * EDSCALE)); p_theme->set_stylebox("LaunchPadMovieMode", EditorStringName(EditorStyles), style_launch_pad_movie); + Ref style_launch_pad_safe_mode = style_launch_pad->duplicate(); + style_launch_pad_safe_mode->set_bg_color(p_config.accent_color * Color(1, 1, 1, 0.1)); + style_launch_pad_safe_mode->set_border_color(p_config.warning_color); + style_launch_pad_safe_mode->set_border_width_all(Math::round(2 * EDSCALE)); + p_theme->set_stylebox("LaunchPadSafeMode", EditorStringName(EditorStyles), style_launch_pad_safe_mode); p_theme->set_stylebox("MovieWriterButtonNormal", EditorStringName(EditorStyles), make_empty_stylebox(0, 0, 0, 0)); Ref style_write_movie_button = p_config.button_style_pressed->duplicate(); @@ -1944,6 +1949,16 @@ void EditorThemeManager::_populate_editor_styles(const Ref &p_theme p_theme->set_color("movie_writer_icon_pressed", EditorStringName(EditorStyles), Color(0, 0, 0, 0.84)); p_theme->set_color("movie_writer_icon_hover", EditorStringName(EditorStyles), Color(1, 1, 1, 0.9)); p_theme->set_color("movie_writer_icon_hover_pressed", EditorStringName(EditorStyles), Color(0, 0, 0, 0.84)); + + Ref style_safe_mode_button = p_config.button_style_pressed->duplicate(); + style_safe_mode_button->set_bg_color(p_config.warning_color); + style_safe_mode_button->set_corner_radius_all(p_config.corner_radius * EDSCALE); + style_safe_mode_button->set_content_margin_all(0); + // Safe mode button is implicitly styled from the panel's background. + // So, remove any existing borders. (e.g. from draw_extra_borders config) + style_safe_mode_button->set_border_width_all(0); + style_safe_mode_button->set_expand_margin(SIDE_RIGHT, 2 * EDSCALE); + p_theme->set_stylebox("SafeModeButton", EditorStringName(EditorStyles), style_safe_mode_button); } // Standard GUI variations. diff --git a/main/main.cpp b/main/main.cpp index d3c768946c26..60a5f76598f8 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -197,6 +197,7 @@ static uint64_t quit_after = 0; static OS::ProcessID editor_pid = 0; #ifdef TOOLS_ENABLED static bool found_project = false; +static bool safe_mode = false; static bool auto_build_solutions = false; static String debug_server_uri; static bool wait_for_import = false; @@ -548,6 +549,7 @@ void Main::print_help(const char *p_binary) { #ifdef TOOLS_ENABLED print_help_option("-e, --editor", "Start the editor instead of running the scene.\n", CLI_OPTION_AVAILABILITY_EDITOR); print_help_option("-p, --project-manager", "Start the project manager, even if a project is auto-detected.\n", CLI_OPTION_AVAILABILITY_EDITOR); + print_help_option("--safe-mode", "Start the editor in safe mode, which disables typical features that can cause startup crashes, such as tool scripts, editor plugins, GDExtension addons, and others.\n", CLI_OPTION_AVAILABILITY_EDITOR); print_help_option("--debug-server ", "Start the editor debug server (://[:port], e.g. tcp://127.0.0.1:6007)\n", CLI_OPTION_AVAILABILITY_EDITOR); print_help_option("--dap-port ", "Use the specified port for the GDScript Debugger Adaptor protocol. Recommended port range [1024, 49151].\n", CLI_OPTION_AVAILABILITY_EDITOR); #if defined(MODULE_GDSCRIPT_ENABLED) && !defined(GDSCRIPT_NO_LSP) @@ -1418,6 +1420,8 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph editor = true; } else if (arg == "-p" || arg == "--project-manager") { // starts project manager project_manager = true; + } else if (arg == "--safe-mode") { // Enables safe mode. + safe_mode = true; } else if (arg == "--debug-server") { if (N) { debug_server_uri = N->get(); @@ -1876,6 +1880,9 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph if (editor) { Engine::get_singleton()->set_editor_hint(true); Engine::get_singleton()->set_extension_reloading_enabled(true); + + // Create initialization lock file to detect crashes during startup. + OS::get_singleton()->create_lock_file(); } #endif @@ -1921,6 +1928,10 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph if (project_manager) { Engine::get_singleton()->set_project_manager_hint(true); } + + if (safe_mode) { + Engine::get_singleton()->set_safe_mode_hint(true); + } #endif GLOBAL_DEF("debug/file_logging/enable_file_logging", false); @@ -2676,6 +2687,10 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph print_help(execpath); } + if (editor) { + OS::get_singleton()->remove_lock_file(); + } + EngineDebugger::deinitialize(); if (performance) { @@ -3579,6 +3594,8 @@ int Main::start() { editor = true; } else if (E->get() == "-p" || E->get() == "--project-manager") { project_manager = true; + } else if (E->get() == "--safe-mode") { + safe_mode = true; } else if (E->get() == "--install-android-build-template") { install_android_build_template = true; #endif // TOOLS_ENABLED @@ -4172,7 +4189,7 @@ int Main::start() { #ifdef TOOLS_ENABLED if (editor) { - if (game_path != String(GLOBAL_GET("application/run/main_scene")) || !editor_node->has_scenes_in_session()) { + if (!safe_mode && (game_path != String(GLOBAL_GET("application/run/main_scene")) || !editor_node->has_scenes_in_session())) { Error serr = editor_node->load_scene(local_game_path); if (serr != OK) { ERR_PRINT("Failed to load scene"); @@ -4255,6 +4272,10 @@ int Main::start() { Crypto::load_default_certificates( EditorSettings::get_singleton()->get_setting("network/tls/editor_tls_certificates").operator String()); } + + if (safe_mode) { + Engine::get_singleton()->set_safe_mode_hint(true); + } #endif } diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 8c094c0ab055..62a32acaca92 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -256,7 +256,7 @@ Variant GDScript::_new(const Variant **p_args, int p_argcount, Callable::CallErr bool GDScript::can_instantiate() const { #ifdef TOOLS_ENABLED - return valid && (tool || ScriptServer::is_scripting_enabled()); + return valid && (tool || ScriptServer::is_scripting_enabled()) && !Engine::get_singleton()->is_safe_mode_hint(); #else return valid; #endif diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 380b401683cd..b1f5357a9662 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -2328,7 +2328,7 @@ void CSharpScript::update_script_class_info(Ref p_script) { bool CSharpScript::can_instantiate() const { #ifdef TOOLS_ENABLED - bool extra_cond = type_info.is_tool || ScriptServer::is_scripting_enabled(); + bool extra_cond = (type_info.is_tool || ScriptServer::is_scripting_enabled()) && !Engine::get_singleton()->is_safe_mode_hint(); #else bool extra_cond = true; #endif diff --git a/platform/android/java_godot_io_wrapper.cpp b/platform/android/java_godot_io_wrapper.cpp index 623db39985f2..eb23d3579861 100644 --- a/platform/android/java_godot_io_wrapper.cpp +++ b/platform/android/java_godot_io_wrapper.cpp @@ -106,7 +106,7 @@ String GodotIOJavaWrapper::get_cache_dir() { } } -String GodotIOJavaWrapper::get_user_data_dir() { +String GodotIOJavaWrapper::get_user_data_dir(const String &p_appname) { if (_get_data_dir) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, String()); diff --git a/platform/android/java_godot_io_wrapper.h b/platform/android/java_godot_io_wrapper.h index 0a372641cbb9..7a0f8e1a605f 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -71,7 +71,7 @@ class GodotIOJavaWrapper { Error open_uri(const String &p_uri); String get_cache_dir(); - String get_user_data_dir(); + String get_user_data_dir(const String &p_appname); String get_locale(); String get_model(); int get_screen_dpi(); diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 7b0d3a29e90f..e8f8e39293e5 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -413,7 +413,7 @@ String OS_Android::get_model_name() const { } String OS_Android::get_data_path() const { - return get_user_data_dir(); + return OS::get_user_data_dir(); } void OS_Android::_load_system_font_config() { @@ -647,12 +647,12 @@ String OS_Android::get_executable_path() const { return OS::get_executable_path(); } -String OS_Android::get_user_data_dir() const { +String OS_Android::get_user_data_dir(const String &p_appname) const { if (!data_dir_cache.is_empty()) { return data_dir_cache; } - String data_dir = godot_io_java->get_user_data_dir(); + String data_dir = godot_io_java->get_user_data_dir(p_appname); if (!data_dir.is_empty()) { data_dir_cache = _remove_symlink(data_dir); return data_dir_cache; @@ -751,7 +751,7 @@ void OS_Android::vibrate_handheld(int p_duration_ms, float p_amplitude) { } String OS_Android::get_config_path() const { - return get_user_data_dir().path_join("config"); + return OS::get_user_data_dir().path_join("config"); } void OS_Android::benchmark_begin_measure(const String &p_context, const String &p_what) { @@ -881,7 +881,7 @@ String OS_Android::get_system_ca_certificates() { } Error OS_Android::setup_remote_filesystem(const String &p_server_host, int p_port, const String &p_password, String &r_project_path) { - r_project_path = get_user_data_dir(); + r_project_path = OS::get_user_data_dir(); Error err = OS_Unix::setup_remote_filesystem(p_server_host, p_port, p_password, r_project_path); if (err == OK) { remote_fs_dir = r_project_path; diff --git a/platform/android/os_android.h b/platform/android/os_android.h index fb3cdf0d4c0d..596c07794c40 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -145,7 +145,7 @@ class OS_Android : public OS_Unix { virtual String get_system_font_path(const String &p_font_name, int p_weight = 400, int p_stretch = 100, bool p_italic = false) const override; virtual Vector get_system_font_path_for_text(const String &p_font_name, const String &p_text, const String &p_locale = String(), const String &p_script = String(), int p_weight = 400, int p_stretch = 100, bool p_italic = false) const override; virtual String get_executable_path() const override; - virtual String get_user_data_dir() const override; + virtual String get_user_data_dir(const String &p_appname) const override; virtual String get_data_path() const override; virtual String get_cache_path() const override; virtual String get_resource_dir() const override; diff --git a/platform/ios/os_ios.h b/platform/ios/os_ios.h index b7c5a7306560..63dd7a51263f 100644 --- a/platform/ios/os_ios.h +++ b/platform/ios/os_ios.h @@ -114,7 +114,7 @@ class OS_IOS : public OS_Unix { virtual Error shell_open(const String &p_uri) override; - virtual String get_user_data_dir() const override; + virtual String get_user_data_dir(const String &p_appname) const override; virtual String get_cache_path() const override; diff --git a/platform/ios/os_ios.mm b/platform/ios/os_ios.mm index 590238be77f7..ad6db1244c31 100644 --- a/platform/ios/os_ios.mm +++ b/platform/ios/os_ios.mm @@ -314,7 +314,7 @@ void register_dynamic_symbol(char *name, void *address) { return OK; } -String OS_IOS::get_user_data_dir() const { +String OS_IOS::get_user_data_dir(const String &p_appname) const { static String ret; if (ret.is_empty()) { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); diff --git a/platform/web/os_web.cpp b/platform/web/os_web.cpp index 51facbaa845d..fd425f34fdf4 100644 --- a/platform/web/os_web.cpp +++ b/platform/web/os_web.cpp @@ -178,19 +178,18 @@ void OS_Web::vibrate_handheld(int p_duration_ms, float p_amplitude) { godot_js_input_vibrate_handheld(p_duration_ms); } -String OS_Web::get_user_data_dir() const { +String OS_Web::get_user_data_dir(const String &p_appname) const { String userfs = "/userfs"; - String appname = get_safe_dir_name(GLOBAL_GET("application/config/name")); - if (!appname.is_empty()) { + if (!p_appname.is_empty()) { bool use_custom_dir = GLOBAL_GET("application/config/use_custom_user_dir"); if (use_custom_dir) { String custom_dir = get_safe_dir_name(GLOBAL_GET("application/config/custom_user_dir_name"), true); if (custom_dir.is_empty()) { - custom_dir = appname; + custom_dir = p_appname; } return userfs.path_join(custom_dir).replace("\\", "/"); } else { - return userfs.path_join(get_godot_dir_name()).path_join("app_userdata").path_join(appname).replace("\\", "/"); + return userfs.path_join(get_godot_dir_name()).path_join("app_userdata").path_join(p_appname).replace("\\", "/"); } } diff --git a/platform/web/os_web.h b/platform/web/os_web.h index 1ddb745965ac..e9c4252ff466 100644 --- a/platform/web/os_web.h +++ b/platform/web/os_web.h @@ -103,7 +103,7 @@ class OS_Web : public OS_Unix { String get_cache_path() const override; String get_config_path() const override; String get_data_path() const override; - String get_user_data_dir() const override; + String get_user_data_dir(const String &p_appname) const override; bool is_userfs_persistent() const override; diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 4d253059b24b..540fe181c4f9 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -2119,18 +2119,17 @@ String OS_Windows::get_system_dir(SystemDir p_dir, bool p_shared_storage) const return path; } -String OS_Windows::get_user_data_dir() const { - String appname = get_safe_dir_name(GLOBAL_GET("application/config/name")); - if (!appname.is_empty()) { +String OS_Windows::get_user_data_dir(const String &p_appname) const { + if (!p_appname.is_empty()) { bool use_custom_dir = GLOBAL_GET("application/config/use_custom_user_dir"); if (use_custom_dir) { String custom_dir = get_safe_dir_name(GLOBAL_GET("application/config/custom_user_dir_name"), true); if (custom_dir.is_empty()) { - custom_dir = appname; + custom_dir = p_appname; } return get_data_path().path_join(custom_dir).replace("\\", "/"); } else { - return get_data_path().path_join(get_godot_dir_name()).path_join("app_userdata").path_join(appname).replace("\\", "/"); + return get_data_path().path_join(get_godot_dir_name()).path_join("app_userdata").path_join(p_appname).replace("\\", "/"); } } diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h index a32e535f0b0e..4c55f980142c 100644 --- a/platform/windows/os_windows.h +++ b/platform/windows/os_windows.h @@ -227,7 +227,7 @@ class OS_Windows : public OS { virtual String get_godot_dir_name() const override; virtual String get_system_dir(SystemDir p_dir, bool p_shared_storage = true) const override; - virtual String get_user_data_dir() const override; + virtual String get_user_data_dir(const String &p_appname) const override; virtual String get_unique_id() const override;