diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index af2e3530ed3..2a8925532ba 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -42,6 +42,8 @@ #include "sdk/integration.hpp" #include "sprite/sprite_manager.hpp" #include "supertux/game_manager.hpp" +#include "supertux/gameconfig.hpp" +#include "supertux/globals.hpp" #include "supertux/level.hpp" #include "supertux/level_parser.hpp" #include "supertux/menu/menu_storage.hpp" @@ -54,7 +56,6 @@ #include "supertux/world.hpp" #include "util/file_system.hpp" #include "util/reader_mapping.hpp" -#include "util/string_util.hpp" #include "video/compositor.hpp" #include "video/drawing_context.hpp" #include "video/surface.hpp" @@ -78,7 +79,7 @@ Editor::Editor() : m_level(), m_world(), m_levelfile(), - m_test_levelfile(), + m_autosave_levelfile(), m_quit_request(false), m_newlevel_request(false), m_reload_request(false), @@ -100,7 +101,8 @@ Editor::Editor() : m_bgr_surface(Surface::from_file("images/background/antarctic/arctis2.png")), m_undo_manager(new UndoManager), m_ignore_sector_change(false), - m_level_first_loaded(false) + m_level_first_loaded(false), + m_time_since_last_save(0.f) { auto toolbox_widget = std::make_unique(*this); auto layers_widget = std::make_unique(*this); @@ -152,6 +154,25 @@ Editor::draw(Compositor& compositor) void Editor::update(float dt_sec, const Controller& controller) { + // Auto-save (interval) + if (m_level) { + m_time_since_last_save += dt_sec; + if (m_time_since_last_save >= static_cast(std::max( + g_config->editor_autosave_frequency, 1)) * 60.f) { + m_time_since_last_save = 0.f; + std::string backup_filename = get_autosave_from_levelname(m_levelfile); + std::string directory = get_level_directory(); + + // Set the test level file even though we're not testing, so that + // if the user quits the editor without ever testing, it'll delete + // the autosave file anyways + m_autosave_levelfile = FileSystem::join(directory, backup_filename); + m_level->save(m_autosave_levelfile); + } + } else { + m_time_since_last_save = 0.f; + } + // Pass all requests if (m_reload_request) { reload_level(); @@ -209,12 +230,31 @@ Editor::update(float dt_sec, const Controller& controller) } } +void +Editor::remove_autosave_file() +{ + // Clear the auto-save file + if (!m_autosave_levelfile.empty()) + { + // Try to remove the test level using the PhysFS file system + if (physfsutil::remove(m_autosave_levelfile) != 0) + { + // This file is not inside any PhysFS mounts, + // try to remove this using normal file system + // methods. + FileSystem::remove(m_autosave_levelfile); + } + } +} + void Editor::save_level() { m_undo_manager->reset_index(); m_level->save(m_world ? FileSystem::join(m_world->get_basedir(), m_levelfile) : m_levelfile); + m_time_since_last_save = 0.f; + remove_autosave_file(); } std::string @@ -242,7 +282,7 @@ Editor::test_level(const boost::optional>& test_p Tile::draw_editor_images = false; Compositor::s_render_lighting = true; - std::string backup_filename = m_levelfile + "~"; + std::string backup_filename = get_autosave_from_levelname(m_levelfile); std::string directory = get_level_directory(); // This is jank to get an owned World pointer, GameManager/World @@ -254,8 +294,10 @@ Editor::test_level(const boost::optional>& test_p current_world = owned_world.get(); } - m_test_levelfile = FileSystem::join(directory, backup_filename); - m_level->save(m_test_levelfile); + m_autosave_levelfile = FileSystem::join(directory, backup_filename); + m_level->save(m_autosave_levelfile); + m_time_since_last_save = 0.f; + if (!m_level->is_worldmap()) { Integration::set_level(m_level->get_name().c_str()); @@ -266,7 +308,7 @@ Editor::test_level(const boost::optional>& test_p { Integration::set_worldmap(m_level->get_name().c_str()); Integration::set_status(TESTING_WORLDMAP); - GameManager::current()->start_worldmap(*current_world, "", m_test_levelfile); + GameManager::current()->start_worldmap(*current_world, "", m_autosave_levelfile); } m_leveltested = true; @@ -472,6 +514,12 @@ Editor::reload_level() StringUtil::has_suffix(m_levelfile, ".stwm"), true)); ReaderMapping::s_translations_enabled = true; + + // Autosave files : Once the level is loaded, make sure + // to use the regular file + m_levelfile = get_levelname_from_autosave(m_levelfile); + m_autosave_levelfile = FileSystem::join(get_level_directory(), + get_autosave_from_levelname(m_levelfile)); } void @@ -481,6 +529,8 @@ Editor::quit_editor() auto quit = [this] () { + remove_autosave_file(); + //Quit level editor m_world = nullptr; m_levelfile = ""; @@ -573,17 +623,6 @@ Editor::setup() // Reactivate the editor after level test if (m_leveltested) { - if (!m_test_levelfile.empty()) - { - // Try to remove the test level using the PhysFS file system - if (physfsutil::remove(m_test_levelfile) != 0) - { - // This file is not inside any PhysFS mounts, - // try to remove this using normal file system - // methods. - FileSystem::remove(m_test_levelfile); - } - } m_leveltested = false; Tile::draw_editor_images = true; m_level->reactivate(); diff --git a/src/editor/editor.hpp b/src/editor/editor.hpp index 9433694912b..37ae2687d1b 100644 --- a/src/editor/editor.hpp +++ b/src/editor/editor.hpp @@ -30,6 +30,7 @@ #include "util/currenton.hpp" #include "util/file_system.hpp" #include "util/log.hpp" +#include "util/string_util.hpp" #include "video/surface_ptr.hpp" class GameObject; @@ -48,6 +49,17 @@ class Editor final : public Screen, public: static bool is_active(); +private: + static bool is_autosave_file(const std::string& filename) { + return StringUtil::has_suffix(filename, "~"); + } + static std::string get_levelname_from_autosave(const std::string& filename) { + return is_autosave_file(filename) ? filename.substr(0, filename.size() - 1) : filename; + } + static std::string get_autosave_from_levelname(const std::string& filename) { + return is_autosave_file(filename) ? filename : filename + "~"; + } + public: static bool s_resaving_in_progress; @@ -93,6 +105,8 @@ class Editor final : public Screen, bool is_testing_level() const { return m_leveltested; } + void remove_autosave_file(); + /** Checks whether the level can be saved and does not contain obvious issues (currently: check if main sector and a spawn point named "main" is present) */ @@ -145,7 +159,7 @@ class Editor final : public Screen, std::unique_ptr m_world; std::string m_levelfile; - std::string m_test_levelfile; + std::string m_autosave_levelfile; public: bool m_quit_request; @@ -179,6 +193,8 @@ class Editor final : public Screen, bool m_ignore_sector_change; bool m_level_first_loaded; + + float m_time_since_last_save; private: Editor(const Editor&) = delete; diff --git a/src/gui/dialog.cpp b/src/gui/dialog.cpp index cd70224c9fd..31503c02d93 100644 --- a/src/gui/dialog.cpp +++ b/src/gui/dialog.cpp @@ -29,12 +29,13 @@ #include "video/video_system.hpp" #include "video/viewport.hpp" -Dialog::Dialog(bool passive) : +Dialog::Dialog(bool passive, bool auto_clear_dialogs) : m_text(), m_buttons(), m_selected_button(), m_cancel_button(-1), m_passive(passive), + m_clear_diags(auto_clear_dialogs), m_text_size() { } @@ -258,7 +259,10 @@ Dialog::on_button_click(int button) const { m_buttons[button].callback(); } - MenuManager::instance().set_dialog({}); + if (m_clear_diags || button == m_cancel_button) + { + MenuManager::instance().set_dialog({}); + } } /* EOF */ diff --git a/src/gui/dialog.hpp b/src/gui/dialog.hpp index e976217557e..1960d6b47cb 100644 --- a/src/gui/dialog.hpp +++ b/src/gui/dialog.hpp @@ -43,11 +43,12 @@ class Dialog int m_selected_button; int m_cancel_button; bool m_passive; + bool m_clear_diags; Sizef m_text_size; public: - Dialog(bool passive = false); + Dialog(bool passive = false, bool auto_clear_dialogs = true); virtual ~Dialog(); void set_text(const std::string& text); diff --git a/src/supertux/gameconfig.cpp b/src/supertux/gameconfig.cpp index bb5cf2241f5..4846233ee61 100644 --- a/src/supertux/gameconfig.cpp +++ b/src/supertux/gameconfig.cpp @@ -61,6 +61,7 @@ Config::Config() : enable_discord(false), discord_hide_editor(false), #endif + editor_autosave_frequency(5), repository_url() { } @@ -93,6 +94,8 @@ Config::load() #endif } + config_mapping.get("editor_autosave_frequency", editor_autosave_frequency); + EditorOverlayWidget::autotile_help = !developer_mode; if (is_christmas()) { @@ -209,7 +212,9 @@ Config::save() #endif } writer.end_list("integrations"); - + + writer.write("editor_autosave_frequency", editor_autosave_frequency); + if (is_christmas()) { writer.write("christmas", christmas_mode); } diff --git a/src/supertux/gameconfig.hpp b/src/supertux/gameconfig.hpp index 052aedcb57e..86b5b3387ca 100644 --- a/src/supertux/gameconfig.hpp +++ b/src/supertux/gameconfig.hpp @@ -103,6 +103,8 @@ class Config final bool discord_hide_editor; #endif + int editor_autosave_frequency; + std::string repository_url; bool is_christmas() const { diff --git a/src/supertux/level.cpp b/src/supertux/level.cpp index 00fd529f54f..a3efe5f7475 100644 --- a/src/supertux/level.cpp +++ b/src/supertux/level.cpp @@ -29,6 +29,8 @@ #include #include +#include + Level* Level::s_current = nullptr; Level::Level(bool worldmap) : @@ -89,7 +91,9 @@ Level::save(const std::string& filepath, bool retry) Writer writer(filepath); save(writer); - log_warning << "Level saved as " << filepath << "." << std::endl; + log_warning << "Level saved as " << filepath << "." + << (boost::algorithm::ends_with(filepath, "~") ? " [Autosave]" : "") + << std::endl; } catch(std::exception& e) { if (retry) { std::stringstream msg; diff --git a/src/supertux/menu/editor_level_select_menu.cpp b/src/supertux/menu/editor_level_select_menu.cpp index 26a2388868b..b5ed2cdbeb4 100644 --- a/src/supertux/menu/editor_level_select_menu.cpp +++ b/src/supertux/menu/editor_level_select_menu.cpp @@ -134,6 +134,14 @@ EditorLevelSelectMenu::create_item(bool worldmap) } } +void +EditorLevelSelectMenu::open_level(const std::string& filename) +{ + auto editor = Editor::current(); + editor->set_level(filename); + MenuManager::instance().clear_menu_stack(); +} + void EditorLevelSelectMenu::menu_action(MenuItem& item) { @@ -141,9 +149,29 @@ EditorLevelSelectMenu::menu_action(MenuItem& item) World* world = editor->get_world(); if (item.get_id() >= 0) { - editor->set_level(m_levelset->get_level_filename(item.get_id())); - MenuManager::instance().clear_menu_stack(); + std::string file_name = m_levelset->get_level_filename(item.get_id()); + std::string file_name_full = FileSystem::join(editor->get_level_directory(), file_name); + + if (PHYSFS_exists((file_name_full + "~").c_str())) { + auto dialog = std::make_unique(/* passive = */ false, /* auto_clear_dialogs = */ false); + dialog->set_text(_("An auto-save recovery file was found. Would you like to restore the recovery\nfile and resume where you were before the editor crashed?")); + dialog->clear_buttons(); + dialog->add_default_button(_("Yes"), [this, file_name] { + open_level(file_name + "~"); + MenuManager::instance().set_dialog({}); + }); + dialog->add_button(_("No"), [this, file_name] { + Dialog::show_confirmation(_("This will delete the auto-save file. Are you sure?"), [this, file_name] { + open_level(file_name); + }); + }); + dialog->add_cancel_button(_("Cancel")); + MenuManager::instance().set_dialog(std::move(dialog)); + } else { + open_level(file_name); + } + } else { switch (item.get_id()) { case -1: diff --git a/src/supertux/menu/editor_level_select_menu.hpp b/src/supertux/menu/editor_level_select_menu.hpp index 7227b36d0ba..1724e719dbf 100644 --- a/src/supertux/menu/editor_level_select_menu.hpp +++ b/src/supertux/menu/editor_level_select_menu.hpp @@ -34,6 +34,8 @@ class EditorLevelSelectMenu final : public Menu void menu_action(MenuItem& item) override; + void open_level(const std::string& filename); + private: void initialize(); void create_level(); diff --git a/src/supertux/menu/editor_menu.cpp b/src/supertux/menu/editor_menu.cpp index 28b02e210e6..c18e3d0f06c 100644 --- a/src/supertux/menu/editor_menu.cpp +++ b/src/supertux/menu/editor_menu.cpp @@ -22,6 +22,7 @@ #include "gui/menu_manager.hpp" #include "supertux/level.hpp" #include "supertux/gameconfig.hpp" +#include "supertux/globals.hpp" #include "supertux/menu/menu_storage.hpp" #include "util/gettext.hpp" #include "video/compositor.hpp" @@ -68,6 +69,7 @@ EditorMenu::EditorMenu() add_toggle(-1, _("Render Light"), &Compositor::s_render_lighting); add_toggle(-1, _("Autotile Mode"), &EditorOverlayWidget::autotile_mode); add_toggle(-1, _("Enable Autotile Help"), &EditorOverlayWidget::autotile_help); + add_intfield(_("Autosave Frequency"), &(g_config->editor_autosave_frequency)); add_submenu(worldmap ? _("Worldmap Settings") : _("Level Settings"), MenuStorage::EDITOR_LEVEL_MENU);