From 015a1e7e13af6c59211624eda52f1454ba8047da 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 a0b1ffa98123..a5d2b5cd0afa 100644 --- a/core/config/engine.h +++ b/core/config/engine.h @@ -84,6 +84,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; @@ -156,6 +157,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; } @@ -165,6 +169,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 fff938858fd3..1e5267b7ae0d 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 642de11a9f10..323c7c240877 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 30d2a4266fe4..7b9adc02f60f 100644 --- a/core/os/os.h +++ b/core/os/os.h @@ -278,6 +278,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; @@ -296,6 +297,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 8a9b13006897..6fccc30e52f8 100644 --- a/drivers/unix/os_unix.cpp +++ b/drivers/unix/os_unix.cpp @@ -872,18 +872,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 3add5df055e9..4b330863fc26 100644 --- a/drivers/unix/os_unix.h +++ b/drivers/unix/os_unix.h @@ -100,7 +100,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 b4265f9fc0e0..44779586c9e3 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)); } @@ -262,6 +266,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 2853ebc4994c..5ca40d1d020e 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -730,6 +730,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; @@ -1150,9 +1154,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(); @@ -5346,6 +5356,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 55caed4bb415..2bdb21a3d9ce 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 908b1e671953..8e43a52281c0 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_icon(get_editor_theme_icon(SNAME("NodeWarning"))); + safe_mode_reload_button->set_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_icon(get_editor_theme_icon(SNAME("Pause"))); stop_button->set_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_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 2e36b66025a2..3faab1a266d7 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 fec8d4b89721..9687d8c27d86 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 30878a248809..b6cc6291575c 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_icon(get_editor_theme_icon(SNAME("Load"))); scan_btn->set_icon(get_editor_theme_icon(SNAME("Search"))); open_btn->set_icon(get_editor_theme_icon(SNAME("Edit"))); + open_options_btn->set_icon(get_editor_theme_icon(SNAME("Collapse"))); run_btn->set_icon(get_editor_theme_icon(SNAME("Play"))); rename_btn->set_icon(get_editor_theme_icon(SNAME("Rename"))); manage_tags_btn->set_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 541ab01e6268..947147525e6b 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 19aeeb0612d8..2c33c51398a9 100644 --- a/editor/register_editor_types.cpp +++ b/editor/register_editor_types.cpp @@ -216,7 +216,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 17bcbacfc204..488cee7202d8 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -1910,6 +1910,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(); @@ -1927,6 +1932,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 36912c4fa394..894ef5d0547e 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -196,6 +196,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; @@ -546,6 +547,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) @@ -1408,6 +1410,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(); @@ -1855,6 +1859,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 @@ -1900,6 +1907,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); @@ -2645,6 +2656,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) { @@ -3544,6 +3559,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 @@ -4132,7 +4149,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"); @@ -4209,6 +4226,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 7b9aa70686a7..66aa5d0631d3 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -249,7 +249,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 3d1299446982..fde40e9eaf99 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 49913b9c30ad..62c62818de7d 100644 --- a/platform/android/java_godot_io_wrapper.cpp +++ b/platform/android/java_godot_io_wrapper.cpp @@ -105,7 +105,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 c113a13040d9..94390422c05e 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -70,7 +70,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 adc72a79e9cf..1c2b5a4e062e 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -1903,18 +1903,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 4f9bc049ee5b..adfb1242bca2 100644 --- a/platform/windows/os_windows.h +++ b/platform/windows/os_windows.h @@ -218,7 +218,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;