Add other versions of Wesnoth to the Load Game dialog

If a save directory from another version is found, a drop-down list of
directories is added below the filter box in the Load Game dialog.

Refactor save_index_class to allow instances for other directories.

Design idea: all new saves go in to the main save directory. Games can be
loaded from other directories, but those directories are treated as read-only.
When building an index for other directories, the index is kept in memory and
not written to disk - this results in some noticeable lag in the UI when
opening the other versions' directories, but not the usual save dir.
This commit is contained in:
Steve Cotton 2019-10-27 06:57:45 +01:00
parent 22acd36155
commit b66bdb8a16
16 changed files with 402 additions and 128 deletions

View file

@ -19,6 +19,7 @@
* Add mushroom defense cap to mounted and some flying units
* Dwarvish Lord and Steelclad: reduce hitpoints by 3 and reduce impact and pierce resistance to 20%
### User interface
* The load-game dialog can now see the directories used by Wesnoth 1.14, 1.12, etc.
### Lua API
### WML engine
### Packaging

View file

@ -412,15 +412,42 @@
grow_factor = 0
[column]
border = "all"
border_size = 5
horizontal_alignment = "left"
[text_box]
id = "txtFilter"
definition = "default"
{FILTER_TEXT_BOX_HINT}
[/text_box]
[grid]
[row]
[column]
border = "all"
border_size = 5
horizontal_alignment = "left"
[text_box]
id = "txtFilter"
definition = "default"
{FILTER_TEXT_BOX_HINT}
[/text_box]
[/column]
[column]
border = "all"
border_size = 5
horizontal_alignment = "left"
[menu_button]
id = "dirList"
definition = "default"
tooltip = _ "Show saves from a different version of Wesnoth"
[/menu_button]
[/column]
[/row]
[/grid]
[/column]

View file

@ -23,11 +23,11 @@
#include "config.hpp"
#include "deprecation.hpp"
#include "game_config.hpp"
#include "game_version.hpp"
#include "gettext.hpp"
#include "log.hpp"
#include "serialization/unicode.hpp"
#include "serialization/unicode_cast.hpp"
#include "game_version.hpp"
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>
@ -513,6 +513,13 @@ std::string get_next_filename(const std::string& name, const std::string& extens
static bfs::path user_data_dir, user_config_dir, cache_dir;
static const std::string get_version_path_suffix(const version_info& version)
{
std::ostringstream s;
s << version.major_version() << '.' << version.minor_version();
return s.str();
}
static const std::string& get_version_path_suffix()
{
static std::string suffix;
@ -521,9 +528,7 @@ static const std::string& get_version_path_suffix()
// the version number cannot change during runtime.
if(suffix.empty()) {
std::ostringstream s;
s << game_config::wesnoth_version.major_version() << '.' << game_config::wesnoth_version.minor_version();
suffix = s.str();
suffix = get_version_path_suffix(game_config::wesnoth_version);
}
return suffix;
@ -817,6 +822,36 @@ std::string get_cache_dir()
return cache_dir.string();
}
std::vector<other_version_dir> find_other_version_saves_dirs()
{
const auto& w_ver = game_config::wesnoth_version;
const auto& ms_ver = game_config::min_savegame_version;
if(w_ver.major_version() != 1 || ms_ver.major_version() != 1) {
// Unimplemented, assuming that version 2 won't use WML-based saves
return {};
}
std::vector<other_version_dir> result;
// For 1.16, check for saves from all versions up to 1.20.
for(auto minor = ms_ver.minor_version(); minor <= w_ver.minor_version() + 4; ++minor) {
if(minor == w_ver.minor_version())
continue;
auto version = version_info{};
version.set_major_version(w_ver.major_version());
version.set_minor_version(minor);
auto suffix = get_version_path_suffix(version);
auto path = get_user_data_path().parent_path() / suffix / "saves";
if(bfs::exists(path)) {
result.emplace_back(suffix, path.generic_string());
}
}
return result;
}
std::string get_cwd()
{
error_code ec;

View file

@ -23,9 +23,9 @@
#include <ctime>
#include <functional>
#include <iosfwd>
#include <memory>
#include <string>
#include <vector>
#include <memory>
#include "exceptions.hpp"
#include "serialization/string_utils.hpp"
@ -172,19 +172,46 @@ std::string get_user_config_dir();
std::string get_user_data_dir();
std::string get_cache_dir();
struct other_version_dir
{
/**
* Here the version is given as a string instead of a version_info, because the
* logic of how many components are significant ("1.16" rather than
* "1.16.0") is encapsulated in find_other_version_saves_dirs().
*/
std::string version;
std::string path;
// constructor because emplace_back() doesn't use aggregate initialization
other_version_dir(const std::string& v, const std::string& p)
: version(v)
, path(p)
{
}
};
/**
* Searches for directories containing saves created by other versions of Wesnoth.
*
* The directories returned will exist, but might not contain any saves. Changes to
* the filesystem (by running other versions or by deleting old directories) may
* change the results returned by the function.
*/
std::vector<other_version_dir> find_other_version_saves_dirs();
std::string get_cwd();
std::string get_exe_dir();
bool make_directory(const std::string& dirname);
bool delete_directory(const std::string& dirname, const bool keep_pbl = false);
bool delete_file(const std::string &filename);
bool delete_file(const std::string& filename);
bool looks_like_pbl(const std::string& file);
// Basic disk I/O:
/** Basic disk I/O - read file. */
std::string read_file(const std::string &fname);
std::string read_file(const std::string& fname);
filesystem::scoped_istream istream_file(const std::string& fname, bool treat_failure_as_error = true);
filesystem::scoped_ostream ostream_file(const std::string& fname, bool create_directory = true);
/** Throws io_exception if an error occurs. */

View file

@ -50,6 +50,7 @@
#include "preferences/general.hpp" // for disable_preferences_save, etc
#include "preferences/display.hpp"
#include "savegame.hpp" // for clean_saves, etc
#include "save_index.hpp"
#include "scripting/application_lua_kernel.hpp"
#include "sdl/surface.hpp" // for surface
#include "serialization/compression.hpp" // for format::NONE
@ -185,14 +186,14 @@ game_launcher::game_launcher(const commandline_options& cmdline_opts, const char
{
jump_to_editor_ = true;
if (!cmdline_opts_.editor->empty())
load_data_.reset(new savegame::load_game_metadata{ *cmdline_opts_.editor });
load_data_.reset(new savegame::load_game_metadata{ savegame::save_index_class::default_saves_dir(), *cmdline_opts_.editor });
}
if (cmdline_opts_.fps)
preferences::set_show_fps(true);
if (cmdline_opts_.fullscreen)
video().set_fullscreen(true);
if (cmdline_opts_.load)
load_data_.reset(new savegame::load_game_metadata{ *cmdline_opts_.load });
load_data_.reset(new savegame::load_game_metadata{ savegame::save_index_class::default_saves_dir(), *cmdline_opts_.load });
if (cmdline_opts_.max_fps) {
int fps = utils::clamp(*cmdline_opts_.max_fps, 1, 1000);
fps = 1000 / fps;
@ -522,7 +523,7 @@ int game_launcher::unit_test()
savegame::replay_savegame save(state_, compression::NONE);
save.save_game_automatic(false, "unit_test_replay"); //false means don't check for overwrite
load_data_.reset(new savegame::load_game_metadata{ "unit_test_replay" , "", true, true, false });
load_data_.reset(new savegame::load_game_metadata{ savegame::save_index_class::default_saves_dir(), "unit_test_replay" , "", true, true, false });
if (!load_game()) {
std::cerr << "Failed to load the replay!" << std::endl;
@ -601,7 +602,7 @@ bool game_launcher::load_game()
DBG_GENERAL << "Current campaign type: " << state_.classification().campaign_type << std::endl;
savegame::loadgame load(game_config_manager::get()->game_config(), state_);
savegame::loadgame load(savegame::save_index_class::default_saves_dir(), game_config_manager::get()->game_config(), state_);
if (load_data_) {
std::unique_ptr<savegame::load_game_metadata> load_data = std::move(load_data_);
load.data() = std::move(*load_data);

View file

@ -20,10 +20,9 @@
#include "filesystem.hpp"
#include "formatter.hpp"
#include "formula/string_utils.hpp"
#include "gettext.hpp"
#include "game_config.hpp"
#include "preferences/game.hpp"
#include "game_classification.hpp"
#include "game_config.hpp"
#include "gettext.hpp"
#include "gui/auxiliary/field.hpp"
#include "gui/core/log.hpp"
#include "gui/dialogs/game_delete.hpp"
@ -32,18 +31,19 @@
#include "gui/widgets/label.hpp"
#include "gui/widgets/listbox.hpp"
#include "gui/widgets/minimap.hpp"
#include "gui/widgets/settings.hpp"
#include "gui/widgets/scroll_label.hpp"
#include "gui/widgets/settings.hpp"
#include "gui/widgets/text_box.hpp"
#include "gui/widgets/toggle_button.hpp"
#include "gui/widgets/window.hpp"
#include "picture.hpp"
#include "language.hpp"
#include "picture.hpp"
#include "preferences/game.hpp"
#include "serialization/string_utils.hpp"
#include "utils/general.hpp"
#include "utils/functional.hpp"
#include <cctype>
#include "utils/functional.hpp"
static lg::log_domain log_gameloaddlg{"gui/dialogs/game_load_dialog"};
#define ERR_GAMELOADDLG LOG_STREAM(err, log_gameloaddlg)
@ -69,6 +69,9 @@ namespace dialogs
* txtFilter & & text & m &
* The filter for the listbox items. $
*
* dirList & & menu_button & m &
* Allows changing directory to the directories for old versions of Wesnoth. $
*
* savegame_list & & listbox & m &
* List of savegames. $
*
@ -90,6 +93,9 @@ namespace dialogs
* -lblSummary & & label & m &
* Summary of the selected savegame. $
*
* delete & & button & m &
* Delete the selected savegame. $
*
* @end{table}
*/
@ -97,11 +103,12 @@ REGISTER_DIALOG(game_load)
game_load::game_load(const config& cache_config, savegame::load_game_metadata& data)
: filename_(data.filename)
, save_index_manager_(data.manager)
, change_difficulty_(register_bool("change_difficulty", true, data.select_difficulty))
, show_replay_(register_bool("show_replay", true, data.show_replay))
, cancel_orders_(register_bool("cancel_orders", true, data.cancel_orders))
, summary_(data.summary)
, games_({savegame::get_saves_list()})
, games_()
, cache_config_(cache_config)
, last_words_()
{
@ -116,19 +123,76 @@ void game_load::pre_show(window& window)
text_box* filter = find_widget<text_box>(&window, "txtFilter", false, true);
filter->set_text_changed_callback(
std::bind(&game_load::filter_text_changed, this, _1, _2));
filter->set_text_changed_callback(std::bind(&game_load::filter_text_changed, this, _1, _2));
listbox& list = find_widget<listbox>(&window, "savegame_list", false);
connect_signal_notify_modified(list,
std::bind(&game_load::display_savegame, this, std::ref(window)));
connect_signal_notify_modified(list, std::bind(&game_load::display_savegame, this, std::ref(window)));
window.keyboard_capture(filter);
window.add_to_keyboard_chain(&list);
list.register_sorting_option(0, [this](const int i) { return games_[i].name(); });
list.register_sorting_option(1, [this](const int i) { return games_[i].modified(); });
populate_game_list(window);
connect_signal_mouse_left_click(find_widget<button>(&window, "delete", false),
std::bind(&game_load::delete_button_callback, this, std::ref(window)));
connect_signal_mouse_left_click(find_widget<button>(&window, "browse_saves_folder", false),
std::bind(&game_load::browse_button_callback, this));
menu_button& dir_list = find_widget<menu_button>(&window, "dirList", false);
dir_list.set_use_markup(true);
dir_list.set_active(true);
set_save_dir_list(dir_list);
connect_signal_notify_modified(dir_list, std::bind(&game_load::handle_dir_select, this, std::ref(window)));
display_savegame(window);
}
void game_load::set_save_dir_list(menu_button& dir_list)
{
const auto other_dirs = filesystem::find_other_version_saves_dirs();
if(other_dirs.empty()) {
dir_list.set_visible(widget::visibility::invisible);
return;
}
std::vector<config> options;
// The first option in the list is the current version's save dir
{
config option;
option["label"] = _("Normal saves directory");
option["path"] = "";
options.push_back(std::move(option));
}
for(const auto& known_dir : filesystem::find_other_version_saves_dirs()) {
if(!known_dir.path.empty()) {
config option;
option["label"] = known_dir.version;
option["path"] = known_dir.path;
option["details"] = formatter() << "<span color='#777777'>(" << known_dir.path << ")</span>";
options.push_back(std::move(option));
}
}
dir_list.set_values(options, 0);
dir_list.set_visible(widget::visibility::visible);
}
void game_load::populate_game_list(window& window)
{
listbox& list = find_widget<listbox>(&window, "savegame_list", false);
list.clear();
games_ = save_index_manager_->get_saves_list();
for(const auto& game : games_) {
std::map<std::string, string_map> data;
string_map item;
@ -144,19 +208,7 @@ void game_load::pre_show(window& window)
list.add_row(data);
}
list.register_sorting_option(0, [this](const int i) { return games_[i].name(); });
list.register_sorting_option(1, [this](const int i) { return games_[i].modified(); });
connect_signal_mouse_left_click(
find_widget<button>(&window, "delete", false),
std::bind(&game_load::delete_button_callback,
this, std::ref(window)));
connect_signal_mouse_left_click(
find_widget<button>(&window, "browse_saves_folder", false),
std::bind(&desktop::open_object, filesystem::get_saves_dir()));
display_savegame(window);
find_widget<button>(&window, "delete", false).set_active(!save_index_manager_->read_only());
}
void game_load::display_savegame_internal(window& window)
@ -432,6 +484,10 @@ void game_load::evaluate_summary_string(std::stringstream& str, const config& cf
}
}
}
void game_load::browse_button_callback()
{
desktop::open_object(save_index_manager_->dir());
}
void game_load::delete_button_callback(window& window)
{
@ -448,7 +504,7 @@ void game_load::delete_button_callback(window& window)
}
// Delete the file
savegame::delete_game(games_[index].name());
save_index_manager_->delete_game(games_[index].name());
// Remove it from the list of saves
games_.erase(games_.begin() + index);
@ -484,5 +540,20 @@ void game_load::key_press_callback(window& window, const SDL_Keycode key)
}
}
void game_load::handle_dir_select(window& window)
{
menu_button& dir_list = find_widget<menu_button>(&window, "dirList", false);
const auto& path = dir_list.get_value_config()["path"].str();
if(path.empty()) {
save_index_manager_ = savegame::save_index_class::default_saves_dir();
} else {
save_index_manager_ = std::make_shared<savegame::save_index_class>(path);
}
populate_game_list(window);
display_savegame(window);
}
} // namespace dialogs
} // namespace gui2

View file

@ -14,11 +14,12 @@
#pragma once
#include "gettext.hpp"
#include "gui/dialogs/modal_dialog.hpp"
#include "gui/dialogs/transient_message.hpp"
#include "savegame.hpp"
#include "gui/widgets/menu_button.hpp"
#include "save_index.hpp"
#include "gettext.hpp"
#include "savegame.hpp"
#include <SDL2/SDL_keycode.h>
@ -28,7 +29,6 @@ class text_box_base;
namespace dialogs
{
class game_load : public modal_dialog
{
public:
@ -36,11 +36,6 @@ public:
static bool execute(const config& cache_config, savegame::load_game_metadata& data)
{
if(savegame::get_saves_list().empty()) {
gui2::show_transient_message(_("No Saved Games"), _("There are no save files to load"));
return false;
}
return game_load(cache_config, data).show();
}
@ -51,8 +46,15 @@ private:
/** Inherited from modal_dialog, implemented by REGISTER_DIALOG. */
virtual const std::string& window_id() const override;
void set_save_dir_list(menu_button& dir_list);
/** Update (both internally and visually) the list of games. */
void populate_game_list(window& window);
void filter_text_changed(text_box_base* textbox, const std::string& text);
void browse_button_callback();
void delete_button_callback(window& window);
void handle_dir_select(window& window);
void display_savegame_internal(window& window);
void display_savegame(window& window);
@ -61,6 +63,7 @@ private:
void key_press_callback(window& window, const SDL_Keycode key);
std::string& filename_;
std::shared_ptr<savegame::save_index_class>& save_index_manager_;
field_bool* change_difficulty_;
field_bool* show_replay_;

View file

@ -17,6 +17,9 @@
#include "gui/dialogs/multiplayer/mp_create_game.hpp"
#include "filesystem.hpp"
#include "formatter.hpp"
#include "formula/string_utils.hpp"
#include "game_config.hpp"
#include "game_config_manager.hpp"
#include "game_initialization/lobby_data.hpp"
#include "gettext.hpp"
@ -27,12 +30,8 @@
#include "gui/widgets/button.hpp"
#include "gui/widgets/image.hpp"
#include "gui/widgets/integer_selector.hpp"
#include "gui/widgets/menu_button.hpp"
#include "preferences/game.hpp"
#include "gui/widgets/listbox.hpp"
#include "formatter.hpp"
#include "formula/string_utils.hpp"
#include "game_config.hpp"
#include "gui/widgets/menu_button.hpp"
#include "gui/widgets/minimap.hpp"
#include "gui/widgets/settings.hpp"
#include "gui/widgets/slider.hpp"
@ -42,8 +41,10 @@
#include "gui/widgets/toggle_button.hpp"
#include "gui/widgets/toggle_panel.hpp"
#include "log.hpp"
#include "savegame.hpp"
#include "map_settings.hpp"
#include "preferences/game.hpp"
#include "save_index.hpp"
#include "savegame.hpp"
#include <boost/algorithm/string.hpp>
@ -789,7 +790,7 @@ void mp_create_game::update_map_settings()
void mp_create_game::load_game_callback(window& window)
{
savegame::loadgame load(cfg_, create_engine_.get_state());
savegame::loadgame load(savegame::save_index_class::default_saves_dir(), cfg_, create_engine_.get_state());
if(!load.load_multiplayer_game()) {
return;

View file

@ -549,5 +549,6 @@ hotkey::ACTION_STATE play_controller::hotkey_handler::get_action_state(hotkey::H
void play_controller::hotkey_handler::load_autosave(const std::string& filename)
{
throw savegame::load_game_exception(filename);
throw savegame::load_game_exception(
savegame::load_game_metadata{savegame::save_index_class::default_saves_dir(), filename});
}

View file

@ -14,6 +14,7 @@
#include "hotkey/hotkey_handler_sp.hpp"
#include "filesystem.hpp" // for get_saves_dir()
#include "font/standard_colors.hpp"
#include "formula/string_utils.hpp"
#include "hotkey/hotkey_command.hpp"
@ -308,7 +309,7 @@ void playsingle_controller::hotkey_handler::load_autosave(const std::string& fil
{
config savegame;
std::string error_log;
savegame::read_save_file(filename, savegame, &error_log);
savegame::read_save_file(filesystem::get_saves_dir(), filename, savegame, &error_log);
if(!error_log.empty() || savegame.child_or_empty("snapshot")["replay_pos"].to_int(-1) < 0 ) {
gui2::show_error_message(_("The file you have tried to load is corrupt: '") + error_log);

View file

@ -47,9 +47,10 @@
#include "replay.hpp"
#include "reports.hpp"
#include "resources.hpp"
#include "savegame.hpp"
#include "saved_game.hpp"
#include "save_blocker.hpp"
#include "save_index.hpp"
#include "saved_game.hpp"
#include "savegame.hpp"
#include "scripting/game_lua_kernel.hpp"
#include "scripting/plugins/context.hpp"
#include "sound.hpp"
@ -909,7 +910,7 @@ void play_controller::save_map()
void play_controller::load_game()
{
savegame::loadgame load(game_config_, saved_game_);
savegame::loadgame load(savegame::save_index_class::default_saves_dir(), game_config_, saved_game_);
load.load_game_ingame();
}

View file

@ -43,7 +43,7 @@ void extract_summary_from_config(config&, config&);
void save_index_class::rebuild(const std::string& name)
{
std::time_t modified = filesystem::file_modified_time(filesystem::get_saves_dir() + "/" + name);
std::time_t modified = filesystem::file_modified_time(dir_ + "/" + name);
rebuild(name, modified);
}
@ -56,7 +56,7 @@ void save_index_class::rebuild(const std::string& name, const std::time_t& modif
try {
config full;
std::string dummy;
read_save_file(name, full, &dummy);
read_save_file(dir_, name, full, &dummy);
extract_summary_from_config(full, summary);
} catch(const game::load_game_failed&) {
@ -92,10 +92,20 @@ config& save_index_class::get(const std::string& name)
return result;
}
const std::string& save_index_class::dir() const
{
return dir_;
}
void save_index_class::write_save_index()
{
log_scope("write_save_index()");
if(read_only_) {
LOG_SAVE << "no-op: read_only instance";
return;
}
try {
filesystem::scoped_ostream stream = filesystem::ostream_file(filesystem::get_save_index_file());
@ -110,13 +120,21 @@ void save_index_class::write_save_index()
}
}
save_index_class::save_index_class()
save_index_class::save_index_class(const std::string& dir)
: loaded_(false)
, data_()
, modified_()
, dir_(dir)
, read_only_(true)
{
}
save_index_class::save_index_class(create_for_default_saves_dir)
: save_index_class(filesystem::get_saves_dir())
{
read_only_ = false;
}
config& save_index_class::data(const std::string& name)
{
config& cfg = data();
@ -167,7 +185,11 @@ void save_index_class::fix_leader_image_path(config& data)
}
}
save_index_class save_index_manager;
std::shared_ptr<save_index_class> save_index_class::default_saves_dir()
{
static auto instance = std::make_shared<save_index_class>(create_for_default_saves_dir::yes);
return instance;
}
class filename_filter
{
@ -187,12 +209,12 @@ private:
};
/** Get a list of available saves. */
std::vector<save_info> get_saves_list(const std::string* dir, const std::string* filter)
std::vector<save_info> save_index_class::get_saves_list(const std::string* filter)
{
create_save_info creator(dir);
create_save_info creator(shared_from_this());
std::vector<std::string> filenames;
filesystem::get_files_in_dir(creator.dir, &filenames);
filesystem::get_files_in_dir(dir(), &filenames);
if(filter) {
filenames.erase(
@ -208,7 +230,7 @@ std::vector<save_info> get_saves_list(const std::string* dir, const std::string*
const config& save_info::summary() const
{
return save_index_manager.get(name());
return save_index_->get(name());
}
std::string save_info::format_time_local() const
@ -255,12 +277,12 @@ bool save_info_less_time::operator()(const save_info& a, const save_info& b) con
}
}
static filesystem::scoped_istream find_save_file(
static filesystem::scoped_istream find_save_file(const std::string& dir,
const std::string& name, const std::vector<std::string>& suffixes)
{
for(const std::string& suf : suffixes) {
filesystem::scoped_istream file_stream =
filesystem::istream_file(filesystem::get_saves_dir() + "/" + name + suf);
filesystem::istream_file(dir + "/" + name + suf);
if(!file_stream->fail()) {
return file_stream;
@ -271,10 +293,10 @@ static filesystem::scoped_istream find_save_file(
throw game::load_game_failed();
}
void read_save_file(const std::string& name, config& cfg, std::string* error_log)
void read_save_file(const std::string& dir, const std::string& name, config& cfg, std::string* error_log)
{
static const std::vector<std::string> suffixes{"", ".gz", ".bz2"};
filesystem::scoped_istream file_stream = find_save_file(name, suffixes);
filesystem::scoped_istream file_stream = find_save_file(dir, name, suffixes);
cfg.clear();
try {
@ -312,8 +334,14 @@ void read_save_file(const std::string& name, config& cfg, std::string* error_log
}
}
void remove_old_auto_saves(const int autosavemax, const int infinite_auto_saves)
void save_index_class::delete_old_auto_saves(const int autosavemax, const int infinite_auto_saves)
{
log_scope("delete_old_auto_saves()");
if(read_only_) {
LOG_SAVE << "no-op: read_only instance";
return;
}
const std::string auto_save = _("Auto-Save");
int countdown = autosavemax;
@ -321,7 +349,7 @@ void remove_old_auto_saves(const int autosavemax, const int infinite_auto_saves)
return;
}
std::vector<save_info> games = get_saves_list(nullptr, &auto_save);
std::vector<save_info> games = get_saves_list(&auto_save);
for(std::vector<save_info>::iterator i = games.begin(); i != games.end(); ++i) {
if(countdown-- <= 0) {
LOG_SAVE << "Deleting savegame '" << i->name() << "'\n";
@ -330,23 +358,28 @@ void remove_old_auto_saves(const int autosavemax, const int infinite_auto_saves)
}
}
void delete_game(const std::string& name)
void save_index_class::delete_game(const std::string& name)
{
filesystem::delete_file(filesystem::get_saves_dir() + "/" + name);
if(read_only_) {
log_scope("delete_game()");
LOG_SAVE << "no-op: read_only instance";
return;
}
save_index_manager.remove(name);
filesystem::delete_file(dir() + "/" + name);
remove(name);
}
create_save_info::create_save_info(const std::string* d)
: dir(d ? *d : filesystem::get_saves_dir())
create_save_info::create_save_info(const std::shared_ptr<save_index_class>& manager)
: manager_(manager)
{
}
save_info create_save_info::operator()(const std::string& filename) const
{
std::time_t modified = filesystem::file_modified_time(dir + "/" + filename);
save_index_manager.set_modified(filename, modified);
return save_info(filename, modified);
std::time_t modified = filesystem::file_modified_time(manager_->dir() + "/" + filename);
manager_->set_modified(filename, modified);
return save_info(filename, manager_, modified);
}
void extract_summary_from_config(config& cfg_save, config& cfg_summary)

View file

@ -20,14 +20,17 @@
namespace savegame
{
class save_index_class;
/** Filename and modification date for a file list */
class save_info
{
private:
friend class create_save_info;
save_info(const std::string& name, const std::time_t& modified)
save_info(const std::string& name, const std::shared_ptr<save_index_class>& index, const std::time_t& modified)
: name_(name)
, save_index_(index)
, modified_(modified)
{
}
@ -49,6 +52,7 @@ public:
private:
std::string name_;
std::shared_ptr<save_index_class> save_index_;
std::time_t modified_;
};
@ -61,40 +65,60 @@ struct save_info_less_time
bool operator()(const save_info& a, const save_info& b) const;
};
std::vector<save_info> get_saves_list(const std::string* dir = nullptr, const std::string* filter = nullptr);
/** Read the complete config information out of a savefile. */
void read_save_file(const std::string& name, config& cfg, std::string* error_log);
/** Remove autosaves that are no longer needed (according to the autosave policy in the preferences). */
void remove_old_auto_saves(const int autosavemax, const int infinite_auto_saves);
/** Delete a savegame. */
void delete_game(const std::string& name);
void read_save_file(const std::string& dir, const std::string& name, config& cfg, std::string* error_log);
class create_save_info
{
public:
create_save_info(const std::string* d = nullptr);
explicit create_save_info(const std::shared_ptr<save_index_class>&);
save_info operator()(const std::string& filename) const;
const std::string dir;
std::shared_ptr<save_index_class> manager_;
};
class save_index_class
class save_index_class : public std::enable_shared_from_this<save_index_class>
{
public:
save_index_class();
/**
* Constructor for a read-only instance. To get a writable instance, call default_saves_dir().
*/
explicit save_index_class(const std::string& dir);
/** Syntatic sugar for choosing which constructor to use. */
enum class create_for_default_saves_dir { yes };
explicit save_index_class(create_for_default_saves_dir);
/** Returns an instance for managing saves in filesystem::get_saves_dir() */
static std::shared_ptr<save_index_class> default_saves_dir();
std::vector<save_info> get_saves_list(const std::string* filter=nullptr);
/** Delete a savegame, including deleting the underlying file. */
void delete_game(const std::string& name);
void rebuild(const std::string& name);
void rebuild(const std::string& name, const std::time_t& modified);
/** Delete a savegame from the index, without deleting the underlying file. */
void remove(const std::string& name);
void set_modified(const std::string& name, const std::time_t& modified);
config& get(const std::string& name);
const std::string& dir() const;
/** Delete autosaves that are no longer needed (according to the autosave policy in the preferences). */
void delete_old_auto_saves(const int autosavemax, const int infinite_auto_saves);
/** Sync to disk, no-op if read_only_ is set */
void write_save_index();
/**
* If true, all of delete_game, delete_old_auto_saves and write_save_index will be no-ops.
*/
bool read_only()
{
return read_only_;
}
private:
config& data(const std::string& name);
config& data();
@ -104,7 +128,11 @@ private:
bool loaded_;
config data_;
std::map<std::string, std::time_t> modified_;
const std::string dir_;
/**
* The instance for default_saves_dir() writes a cache file. For other instances,
* write_save_index() and delete() are no-ops.
*/
bool read_only_;
};
extern save_index_class save_index_manager;
} // end of namespace savegame

View file

@ -61,7 +61,8 @@ namespace savegame {
bool save_game_exists(std::string name, compression::format compressed)
{
name += compression::format_extension(compressed);
return filesystem::file_exists(filesystem::get_saves_dir() + "/" + name);
auto manager = save_index_class::default_saves_dir();
return filesystem::file_exists(manager->dir() + "/" + name);
}
void clean_saves(const std::string& label)
@ -69,18 +70,19 @@ void clean_saves(const std::string& label)
const std::string prefix = label + "-" + _("Auto-Save");
LOG_SAVE << "Cleaning saves with prefix '" << prefix << "'\n";
for(const auto& save : get_saves_list()) {
auto manager = save_index_class::default_saves_dir();
for(const auto& save : manager->get_saves_list()) {
if(save.name().compare(0, prefix.length(), prefix) == 0) {
LOG_SAVE << "Deleting savegame '" << save.name() << "'\n";
delete_game(save.name());
manager->delete_game(save.name());
}
}
}
loadgame::loadgame(const config& game_config, saved_game& gamestate)
loadgame::loadgame(const std::shared_ptr<save_index_class>& index, const config& game_config, saved_game& gamestate)
: game_config_(game_config)
, gamestate_(gamestate)
, load_data_()
, load_data_(index)
{}
bool loadgame::show_difficulty_dialog()
@ -136,11 +138,16 @@ bool loadgame::load_game_ingame()
}
}
if(!load_data_.manager) {
ERR_SAVE << "Null pointer in save index" << std::endl;
return false;
}
load_data_.show_replay |= is_replay_save(load_data_.summary);
// Confirm the integrity of the file before throwing the exception.
// Use the summary in the save_index for this.
const config & summary = save_index_manager.get(load_data_.filename);
const config & summary = load_data_.manager->get(load_data_.filename);
if (summary["corrupt"].to_bool(false)) {
gui2::show_error_message(
@ -177,8 +184,13 @@ bool loadgame::load_game()
}
}
if(!load_data_.manager) {
ERR_SAVE << "Null pointer in save index" << std::endl;
return false;
}
std::string error_log;
read_save_file(load_data_.filename, load_data_.load_config, &error_log);
read_save_file(load_data_.manager->dir(), load_data_.filename, load_data_.load_config, &error_log);
convert_old_saves(load_data_.load_config);
@ -276,6 +288,11 @@ bool loadgame::load_multiplayer_game()
return false;
}
if(!load_data_.manager) {
ERR_SAVE << "Null pointer in save index" << std::endl;
return false;
}
// read_save_file needs to be called before we can verify the classification so the data has
// been populated. Since we do that, we report any errors in that process first.
std::string error_log;
@ -283,7 +300,7 @@ bool loadgame::load_multiplayer_game()
cursor::setter cur(cursor::WAIT);
log_scope("load_game");
read_save_file(load_data_.filename, load_data_.load_config, &error_log);
read_save_file(load_data_.manager->dir(), load_data_.filename, load_data_.load_config, &error_log);
copy_era(load_data_.load_config);
}
@ -328,6 +345,7 @@ void loadgame::copy_era(config &cfg)
savegame::savegame(saved_game& gamestate, const compression::format compress_saves, const std::string& title)
: filename_()
, title_(title)
, save_index_manager_(save_index_class::default_saves_dir())
, gamestate_(gamestate)
, error_message_(_("The game could not be saved: "))
, show_confirmation_(false)
@ -448,7 +466,7 @@ bool savegame::save_game(const std::string& filename)
// a player saves a game and exits the game or reloads the cache, the leader image will
// only be available within that specific binary context (when playing another game from
// the came campaign, for example).
save_index_manager.rebuild(filename_);
save_index_manager_->rebuild(filename_);
end = SDL_GetTicks();
LOG_SAVE << "Milliseconds to save " << filename_ << ": " << end - start << std::endl;
@ -506,7 +524,7 @@ void savegame::finish_save_game(const config_writer &out)
if(!out.good()) {
throw game::save_game_failed(_("Could not write to file"));
}
save_index_manager.remove(gamestate_.classification().label);
save_index_manager_->remove(gamestate_.classification().label);
} catch(const filesystem::io_exception& e) {
throw game::save_game_failed(e.what());
}
@ -516,7 +534,7 @@ void savegame::finish_save_game(const config_writer &out)
filesystem::scoped_ostream savegame::open_save_game(const std::string &label)
{
try {
return filesystem::ostream_file(filesystem::get_saves_dir() + "/" + label);
return filesystem::ostream_file(save_index_manager_->dir() + "/" + label);
} catch(const filesystem::io_exception& e) {
throw game::save_game_failed(e.what());
}
@ -573,7 +591,8 @@ void autosave_savegame::autosave(const bool disable_autosave, const int autosave
save_game_automatic();
remove_old_auto_saves(autosave_max, infinite_autosaves);
auto manager = save_index_class::default_saves_dir();
manager->delete_old_auto_saves(autosave_max, infinite_autosaves);
}
std::string autosave_savegame::create_initial_filename(unsigned int turn_number) const

View file

@ -18,6 +18,7 @@
#include "config.hpp"
#include "filesystem.hpp"
#include "lua_jailbreak_exception.hpp"
#include "save_index.hpp"
#include "saved_game.hpp"
#include "serialization/compression.hpp"
@ -26,17 +27,33 @@
class config_writer;
class version_info;
namespace savegame {
namespace savegame
{
/** converts saves from older versions of wesnoth*/
void convert_old_saves(config& cfg);
/** Returns true if there is already a savegame with that name. */
/**
* Returns true if there is already a savegame with this name, looking only in the default save
* directory. Only expected to be used to check whether a subsequent save would overwrite an
* existing file, therefore only expected to be used for the default save dir.
*/
bool save_game_exists(std::string name, compression::format compressed);
/** Delete all autosaves of a certain scenario. */
/**
* Delete all autosaves of a certain scenario from the default save directory.
*
* This is only expected to be called when the player starts the next scenario (or finishes the
* campaign, in the case of the last scenario), so it's expected to correspond to the next scenario
* being written to the default save directory.
*/
void clean_saves(const std::string& label);
struct load_game_metadata {
/** Name of the savefile to be loaded. */
struct load_game_metadata
{
/** There may be different instances of the index for different directories */
std::shared_ptr<save_index_class> manager;
/** Name of the savefile to be loaded (not including the directory). */
std::string filename;
/** The difficulty the save is meant to be loaded with. */
@ -57,10 +74,11 @@ struct load_game_metadata {
/** Config information of the savefile to be loaded. */
config load_config;
explicit load_game_metadata(const std::string& fname = "", const std::string& hard = "",
explicit load_game_metadata(std::shared_ptr<save_index_class> index,
const std::string& fname = "", const std::string& hard = "",
bool replay = false, bool stop = false, bool change = false,
const config& summary = config(), const config& info = config())
: filename(fname), difficulty(hard)
: manager(index), filename(fname), difficulty(hard)
, show_replay(replay), cancel_orders(stop), select_difficulty(change)
, summary(summary), load_config(info)
{
@ -75,13 +93,7 @@ class load_game_exception
: public lua_jailbreak_exception, public std::exception
{
public:
load_game_exception(const std::string& fname)
: lua_jailbreak_exception()
, data_(fname)
{
}
load_game_exception(load_game_metadata&& data)
explicit load_game_exception(load_game_metadata&& data)
: lua_jailbreak_exception()
, data_(data)
{
@ -96,7 +108,7 @@ private:
class loadgame
{
public:
loadgame(const config& game_config, saved_game& gamestate);
loadgame(const std::shared_ptr<save_index_class>& index, const config& game_config, saved_game& gamestate);
virtual ~loadgame() {}
/* In any of the following three function, a bool value of false indicates
@ -143,6 +155,9 @@ private:
* The base class for all savegame stuff.
* This should not be used directly, as it does not directly produce usable saves.
* Instead, use one of the derived classes.
*
* Saves are only created in filesystem::get_saves_dir() - files can be loaded
* from elsewhere, but writes are only to that directory.
*/
class savegame
{
@ -207,6 +222,13 @@ protected:
/** Title of the savegame dialog */
std::string title_;
/** Will (at the time of writing) be save_index_class::default_saves_dir().
There may be different instances the index for different directories, however
writing is only expected to happen in the default save directory.
Making this a class member anyway, while I'm refactoring. */
std::shared_ptr<save_index_class> save_index_manager_;
private:
/** Subclass-specific part of filename building. */
virtual std::string create_initial_filename(unsigned int turn_number) const = 0;

View file

@ -111,6 +111,7 @@
#include "language.hpp"
#include "map/map.hpp"
#include "replay.hpp"
#include "save_index.hpp"
#include "saved_game.hpp"
#include "terrain/type_data.hpp"
#include "tests/utils/fake_display.hpp"
@ -740,7 +741,9 @@ template<>
struct dialog_tester<game_load>
{
config cfg;
savegame::load_game_metadata data;
// It would be good to have a test directory instead of using the same directory as the player,
// however this code will support that - default_saves_dir() will respect --userdata-dir.
savegame::load_game_metadata data{savegame::save_index_class::default_saves_dir()};
dialog_tester()
{
/** @todo Would be nice to add real data to the config. */