diff --git a/core/config/engine.h b/core/config/engine.h index 2b92fcf90961..ec5fb955ac48 100644 --- a/core/config/engine.h +++ b/core/config/engine.h @@ -87,6 +87,7 @@ class Engine { bool project_manager_hint = false; bool extension_reloading = false; bool embedded_in_editor = false; + bool recovery_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_recovery_mode_hint(bool p_enabled) { recovery_mode_hint = p_enabled; } + _FORCE_INLINE_ bool is_recovery_mode_hint() const { return recovery_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_recovery_mode_hint(bool p_enabled) {} + _FORCE_INLINE_ bool is_recovery_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 e6bc4f24acbb..ae570f775939 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_recovery_mode_hint()) { + return LOAD_STATUS_FAILED; + } + 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_recovery_mode_hint()) { + return LOAD_STATUS_FAILED; + } + 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_recovery_mode_hint()) { + return LOAD_STATUS_FAILED; + } + 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_recovery_mode_hint()) { + return; + } + ERR_FAIL_COND(int32_t(p_level) - 1 != level); for (KeyValue> &E : gdextension_map) { E.value->initialize_library(p_level); @@ -221,6 +237,10 @@ void GDExtensionManager::initialize_extensions(GDExtension::InitializationLevel } void GDExtensionManager::deinitialize_extensions(GDExtension::InitializationLevel p_level) { + if (Engine::get_singleton()->is_recovery_mode_hint()) { + return; + } + ERR_FAIL_COND(int32_t(p_level) != level); for (KeyValue> &E : gdextension_map) { E.value->deinitialize_library(p_level); @@ -259,6 +279,10 @@ void GDExtensionManager::_reload_all_scripts() { #endif // TOOLS_ENABLED void GDExtensionManager::load_extensions() { + if (Engine::get_singleton()->is_recovery_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(); @@ -273,6 +297,9 @@ void GDExtensionManager::load_extensions() { void GDExtensionManager::reload_extensions() { #ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_recovery_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 9be7706a2e7c..c161b2212f40 100644 --- a/core/os/os.cpp +++ b/core/os/os.cpp @@ -290,10 +290,28 @@ 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_user_dir) const { return "."; } +String OS::get_user_data_dir() const { + String appname = get_safe_dir_name(GLOBAL_GET("application/config/name")); + if (!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; + } + return get_user_data_dir(custom_dir); + } else { + return get_user_data_dir(get_godot_dir_name().path_join("app_userdata").path_join(appname)); + } + } else { + return get_user_data_dir(get_godot_dir_name().path_join("app_userdata").path_join("[unnamed project]")); + } +} + // Absolute path to res:// String OS::get_resource_dir() const { return ProjectSettings::get_singleton()->get_resource_path(); @@ -304,6 +322,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_recovery_mode_hint()) { + return; + } + + String lock_file_path = get_user_data_dir().path_join(".recovery_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(".recovery_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 bc8e2bcaa717..15c5715dddc1 100644 --- a/core/os/os.h +++ b/core/os/os.h @@ -285,6 +285,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_user_dir) const; virtual String get_user_data_dir() const; virtual String get_resource_dir() const; @@ -303,6 +304,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 4a5c4dab6940..a70be9326e8b 100644 --- a/drivers/unix/os_unix.cpp +++ b/drivers/unix/os_unix.cpp @@ -959,22 +959,8 @@ 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()) { - 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; - } - 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("[unnamed project]"); +String OS_Unix::get_user_data_dir(const String &p_user_dir) const { + return get_data_path().path_join(p_user_dir); } String OS_Unix::get_executable_path() const { diff --git a/drivers/unix/os_unix.h b/drivers/unix/os_unix.h index d331b0fb8e82..fa6eaef6a4e3 100644 --- a/drivers/unix/os_unix.h +++ b/drivers/unix/os_unix.h @@ -106,7 +106,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_user_dir) const override; }; class UnixTerminalLogger : public StdLogger { diff --git a/editor/debugger/editor_debugger_node.cpp b/editor/debugger/editor_debugger_node.cpp index e30bf6c3e72e..75fed9e0fa60 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_recovery_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_recovery_mode_hint()) { + return ERR_UNAVAILABLE; + } + 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 9f0ccbf01112..3d901ba7c068 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_recovery_mode_hint()) { + EditorToaster::get_singleton()->popup_str(TTR("Recovery 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; @@ -1152,9 +1156,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(); @@ -5382,6 +5392,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_recovery_mode_hint()) { + return; + } + if (!bool(EDITOR_GET("interface/scene_tabs/restore_scenes_on_load"))) { return; } @@ -6613,7 +6627,9 @@ void EditorNode::_feature_profile_changed() { editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_3D, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_3D)); editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_SCRIPT, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_SCRIPT)); - editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_GAME, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_GAME)); + if (!Engine::get_singleton()->is_recovery_mode_hint()) { + editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_GAME, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_GAME)); + } if (AssetLibraryEditorPlugin::is_available()) { editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_ASSETLIB, !profile->is_feature_disabled(EditorFeatureProfile::FEATURE_ASSET_LIB)); } @@ -6624,7 +6640,9 @@ void EditorNode::_feature_profile_changed() { editor_dock_manager->set_dock_enabled(history_dock, true); editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_3D, true); editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_SCRIPT, true); - editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_GAME, true); + if (!Engine::get_singleton()->is_recovery_mode_hint()) { + editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_GAME, true); + } if (AssetLibraryEditorPlugin::is_available()) { editor_main_screen->set_button_enabled(EditorMainScreen::EDITOR_ASSETLIB, true); } @@ -7761,7 +7779,10 @@ EditorNode::EditorNode() { add_editor_plugin(memnew(CanvasItemEditorPlugin)); add_editor_plugin(memnew(Node3DEditorPlugin)); add_editor_plugin(memnew(ScriptEditorPlugin)); - add_editor_plugin(memnew(GameViewPlugin)); + + if (!Engine::get_singleton()->is_recovery_mode_hint()) { + add_editor_plugin(memnew(GameViewPlugin)); + } EditorAudioBuses *audio_bus_editor = EditorAudioBuses::register_editor(); diff --git a/editor/editor_node.h b/editor/editor_node.h index e97a4648723c..ff0373425e5f 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 @@ -548,6 +548,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 49bd212ac2f1..1b63129d6ba8 100644 --- a/editor/gui/editor_run_bar.cpp +++ b/editor/gui/editor_run_bar.cpp @@ -40,6 +40,8 @@ #include "editor/editor_string_names.h" #include "editor/gui/editor_bottom_panel.h" #include "editor/gui/editor_quick_open_dialog.h" +#include "editor/gui/editor_toaster.h" +#include "editor/themes/editor_scale.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/panel_container.h" @@ -52,7 +54,35 @@ void EditorRunBar::_notification(int p_what) { _reset_play_buttons(); } break; + case NOTIFICATION_READY: { + if (Engine::get_singleton()->is_recovery_mode_hint()) { + recovery_mode_show_dialog(); + } + } break; + case NOTIFICATION_THEME_CHANGED: { + if (Engine::get_singleton()->is_recovery_mode_hint()) { + main_panel->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("LaunchPadRecoveryMode"), EditorStringName(EditorStyles))); + recovery_mode_panel->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("RecoveryModeButton"), EditorStringName(EditorStyles))); + recovery_mode_button->add_theme_style_override("hover", get_theme_stylebox(SNAME("RecoveryModeButton"), EditorStringName(EditorStyles))); + + recovery_mode_button->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning"))); + recovery_mode_reload_button->set_button_icon(get_editor_theme_icon(SNAME("Reload"))); + + recovery_mode_button->begin_bulk_theme_override(); + recovery_mode_button->add_theme_color_override("icon_normal_color", Color(0.3, 0.3, 0.3, 1)); + recovery_mode_button->add_theme_color_override("icon_pressed_color", Color(0.4, 0.4, 0.4, 1)); + recovery_mode_button->add_theme_color_override("icon_hover_color", Color(0.6, 0.6, 0.6, 1)); + Color dark_color = get_theme_color("recovery_mode_text_color", EditorStringName(Editor)); + recovery_mode_button->add_theme_color_override(SceneStringName(font_color), dark_color); + recovery_mode_button->add_theme_color_override("font_pressed_color", dark_color.lightened(0.2)); + recovery_mode_button->add_theme_color_override("font_hover_color", dark_color.lightened(0.4)); + recovery_mode_button->add_theme_color_override("font_hover_pressed_color", dark_color.lightened(0.2)); + recovery_mode_button->end_bulk_theme_override(); + + return; + } + _update_play_buttons(); profiler_autostart_indicator->set_button_icon(get_editor_theme_icon(SNAME("ProfilerAutostartWarning"))); pause_button->set_button_icon(get_editor_theme_icon(SNAME("Pause"))); @@ -79,6 +109,10 @@ void EditorRunBar::_notification(int p_what) { } void EditorRunBar::_reset_play_buttons() { + if (Engine::get_singleton()->is_recovery_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.")); @@ -93,6 +127,10 @@ void EditorRunBar::_reset_play_buttons() { } void EditorRunBar::_update_play_buttons() { + if (Engine::get_singleton()->is_recovery_mode_hint()) { + return; + } + _reset_play_buttons(); if (!is_playing()) { return; @@ -278,7 +316,20 @@ void EditorRunBar::_profiler_autostart_indicator_pressed() { } } +void EditorRunBar::recovery_mode_show_dialog() { + recovery_mode_popup->popup_centered(); +} + +void EditorRunBar::recovery_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_recovery_mode_hint()) { + EditorToaster::get_singleton()->popup_str(TTR("Recovery Mode is enabled. Disable it to run the project."), EditorToaster::SEVERITY_WARNING); + return; + } + if (p_from_native) { run_native->resume_run_native(); } else { @@ -290,6 +341,11 @@ void EditorRunBar::play_main_scene(bool p_from_native) { } void EditorRunBar::play_current_scene(bool p_reload) { + if (Engine::get_singleton()->is_recovery_mode_hint()) { + EditorToaster::get_singleton()->popup_str(TTR("Recovery 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(); @@ -304,6 +360,11 @@ void EditorRunBar::play_current_scene(bool p_reload) { } void EditorRunBar::play_custom_scene(const String &p_custom) { + if (Engine::get_singleton()->is_recovery_mode_hint()) { + EditorToaster::get_singleton()->popup_str(TTR("Recovery Mode is enabled. Disable it to run the project."), EditorToaster::SEVERITY_WARNING); + return; + } + stop_playing(); current_mode = RunMode::RUN_CUSTOM; @@ -378,6 +439,7 @@ void EditorRunBar::update_profiler_autostart_indicator() { bool visual_profiler_active = EditorSettings::get_singleton()->get_project_metadata("debug_options", "autostart_visual_profiler", false); bool network_profiler_active = EditorSettings::get_singleton()->get_project_metadata("debug_options", "autostart_network_profiler", false); bool any_profiler_active = profiler_active | visual_profiler_active | network_profiler_active; + any_profiler_active &= !Engine::get_singleton()->is_recovery_mode_hint(); profiler_autostart_indicator->set_visible(any_profiler_active); if (any_profiler_active) { String tooltip = TTR("Autostart is enabled for the following profilers, which can have a performance impact:"); @@ -425,6 +487,42 @@ EditorRunBar::EditorRunBar() { main_hbox = memnew(HBoxContainer); main_panel->add_child(main_hbox); + if (Engine::get_singleton()->is_recovery_mode_hint()) { + recovery_mode_popup = memnew(AcceptDialog); + recovery_mode_popup->set_min_size(Size2(550, 70) * EDSCALE); + recovery_mode_popup->set_title(TTR("Recovery Mode")); + recovery_mode_popup->set_text( + TTR("Godot opened the project in Recovery Mode, which is a special mode that can help recover projects that crash the engine upon initialization. The following features have been temporarily disabled:") + + String::utf8("\n\n• ") + TTR("Tool scripts") + + String::utf8("\n• ") + TTR("Editor plugins") + + String::utf8("\n• ") + TTR("GDExtension addons") + + String::utf8("\n• ") + TTR("Automatic scene restoring") + + String::utf8("\n\n") + TTR("If 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.") + + String::utf8("\n\n") + TTR("To disable Recovery Mode, reload the project by pressing the Reload button next to the Recovery Mode banner, or by reopening the project normally.")); + recovery_mode_popup->set_autowrap(true); + add_child(recovery_mode_popup); + + recovery_mode_reload_button = memnew(Button); + main_hbox->add_child(recovery_mode_reload_button); + recovery_mode_reload_button->set_theme_type_variation("RunBarButton"); + recovery_mode_reload_button->set_focus_mode(Control::FOCUS_NONE); + recovery_mode_reload_button->set_tooltip_text(TTR("Disable recovery mode and reload the project.")); + recovery_mode_reload_button->connect(SceneStringName(pressed), callable_mp(this, &EditorRunBar::recovery_mode_reload_project)); + + recovery_mode_panel = memnew(PanelContainer); + main_hbox->add_child(recovery_mode_panel); + + recovery_mode_button = memnew(Button); + recovery_mode_panel->add_child(recovery_mode_button); + recovery_mode_button->set_theme_type_variation("RunBarButton"); + recovery_mode_button->set_focus_mode(Control::FOCUS_NONE); + recovery_mode_button->set_text(TTR("Recovery Mode")); + recovery_mode_button->set_tooltip_text(TTR("Recovery Mode is enabled. Click for more details.")); + recovery_mode_button->connect(SceneStringName(pressed), callable_mp(this, &EditorRunBar::recovery_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 b1e7e168473c..3892380c6f7d 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); @@ -58,6 +59,11 @@ class EditorRunBar : public MarginContainer { Button *profiler_autostart_indicator = nullptr; + PanelContainer *recovery_mode_panel = nullptr; + Button *recovery_mode_button = nullptr; + Button *recovery_mode_reload_button = nullptr; + AcceptDialog *recovery_mode_popup = nullptr; + Button *play_button = nullptr; Button *pause_button = nullptr; Button *stop_button = nullptr; @@ -95,6 +101,9 @@ class EditorRunBar : public MarginContainer { public: static EditorRunBar *get_singleton() { return singleton; } + void recovery_mode_show_dialog(); + void recovery_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 e5a7a53ffa7e..fbe679cd15c8 100644 --- a/editor/gui/scene_tree_editor.cpp +++ b/editor/gui/scene_tree_editor.cpp @@ -570,8 +570,13 @@ void SceneTreeEditor::_update_node(Node *p_node, TreeItem *p_item, bool p_part_o 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_recovery_mode_hint()) { + additional_notes += "\n" + TTR("This script can run in the editor.\nIt is currently disabled due to recovery 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 9d14ac83d31d..f3692ad40dfe 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_recovery_mode_hint(); #endif } diff --git a/editor/plugins/editor_plugin_settings.cpp b/editor/plugins/editor_plugin_settings.cpp index 0af9eba1fdba..a80eb2a2a148 100644 --- a/editor/plugins/editor_plugin_settings.cpp +++ b/editor/plugins/editor_plugin_settings.cpp @@ -37,6 +37,9 @@ #include "editor/editor_node.h" #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/texture_rect.h" #include "scene/gui/tree.h" void EditorPluginSettings::_notification(int p_what) { @@ -49,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_recovery_mode_hint()) { + recovery_mode_icon->set_texture(get_editor_theme_icon(SNAME("NodeWarning"))); + } + } break; } } @@ -204,6 +213,23 @@ EditorPluginSettings::EditorPluginSettings() { plugin_config_dialog->config(""); add_child(plugin_config_dialog); + if (Engine::get_singleton()->is_recovery_mode_hint()) { + HBoxContainer *c = memnew(HBoxContainer); + add_child(c); + + recovery_mode_icon = memnew(TextureRect); + recovery_mode_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED); + c->add_child(recovery_mode_icon); + + Label *recovery_mode_label = memnew(Label(TTR("Recovery mode is enabled. Enabled plugins will not run while this mode is active."))); + recovery_mode_label->set_theme_type_variation("HeaderSmall"); + recovery_mode_label->set_h_size_flags(SIZE_EXPAND_FILL); + c->add_child(recovery_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 f591022acd7f..e0fdb882a31f 100644 --- a/editor/plugins/editor_plugin_settings.h +++ b/editor/plugins/editor_plugin_settings.h @@ -33,6 +33,7 @@ #include "editor/plugins/plugin_config_dialog.h" +class TextureRect; class Tree; class EditorPluginSettings : public VBoxContainer { @@ -54,6 +55,7 @@ class EditorPluginSettings : public VBoxContainer { }; PluginConfigDialog *plugin_config_dialog = nullptr; + TextureRect *recovery_mode_icon = nullptr; Tree *plugin_list = nullptr; bool updating = false; diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 463a15e09a97..9a82ec879d5f 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -244,6 +244,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")); @@ -263,6 +264,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. @@ -495,6 +499,10 @@ void ProjectManager::_open_selected_projects() { args.push_back("--editor"); + if (open_in_recovery_mode) { + args.push_back("--recovery-mode"); + } + Error err = OS::get_singleton()->create_instance(args); if (err != OK) { loading_label->hide(); @@ -510,7 +518,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; @@ -599,6 +607,22 @@ void ProjectManager::_open_selected_projects_ask() { _open_selected_projects(); } +void ProjectManager::_open_selected_projects_check_recovery_mode() { + ProjectList::Item project = project_list->get_selected_projects()[0]; + if (project.missing) { + return; + } + + open_in_recovery_mode = false; + // Check if the project failed to load during last startup. + if (project.recovery_mode) { + _open_recovery_mode_ask(false); + return; + } + + _open_selected_projects_check_warnings(); +} + void ProjectManager::_open_selected_projects_with_migration() { #ifndef DISABLE_DEPRECATED if (project_list->get_selected_projects().size() == 1) { @@ -691,6 +715,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); @@ -698,6 +723,38 @@ 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_recovery_mode_ask(bool manual) { + String recovery_mode_details; + + // Only show the initial crash preamble if this popup wasn't manually triggered. + if (!manual) { + recovery_mode_details += + 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 Recovery Mode.") + + String::utf8("\n\n"); + } + + recovery_mode_details += + TTR("Recovery Mode is a special mode that may help to recover projects that crash the engine during initialization. This mode temporarily disables the following features:") + + String::utf8("\n\n• ") + TTR("Tool scripts") + + String::utf8("\n• ") + TTR("Editor plugins") + + String::utf8("\n• ") + TTR("GDExtension addons") + + String::utf8("\n• ") + TTR("Automatic scene restoring") + + String::utf8("\n\n") + TTR("This mode is intended only for basic editing to troubleshoot such issues, and therefore it will not be possible to run the project during this mode. It is also a good idea to make a backup of your project before proceeding.") + + String::utf8("\n\n") + TTR("Edit the project in Recovery Mode?"); + + open_recovery_mode_ask->set_text(recovery_mode_details); + open_recovery_mode_ask->popup_centered(Size2(550, 70) * EDSCALE); +} + void ProjectManager::_on_projects_updated() { Vector selected_projects = project_list->get_selected_projects(); int index = 0; @@ -711,6 +768,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 recovery mode. + _open_recovery_mode_ask(true); + break; + } +} + +void ProjectManager::_on_recovery_mode_popup_open_normal() { + open_recovery_mode_ask->hide(); + open_in_recovery_mode = false; + _open_selected_projects_check_warnings(); +} + +void ProjectManager::_on_recovery_mode_popup_open_recovery() { + open_in_recovery_mode = true; + _open_selected_projects_check_warnings(); +} + void ProjectManager::_on_project_created(const String &dir, bool edit) { project_list->add_project(dir, false); project_list->save_config(); @@ -723,7 +799,7 @@ void ProjectManager::_on_project_created(const String &dir, bool edit) { _update_list_placeholder(); if (edit) { - _open_selected_projects_ask(); + _open_selected_projects_check_warnings(); } project_list->update_dock_menu(); @@ -751,7 +827,7 @@ void ProjectManager::_on_search_term_submitted(const String &p_text) { return; } - _open_selected_projects_ask(); + _open_selected_projects_check_recovery_mode(); } LineEdit *ProjectManager::get_search_box() { @@ -968,7 +1044,7 @@ void ProjectManager::shortcut_input(const Ref &p_ev) { switch (k->get_keycode()) { case Key::ENTER: { - _open_selected_projects_ask(); + _open_selected_projects_check_recovery_mode(); } break; case Key::HOME: { if (project_list->get_project_count() > 0) { @@ -1322,7 +1398,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_recovery_mode)); // Empty project list placeholder. { @@ -1381,11 +1457,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", TTRC("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_recovery_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 recovery 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")); @@ -1501,6 +1596,14 @@ ProjectManager::ProjectManager() { multi_run_ask->get_ok_button()->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_run_project_confirm)); add_child(multi_run_ask); + open_recovery_mode_ask = memnew(ConfirmationDialog); + open_recovery_mode_ask->set_min_size(Size2(550, 70) * EDSCALE); + open_recovery_mode_ask->set_autowrap(true); + open_recovery_mode_ask->add_button(TTR("Edit normally"))->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_on_recovery_mode_popup_open_normal)); + open_recovery_mode_ask->set_ok_button_text(TTR("Edit in Recovery Mode")); + open_recovery_mode_ask->get_ok_button()->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_on_recovery_mode_popup_open_recovery)); + add_child(open_recovery_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 481fe7566df0..1cb963a65000 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_recovery_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_recovery_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_recovery_mode_ask(bool manual = false); void _on_project_created(const String &dir, bool edit); void _on_projects_updated(); + void _on_open_options_selected(int p_option); + void _on_recovery_mode_popup_open_normal(); + void _on_recovery_mode_popup_open_recovery(); 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_recovery_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 5b7c307a3f78..a4d3628d1793 100644 --- a/editor/project_manager/project_list.cpp +++ b/editor/project_manager/project_list.cpp @@ -468,14 +468,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 recovery_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(); } @@ -548,7 +550,29 @@ 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 OS::get_user_dir() because it attempts to load paths from the current loaded project through ProjectSettings, + // while here we're parsing project files externally. Therefore, we have to replicate its behavior. + String user_dir; + if (!cf_project_name.is_empty()) { + String appname = OS::get_singleton()->get_safe_dir_name(cf_project_name); + bool use_custom_dir = cf->get_value("application", "config/use_custom_user_dir", false); + if (use_custom_dir) { + String custom_dir = OS::get_singleton()->get_safe_dir_name(cf->get_value("application", "config/custom_user_dir_name", ""), true); + if (custom_dir.is_empty()) { + custom_dir = appname; + } + user_dir = custom_dir; + } else { + user_dir = OS::get_singleton()->get_godot_dir_name().path_join("app_userdata").path_join(appname); + } + } else { + user_dir = OS::get_singleton()->get_godot_dir_name().path_join("app_userdata").path_join("[unnamed project]"); + } + + String recovery_mode_lock_file = OS::get_singleton()->get_user_data_dir(user_dir).path_join(".recovery_mode_lock"); + recovery_mode = FileAccess::exists(recovery_mode_lock_file); + + return Item(project_name, description, project_version, tags, p_path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, recovery_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 7a77974d4aba..217173017bf4 100644 --- a/editor/project_manager/project_list.h +++ b/editor/project_manager/project_list.h @@ -116,6 +116,7 @@ class ProjectList : public ScrollContainer { bool favorite = false; bool grayed = false; bool missing = false; + bool recovery_mode = false; int version = 0; ProjectListItemControl *control = nullptr; @@ -134,6 +135,7 @@ class ProjectList : public ScrollContainer { bool p_favorite, bool p_grayed, bool p_missing, + bool p_recovery_mode, int p_version) { project_name = p_name; description = p_description; @@ -147,6 +149,7 @@ class ProjectList : public ScrollContainer { favorite = p_favorite; grayed = p_grayed; missing = p_missing; + recovery_mode = p_recovery_mode; version = p_version; control = nullptr; diff --git a/editor/register_editor_types.cpp b/editor/register_editor_types.cpp index e4c97f1783df..1dfe399a24a8 100644 --- a/editor/register_editor_types.cpp +++ b/editor/register_editor_types.cpp @@ -214,7 +214,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_recovery_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 cfe9f2724f29..8b36db3f3ca9 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -1945,6 +1945,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_recovery_mode = style_launch_pad->duplicate(); + style_launch_pad_recovery_mode->set_bg_color(p_config.accent_color * Color(1, 1, 1, 0.1)); + style_launch_pad_recovery_mode->set_border_color(p_config.warning_color); + style_launch_pad_recovery_mode->set_border_width_all(Math::round(2 * EDSCALE)); + p_theme->set_stylebox("LaunchPadRecoveryMode", EditorStringName(EditorStyles), style_launch_pad_recovery_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(); @@ -1970,6 +1975,17 @@ void EditorThemeManager::_populate_editor_styles(const Ref &p_theme p_theme->set_stylebox(CoreStringName(normal), "ProfilerAutostartIndicator", style_profiler_autostart); p_theme->set_stylebox(SceneStringName(pressed), "ProfilerAutostartIndicator", style_profiler_autostart); p_theme->set_stylebox("hover", "ProfilerAutostartIndicator", style_profiler_autostart); + + // Recovery mode button style + Ref style_recovery_mode_button = p_config.button_style_pressed->duplicate(); + style_recovery_mode_button->set_bg_color(p_config.warning_color); + style_recovery_mode_button->set_corner_radius_all(p_config.corner_radius * EDSCALE); + style_recovery_mode_button->set_content_margin_all(0); + // Recovery mode button is implicitly styled from the panel's background. + // So, remove any existing borders. (e.g. from draw_extra_borders config) + style_recovery_mode_button->set_border_width_all(0); + style_recovery_mode_button->set_expand_margin(SIDE_RIGHT, 2 * EDSCALE); + p_theme->set_stylebox("RecoveryModeButton", EditorStringName(EditorStyles), style_recovery_mode_button); } // Standard GUI variations. diff --git a/main/main.cpp b/main/main.cpp index bab0dfebaf21..b5853cd8173e 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -199,6 +199,7 @@ static uint64_t quit_after = 0; static OS::ProcessID editor_pid = 0; #ifdef TOOLS_ENABLED static bool found_project = false; +static bool recovery_mode = false; static bool auto_build_solutions = false; static String debug_server_uri; static bool wait_for_import = false; @@ -551,6 +552,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("--recovery-mode", "Start the editor in recovery mode, which disables features that can typically 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) @@ -1435,6 +1437,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 == "--recovery-mode") { // Enables recovery mode. + recovery_mode = true; } else if (arg == "--debug-server") { if (N) { debug_server_uri = N->get(); @@ -1904,6 +1908,9 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph 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(); + main_args.push_back("--editor"); if (!init_windowed && !init_fullscreen) { init_maximized = true; @@ -1919,6 +1926,15 @@ 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 (recovery_mode) { + if (project_manager || !editor) { + OS::get_singleton()->print("Error: Recovery mode can only be used in the editor. Aborting.\n"); + goto error; + } + + Engine::get_singleton()->set_recovery_mode_hint(true); + } #endif OS::get_singleton()->set_cmdline(execpath, main_args, user_args); @@ -2706,6 +2722,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) { @@ -3620,6 +3640,8 @@ int Main::start() { editor = true; } else if (E->get() == "-p" || E->get() == "--project-manager") { project_manager = true; + } else if (E->get() == "--recovery-mode") { + recovery_mode = true; } else if (E->get() == "--install-android-build-template") { install_android_build_template = true; #endif // TOOLS_ENABLED @@ -4213,7 +4235,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 (!recovery_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"); @@ -4296,6 +4318,10 @@ int Main::start() { Crypto::load_default_certificates( EditorSettings::get_singleton()->get_setting("network/tls/editor_tls_certificates").operator String()); } + + if (recovery_mode) { + Engine::get_singleton()->set_recovery_mode_hint(true); + } #endif } diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index c842c0a99b3e..05fb4ac920ae 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -254,7 +254,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_recovery_mode_hint(); #else return valid; #endif diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 27499bc29606..e9a821c65a95 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_recovery_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 b091fdbd2bd7..941995c74bd0 100644 --- a/platform/android/java_godot_io_wrapper.cpp +++ b/platform/android/java_godot_io_wrapper.cpp @@ -118,7 +118,7 @@ String GodotIOJavaWrapper::get_temp_dir() { } } -String GodotIOJavaWrapper::get_user_data_dir() { +String GodotIOJavaWrapper::get_user_data_dir(const String &p_user_dir) { 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 4cb860cb1eeb..e5134426aa27 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -73,7 +73,7 @@ class GodotIOJavaWrapper { Error open_uri(const String &p_uri); String get_cache_dir(); String get_temp_dir(); - String get_user_data_dir(); + String get_user_data_dir(const String &p_user_dir); 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 0d6e73781899..c3162290134a 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() const { @@ -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_user_dir) 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_user_dir); if (!data_dir.is_empty()) { data_dir_cache = _remove_symlink(data_dir); return data_dir_cache; @@ -764,7 +764,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) { @@ -897,7 +897,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 bcef31fbbf54..108a58a2b02a 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -146,7 +146,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_user_dir) const override; virtual String get_data_path() const override; virtual String get_cache_path() const override; virtual String get_temp_path() const override; diff --git a/platform/ios/os_ios.h b/platform/ios/os_ios.h index e83a54abec28..cd688077aca1 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_user_dir) const override; virtual String get_cache_path() const override; virtual String get_temp_path() const override; diff --git a/platform/ios/os_ios.mm b/platform/ios/os_ios.mm index a94eda4e0307..88e19a0a1cbe 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_user_dir) 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 5224b45cb089..ce7c4a4ccfff 100644 --- a/platform/web/os_web.cpp +++ b/platform/web/os_web.cpp @@ -182,23 +182,9 @@ 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_user_dir) const { String userfs = "/userfs"; - String appname = get_safe_dir_name(GLOBAL_GET("application/config/name")); - if (!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; - } - 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("[unnamed project]"); + return userfs.path_join(p_user_dir).replace("\\", "/"); } String OS_Web::get_cache_path() const { diff --git a/platform/web/os_web.h b/platform/web/os_web.h index 8e3f78de5c44..786f7fe98e87 100644 --- a/platform/web/os_web.h +++ b/platform/web/os_web.h @@ -107,7 +107,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_user_dir) const override; bool is_userfs_persistent() const override; diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index d2bc94473ee6..0352a8314a3d 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -2214,22 +2214,8 @@ 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()) { - 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; - } - 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("[unnamed project]"); +String OS_Windows::get_user_data_dir(const String &p_user_dir) const { + return get_data_path().path_join(p_user_dir).replace("\\", "/"); } String OS_Windows::get_unique_id() const { diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h index dc5d80933cde..9f5eef496247 100644 --- a/platform/windows/os_windows.h +++ b/platform/windows/os_windows.h @@ -231,7 +231,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_user_dir) const override; virtual String get_unique_id() const override;