/* $Id$ */ /* Copyright (C) 2003 - 2009 by Jörg Hinrichs, refactored from various places formerly created by David White Part of the Battle for Wesnoth Project http://www.wesnoth.org/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 or at your option any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. See the COPYING file for more details. */ #include "savegame.hpp" #include "dialogs.hpp" //FIXME: move illegal file character function here and get rid of this include #include "foreach.hpp" #include "game_end_exceptions.hpp" #include "game_events.hpp" #include "gettext.hpp" #include "log.hpp" #include "map.hpp" #include "map_label.hpp" #include "preferences_display.hpp" #include "replay.hpp" #include "serialization/binary_or_text.hpp" #include "serialization/parser.hpp" #include "sound.hpp" #include "statistics.hpp" #include "unit_id.hpp" #include "version.hpp" #define LOG_SAVE LOG_STREAM(info, engine) #ifdef _WIN32 #include /** * conv_ansi_utf8() * - Convert a string between ANSI encoding (for Windows filename) and UTF-8 * string &name * - filename to be converted * bool a2u * - if true, convert the string from ANSI to UTF-8. * - if false, reverse. (convert it from UTF-8 to ANSI) */ void conv_ansi_utf8(std::string &name, bool a2u) { int wlen = MultiByteToWideChar(a2u ? CP_ACP : CP_UTF8, 0, name.c_str(), -1, NULL, 0); if (wlen == 0) return; WCHAR *wc = new WCHAR[wlen]; if (wc == NULL) return; if (MultiByteToWideChar(a2u ? CP_ACP : CP_UTF8, 0, name.c_str(), -1, wc, wlen) == 0) { delete wc; return; } int alen = WideCharToMultiByte(!a2u ? CP_ACP : CP_UTF8, 0, wc, wlen, NULL, 0, NULL, NULL); if (alen == 0) { delete wc; return; } CHAR *ac = new CHAR[alen]; if (ac == NULL) { delete wc; return; } WideCharToMultiByte(!a2u ? CP_ACP : CP_UTF8, 0, wc, wlen, ac, alen, NULL, NULL); delete wc; if (ac == NULL) { return; } name = ac; delete ac; return; } void replace_underbar2space(std::string &name) { LOG_SAVE << "conv(A2U)-from:[" << name << "]" << std::endl; conv_ansi_utf8(name, true); LOG_SAVE << "conv(A2U)-to:[" << name << "]" << std::endl; LOG_SAVE << "replace_underbar2space-from:[" << name << "]" << std::endl; std::replace(name.begin(), name.end(), '_', ' '); LOG_SAVE << "replace_underbar2space-to:[" << name << "]" << std::endl; } void replace_space2underbar(std::string &name) { LOG_SAVE << "conv(U2A)-from:[" << name << "]" << std::endl; conv_ansi_utf8(name, false); LOG_SAVE << "conv(U2A)-to:[" << name << "]" << std::endl; LOG_SAVE << "replace_underbar2space-from:[" << name << "]" << std::endl; std::replace(name.begin(), name.end(), ' ', '_'); LOG_SAVE << "replace_underbar2space-to:[" << name << "]" << std::endl; } #else /* ! _WIN32 */ void replace_underbar2space(std::string &name) { std::replace(name.begin(),name.end(),'_',' '); } void replace_space2underbar(std::string &name) { std::replace(name.begin(),name.end(),' ','_'); } #endif /* _WIN32 */ static void read_save_file(const std::string& name, config& cfg, std::string* error_log) { std::string modified_name = name; replace_space2underbar(modified_name); // Try reading the file both with and without underscores scoped_istream file_stream = istream_file(get_saves_dir() + "/" + modified_name); if (file_stream->fail()) file_stream = istream_file(get_saves_dir() + "/" + name); cfg.clear(); try{ if(is_gzip_file(name)) { read_gz(cfg, *file_stream, error_log); } else { detect_format_and_read(cfg, *file_stream, error_log); } } catch (config::error &err) { LOG_SAVE << err.message; throw game::load_game_failed(); } if(cfg.empty()) { LOG_SAVE << "Could not parse file data into config\n"; throw game::load_game_failed(); } } void save_summary::load_summary(const std::string& name, config& cfg_summary, std::string* error_log){ log_scope("load_game_summary"); config cfg; read_save_file(name,cfg,error_log); ::extract_summary_from_config(cfg, cfg_summary); } loadgame::loadgame(display& gui, const config& game_config, game_state& gamestate) : game_config_(game_config) , gui_(gui) , gamestate_(gamestate) { gamestate_ = game_state(); } void loadgame::show_dialog(bool show_replay, bool cancel_orders) { bool show_replay_dialog = show_replay; bool cancel_orders_dialog = cancel_orders; //FIXME: Integrate the load_game dialog into this class filename_ = dialogs::load_game_dialog(gui_, game_config_, &show_replay_dialog, &cancel_orders_dialog); show_replay_ = show_replay_dialog; cancel_orders_ = cancel_orders_dialog; } void loadgame::load_game() { show_dialog(false, false); if(filename_ != "") throw game::load_game_exception(filename_, show_replay_, cancel_orders_); } void loadgame::load_game(std::string& filename, bool show_replay, bool cancel_orders) { filename_ = filename; if (filename_.empty()){ show_dialog(show_replay, cancel_orders); } else{ show_replay_ = show_replay; cancel_orders_ = cancel_orders; } if (filename_.empty()) throw load_game_cancelled_exception(); std::string error_log; read_save_file(filename_, load_config_, &error_log); if(!error_log.empty()) { try { gui::show_error_message(gui_, _("Warning: The file you have tried to load is corrupt. Loading anyway.\n") + error_log); } catch (utils::invalid_utf8_exception&) { gui::show_error_message(gui_, _("Warning: The file you have tried to load is corrupt. Loading anyway.\n") + std::string("(UTF-8 ERROR)")); } } gamestate_.difficulty = load_config_["difficulty"]; gamestate_.campaign_define = load_config_["campaign_define"]; gamestate_.campaign_type = load_config_["campaign_type"]; gamestate_.campaign_xtra_defines = utils::split(load_config_["campaign_extra_defines"]); gamestate_.version = load_config_["version"]; if(gamestate_.version != game_config::version) { const version_info parsed_savegame_version(gamestate_.version); if(game_config::wesnoth_version.minor_version() % 2 != 0 || game_config::wesnoth_version.major_version() != parsed_savegame_version.major_version() || game_config::wesnoth_version.minor_version() != parsed_savegame_version.minor_version()) { check_version_compatibility(); } } } void loadgame::check_version_compatibility() { // do not load if too old, if either the savegame or the current game // has the version 'test' allow loading if(!game_config::is_compatible_savegame_version(gamestate_.version)) { /* GCC-3.3 needs a temp var otherwise compilation fails */ gui::message_dialog dlg(gui_, "", _("This save is from a version too old to be loaded.")); dlg.show(); throw load_game_cancelled_exception(); } const int res = gui::dialog(gui_,"", _("This save is from a different version of the game. Do you want to try to load it?"), gui::YES_NO).show(); if(res == 1) { throw load_game_cancelled_exception(); } } void loadgame::set_gamestate() { gamestate_ = game_state(load_config_, show_replay_); // Get the status of the random in the snapshot. // For a replay we need to restore the start only, the replaying gets at // proper location. // For normal loading also restore the call count. const int seed = lexical_cast_default (load_config_["random_seed"], 42); const unsigned calls = show_replay_ ? 0 : lexical_cast_default (gamestate_.snapshot["random_calls"]); gamestate_.rng().seed_random(seed, calls); } void loadgame::load_multiplayer_game() { show_dialog(false, false); if (filename_.empty()) throw load_game_cancelled_exception(); std::string error_log; { cursor::setter cur(cursor::WAIT); log_scope("load_game"); read_save_file(filename_, load_config_, &error_log); copy_era(load_config_); gamestate_ = game_state(load_config_); } if(!error_log.empty()) { gui::show_error_message(gui_, _("The file you have tried to load is corrupt: '") + error_log); throw load_game_cancelled_exception(); } if(gamestate_.campaign_type != "multiplayer") { /* GCC-3.3 needs a temp var otherwise compilation fails */ gui::message_dialog dlg(gui_, "", _("This is not a multiplayer save")); dlg.show(); throw load_game_cancelled_exception(); } check_version_compatibility(); } void loadgame::copy_era(config &cfg) { const config &replay_start = cfg.child("replay_start"); if (!replay_start) return; const config &era = replay_start.child("era"); if (!era) return; config &snapshot = cfg.child("snapshot"); if (!snapshot) return; snapshot.add_child("era", era); } savegame::savegame(game_state& gamestate, const std::string title) : gamestate_(gamestate) , snapshot_() , filename_() , title_(title) , error_message_(_("The game could not be saved")) , interactive_(false) {} void savegame::save_game_interactive(display& gui, const std::string& message, gui::DIALOG_TYPE dialog_type, const bool has_exit_button, const bool ask_for_filename) { interactive_ = ask_for_filename; create_filename(); const int res = dialogs::get_save_name(gui, message, _("Name: "), &filename_, dialog_type, title_, has_exit_button, ask_for_filename); if (res == 2) throw end_level_exception(QUIT); if (res != 0) return; save_game(&gui); } void savegame::before_save() { gamestate_.replay_data = recorder.get_replay_data(); } void savegame::save_game(const std::string& filename) { filename_ = filename; if (!interactive_) create_filename(); save_game(); } void savegame::save_game(display* gui) { try { before_save(); save_game_internal(filename_); if (gui != NULL && interactive_) gui::message_dialog(*gui,_("Saved"),_("The game has been saved")).show(); } catch(game::save_game_failed&) { if (gui != NULL){ gui::message_dialog to_show(*gui,_("Error"), error_message_); to_show.show(); //do not bother retrying, since the user can just try to save the game again //maybe show a yes-no dialog for "disable autosaves now"? } }; } void savegame::save_game_internal(const std::string& filename) { LOG_SAVE << "savegame::save_game"; filename_ = filename; if(preferences::compress_saves()) { filename_ += ".gz"; } std::stringstream ss; { config_writer out(ss, preferences::compress_saves()); write_game(out); finish_save_game(out); } scoped_ostream os(open_save_game(filename_)); (*os) << ss.str(); if (!os->good()) { throw game::save_game_failed(_("Could not write to file")); } } void savegame::write_game(config_writer &out) const { log_scope("write_game"); out.write_key_val("label", gamestate_.label); out.write_key_val("history", gamestate_.history); out.write_key_val("abbrev", gamestate_.abbrev); out.write_key_val("version", game_config::version); out.write_key_val("scenario", gamestate_.scenario); out.write_key_val("next_scenario", gamestate_.next_scenario); out.write_key_val("completion", gamestate_.completion); out.write_key_val("campaign", gamestate_.campaign); out.write_key_val("campaign_type", gamestate_.campaign_type); out.write_key_val("difficulty", gamestate_.difficulty); out.write_key_val("campaign_define", gamestate_.campaign_define); out.write_key_val("campaign_extra_defines", utils::join(gamestate_.campaign_xtra_defines)); out.write_key_val("random_seed", lexical_cast(gamestate_.rng().get_random_seed())); out.write_key_val("random_calls", lexical_cast(gamestate_.rng().get_random_calls())); out.write_key_val("next_underlying_unit_id", lexical_cast(n_unit::id_manager::instance().get_save_id())); out.write_key_val("end_text", gamestate_.end_text); out.write_key_val("end_text_duration", str_cast(gamestate_.end_text_duration)); out.write_child("variables", gamestate_.get_variables()); for(std::map::const_iterator j=gamestate_.wml_menu_items.begin(); j!=gamestate_.wml_menu_items.end(); ++j) { out.open_child("menu_item"); out.write_key_val("id", j->first); out.write_key_val("image", j->second->image); out.write_key_val("description", j->second->description); out.write_key_val("needs_select", (j->second->needs_select) ? "yes" : "no"); if(!j->second->show_if.empty()) out.write_child("show_if", j->second->show_if); if(!j->second->filter_location.empty()) out.write_child("filter_location", j->second->filter_location); if(!j->second->command.empty()) out.write_child("command", j->second->command); out.close_child("menu_item"); } if (!gamestate_.replay_data.child("replay")) { out.write_child("replay", gamestate_.replay_data); } out.write_child("snapshot",snapshot_); out.write_child("replay_start",gamestate_.starting_pos); out.open_child("statistics"); statistics::write_stats(out); out.close_child("statistics"); } void savegame::finish_save_game(const config_writer &out) { std::string name = gamestate_.label; std::replace(name.begin(),name.end(),' ','_'); std::string fname(get_saves_dir() + "/" + name); try { if(!out.good()) { throw game::save_game_failed(_("Could not write to file")); } config& summary = save_summary(gamestate_.label); extract_summary_data_from_save(summary); const int mod_time = static_cast(file_create_time(fname)); summary["mod_time"] = str_cast(mod_time); write_save_index(); } catch(io_exception& e) { throw game::save_game_failed(e.what()); } } void savegame::extract_summary_data_from_save(config& out) { const bool has_replay = gamestate_.replay_data.empty() == false; const bool has_snapshot = gamestate_.snapshot.child("side"); out["replay"] = has_replay ? "yes" : "no"; out["snapshot"] = has_snapshot ? "yes" : "no"; out["label"] = gamestate_.label; out["campaign"] = gamestate_.campaign; out["campaign_type"] = gamestate_.campaign_type; out["scenario"] = gamestate_.scenario; out["difficulty"] = gamestate_.difficulty; out["version"] = gamestate_.version; out["corrupt"] = ""; if(has_snapshot) { out["turn"] = gamestate_.snapshot["turn_at"]; if(gamestate_.snapshot["turns"] != "-1") { out["turn"] = out["turn"].str() + "/" + gamestate_.snapshot["turns"].str(); } } // Find the first human leader so we can display their icon in the load menu. /** @todo Ideally we should grab all leaders if there's more than 1 human player? */ std::string leader; for(std::map::const_iterator p = gamestate_.players.begin(); p!=gamestate_.players.end(); ++p) { for(std::vector::const_iterator u = p->second.available_units.begin(); u != p->second.available_units.end(); ++u) { if(u->can_recruit()) { leader = u->type_id(); } } } bool shrouded = false; if(leader == "") { const config& snapshot = has_snapshot ? gamestate_.snapshot : gamestate_.starting_pos; foreach (const config &side, snapshot.child_range("side")) { if (side["controller"] != "human") { continue; } if (utils::string_bool(side["shroud"])) { shrouded = true; } foreach (const config &u, side.child_range("unit")) { if (utils::string_bool(u["canrecruit"], false)) { leader = u["id"]; break; } } } } out["leader"] = leader; out["map_data"] = ""; if(!shrouded) { if(has_snapshot) { if (!gamestate_.snapshot.find_child("side", "shroud", "yes")) { out["map_data"] = gamestate_.snapshot["map_data"]; } } else if(has_replay) { if (!gamestate_.starting_pos.find_child("side", "shroud", "yes")) { out["map_data"] = gamestate_.starting_pos["map_data"]; } } } } void savegame::set_filename(std::string filename) { filename.erase(std::remove_if(filename.begin(), filename.end(), dialogs::is_illegal_file_char), filename.end()); filename_ = filename; } scenariostart_savegame::scenariostart_savegame(game_state &gamestate) : savegame(gamestate) { set_filename(gamestate.label); } void scenariostart_savegame::before_save() { //Add the player section to the starting position so we can get the correct recall list //when loading the replay later on write_players(gamestate(), gamestate().starting_pos); } replay_savegame::replay_savegame(game_state &gamestate) : savegame(gamestate, _("Save Replay")) {} void replay_savegame::create_filename() { std::stringstream stream; const std::string ellipsed_name = font::make_text_ellipsis(gamestate().label, font::SIZE_NORMAL, 200); stream << ellipsed_name << " " << _("replay"); set_filename(stream.str()); } autosave_savegame::autosave_savegame(game_state &gamestate, const config& level_cfg, const game_display& gui, const std::vector& teams, const unit_map& units, const gamestatus& gamestatus, const gamemap& map) : game_savegame(gamestate, level_cfg, gui, teams, units, gamestatus, map) { set_error_message(_("Could not auto save the game. Please save the game manually.")); create_filename(); } void autosave_savegame::create_filename() { std::string filename; if (gamestate().label.empty()) filename = _("Auto-Save"); else filename = gamestate().label + "-" + _("Auto-Save") + lexical_cast(gamestatus_.turn()); set_filename(filename); } game_savegame::game_savegame(game_state &gamestate, const config& level_cfg, const game_display& gui, const std::vector& teams, const unit_map& units, const gamestatus& gamestatus, const gamemap& map) : savegame(gamestate, _("Save Game")), level_cfg_(level_cfg), gui_(gui), teams_(teams), units_(units), gamestatus_(gamestatus), map_(map) {} void game_savegame::create_filename() { std::stringstream stream; const std::string ellipsed_name = font::make_text_ellipsis(gamestate().label, font::SIZE_NORMAL, 200); stream << ellipsed_name << " " << _("Turn") << " " << gamestatus_.turn(); set_filename(stream.str()); } void game_savegame::before_save() { savegame::before_save(); write_game_snapshot(); } void game_savegame::write_game_snapshot() { snapshot().merge_attributes(level_cfg_); snapshot()["snapshot"] = "yes"; std::stringstream buf; buf << gui_.playing_team(); snapshot()["playing_team"] = buf.str(); for(std::vector::const_iterator t = teams_.begin(); t != teams_.end(); ++t) { const unsigned int side_num = t - teams_.begin() + 1; config& side = snapshot().add_child("side"); t->write(side); side["no_leader"] = "yes"; buf.str(std::string()); buf << side_num; side["side"] = buf.str(); //current visible units for(unit_map::const_iterator i = units_.begin(); i != units_.end(); ++i) { if(i->second.side() == side_num) { config& u = side.add_child("unit"); i->first.write(u); i->second.write(u); } } //recall list { for(std::map::const_iterator i=gamestate().players.begin(); i!=gamestate().players.end(); ++i) { for(std::vector::const_iterator j = i->second.available_units.begin(); j != i->second.available_units.end(); ++j) { if (j->side() == side_num){ config& u = side.add_child("unit"); j->write(u); } } } } } gamestatus_.write(snapshot()); game_events::write_events(snapshot()); // Write terrain_graphics data in snapshot, too const config::child_list& terrains = level_cfg_.get_children("terrain_graphics"); for(config::child_list::const_iterator tg = terrains.begin(); tg != terrains.end(); ++tg) { snapshot().add_child("terrain_graphics", **tg); } sound::write_music_play_list(snapshot()); gamestate().write_snapshot(snapshot()); //write out the current state of the map snapshot()["map_data"] = map_.write(); gui_.labels().write(snapshot()); }