The Battle for Wesnoth  1.15.2+dev
save_index.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2018 by Jörg Hinrichs, refactored from various
3  places formerly created by David White <dave@whitevine.net>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #include "save_index.hpp"
17 
18 #include "config.hpp"
19 #include "filesystem.hpp"
20 #include "format_time_summary.hpp"
21 #include "game_end_exceptions.hpp"
22 #include "game_errors.hpp"
23 #include "gettext.hpp"
24 #include "log.hpp"
25 #include "preferences/game.hpp"
27 #include "serialization/parser.hpp"
28 #include "team.hpp"
29 
30 #include <boost/algorithm/string/replace.hpp>
31 #include <boost/iostreams/filter/gzip.hpp>
32 
33 static lg::log_domain log_engine("engine");
34 #define LOG_SAVE LOG_STREAM(info, log_engine)
35 #define ERR_SAVE LOG_STREAM(err, log_engine)
36 
37 static lg::log_domain log_enginerefac("enginerefac");
38 #define LOG_RG LOG_STREAM(info, log_enginerefac)
39 
40 namespace savegame
41 {
43 
44 void save_index_class::rebuild(const std::string& name)
45 {
46  std::time_t modified = filesystem::file_modified_time(filesystem::get_saves_dir() + "/" + name);
47  rebuild(name, modified);
48 }
49 
50 void save_index_class::rebuild(const std::string& name, const std::time_t& modified)
51 {
52  log_scope("load_summary_from_file");
53 
54  config& summary = data(name);
55 
56  try {
57  config full;
58  std::string dummy;
59  read_save_file(name, full, &dummy);
60 
61  extract_summary_from_config(full, summary);
62  } catch(const game::load_game_failed&) {
63  summary["corrupt"] = true;
64  }
65 
66  summary["mod_time"] = std::to_string(static_cast<int>(modified));
68 }
69 
70 void save_index_class::remove(const std::string& name)
71 {
72  config& root = data();
73  root.remove_attribute(name);
75 }
76 
77 void save_index_class::set_modified(const std::string& name, const std::time_t& modified)
78 {
79  modified_[name] = modified;
80 }
81 
82 config& save_index_class::get(const std::string& name)
83 {
84  config& result = data(name);
85  std::time_t m = modified_[name];
86 
87  config::attribute_value& mod_time = result["mod_time"];
88  if(mod_time.empty() || mod_time.to_time_t() != m) {
89  rebuild(name, m);
90  }
91 
92  return result;
93 }
94 
96 {
97  log_scope("write_save_index()");
98 
99  try {
101 
103  // TODO: maybe allow writing this using bz2 too?
104  write_gz(*stream, data());
105  } else {
106  write(*stream, data());
107  }
108  } catch(const filesystem::io_exception& e) {
109  ERR_SAVE << "error writing to save index file: '" << e.what() << "'" << std::endl;
110  }
111 }
112 
114  : loaded_(false)
115  , data_()
116  , modified_()
117 {
118 }
119 
120 config& save_index_class::data(const std::string& name)
121 {
122  config& cfg = data();
123  if(config& sv = cfg.find_child("save", "save", name)) {
125  return sv;
126  }
127 
128  config& res = cfg.add_child("save");
129  res["save"] = name;
130  return res;
131 }
132 
134 {
135  const std::string si_file = filesystem::get_save_index_file();
136 
137  // Don't try to load the file if it doesn't exist.
138  if(loaded_ == false && filesystem::file_exists(si_file)) {
139  try {
141  try {
142  read_gz(data_, *stream);
143  } catch(const boost::iostreams::gzip_error&) {
144  stream->seekg(0);
145  read(data_, *stream);
146  }
147  } catch(const filesystem::io_exception& e) {
148  ERR_SAVE << "error reading save index: '" << e.what() << "'" << std::endl;
149  } catch(const config::error& e) {
150  ERR_SAVE << "error parsing save index config file:\n" << e.message << std::endl;
151  data_.clear();
152  }
153 
154  loaded_ = true;
155  }
156 
157  return data_;
158 }
159 
161 {
162  for(config& leader : data.child_range("leader")) {
163  std::string leader_image = leader["leader_image"];
164  boost::algorithm::replace_all(leader_image, "\\", "/");
165 
166  leader["leader_image"] = leader_image;
167  }
168 }
169 
171 
173 {
174 public:
175  filename_filter(const std::string& filter)
176  : filter_(filter)
177  {
178  }
179 
180  bool operator()(const std::string& filename) const
181  {
182  return filename.end() == std::search(filename.begin(), filename.end(), filter_.begin(), filter_.end());
183  }
184 
185 private:
186  std::string filter_;
187 };
188 
189 /** Get a list of available saves. */
190 std::vector<save_info> get_saves_list(const std::string* dir, const std::string* filter)
191 {
192  create_save_info creator(dir);
193 
194  std::vector<std::string> filenames;
195  filesystem::get_files_in_dir(creator.dir, &filenames);
196 
197  if(filter) {
198  filenames.erase(
199  std::remove_if(filenames.begin(), filenames.end(), filename_filter(*filter)), filenames.end());
200  }
201 
202  std::vector<save_info> result;
203  std::transform(filenames.begin(), filenames.end(), std::back_inserter(result), creator);
204  std::sort(result.begin(), result.end(), save_info_less_time());
205 
206  return result;
207 }
208 
210 {
211  return save_index_manager.get(name());
212 }
213 
214 std::string save_info::format_time_local() const
215 {
216  if(std::tm* tm_l = std::localtime(&modified())) {
218  ? _("%a %b %d %I:%M %p %Y")
219  : _("%a %b %d %H:%M %Y");
220 
221  return translation::strftime(format, tm_l);
222  }
223 
224  LOG_SAVE << "localtime() returned null for time " << this->modified() << ", save " << name();
225  return "";
226 }
227 
229 {
230  std::time_t t = modified();
231  return utils::format_time_summary(t);
232 }
233 
235 {
236  // This translatable string must be same one as in replay_savegame::create_initial_filename.
237  // TODO: we really shouldn't be relying on translatable strings like this, especially since
238  // old savefiles may have been created in a different language than the current UI language
239  const std::string replay_str = " " + _("replay");
240  if(a.modified() > b.modified()) {
241  return true;
242  } else if(a.modified() < b.modified()) {
243  return false;
244  } else if(a.name().find(replay_str) == std::string::npos && b.name().find(replay_str) != std::string::npos) {
245  // Special funky case; for files created in the same second,
246  // a replay file sorts less than a non-replay file. Prevents
247  // a timing-dependent bug where it may look like, at the end
248  // of a scenario, the replay and the autosave for the next
249  // scenario are displayed in the wrong order.
250  return true;
251  } else if(a.name().find(replay_str) != std::string::npos && b.name().find(replay_str) == std::string::npos) {
252  return false;
253  } else {
254  return a.name() > b.name();
255  }
256 }
257 
259  const std::string& name, const std::vector<std::string>& suffixes)
260 {
261  for(const std::string& suf : suffixes) {
262  filesystem::scoped_istream file_stream =
264 
265  if(!file_stream->fail()) {
266  return file_stream;
267  }
268  }
269 
270  LOG_SAVE << "Could not open supplied filename '" << name << "'\n";
271  throw game::load_game_failed();
272 }
273 
274 void read_save_file(const std::string& name, config& cfg, std::string* error_log)
275 {
276  static const std::vector<std::string> suffixes{"", ".gz", ".bz2"};
277  filesystem::scoped_istream file_stream = find_save_file(name, suffixes);
278 
279  cfg.clear();
280  try {
281  /*
282  * Test the modified name, since it might use a .gz
283  * file even when not requested.
284  */
285  if(filesystem::is_gzip_file(name)) {
286  read_gz(cfg, *file_stream);
287  } else if(filesystem::is_bzip2_file(name)) {
288  read_bz2(cfg, *file_stream);
289  } else {
290  read(cfg, *file_stream);
291  }
292  } catch(const std::ios_base::failure& e) {
293  LOG_SAVE << e.what();
294 
295  if(error_log) {
296  *error_log += e.what();
297  }
298  throw game::load_game_failed();
299  } catch(const config::error& err) {
300  LOG_SAVE << err.message;
301 
302  if(error_log) {
303  *error_log += err.message;
304  }
305 
306  throw game::load_game_failed();
307  }
308 
309  if(cfg.empty()) {
310  LOG_SAVE << "Could not parse file data into config\n";
311  throw game::load_game_failed();
312  }
313 }
314 
315 void remove_old_auto_saves(const int autosavemax, const int infinite_auto_saves)
316 {
317  const std::string auto_save = _("Auto-Save");
318 
319  int countdown = autosavemax;
320  if(countdown == infinite_auto_saves) {
321  return;
322  }
323 
324  std::vector<save_info> games = get_saves_list(nullptr, &auto_save);
325  for(std::vector<save_info>::iterator i = games.begin(); i != games.end(); ++i) {
326  if(countdown-- <= 0) {
327  LOG_SAVE << "Deleting savegame '" << i->name() << "'\n";
328  delete_game(i->name());
329  }
330  }
331 }
332 
333 void delete_game(const std::string& name)
334 {
336 
337  save_index_manager.remove(name);
338 }
339 
341  : dir(d ? *d : filesystem::get_saves_dir())
342 {
343 }
344 
345 save_info create_save_info::operator()(const std::string& filename) const
346 {
347  std::time_t modified = filesystem::file_modified_time(dir + "/" + filename);
348  save_index_manager.set_modified(filename, modified);
349  return save_info(filename, modified);
350 }
351 
352 void extract_summary_from_config(config& cfg_save, config& cfg_summary)
353 {
354  const config& cfg_snapshot = cfg_save.child("snapshot");
355 
356  // Servergenerated replays contain [scenario] and no [replay_start]
357  const config& cfg_replay_start = cfg_save.child("replay_start")
358  ? cfg_save.child("replay_start")
359  : cfg_save.child("scenario");
360 
361  const config& cfg_replay = cfg_save.child("replay");
362  const bool has_replay = cfg_replay && !cfg_replay.empty();
363  const bool has_snapshot = cfg_snapshot && cfg_snapshot.has_child("side");
364 
365  cfg_summary["replay"] = has_replay;
366  cfg_summary["snapshot"] = has_snapshot;
367 
368  cfg_summary["label"] = cfg_save["label"];
369  cfg_summary["campaign_type"] = cfg_save["campaign_type"];
370 
371  if(cfg_save.has_child("carryover_sides_start")) {
372  cfg_summary["scenario"] = cfg_save.child("carryover_sides_start")["next_scenario"];
373  } else {
374  cfg_summary["scenario"] = cfg_save["scenario"];
375  }
376 
377  cfg_summary["difficulty"] = cfg_save["difficulty"];
378  cfg_summary["random_mode"] = cfg_save["random_mode"];
379 
380  cfg_summary["active_mods"] = cfg_save.child_or_empty("multiplayer")["active_mods"];
381  cfg_summary["campaign"] = cfg_save["campaign"];
382  cfg_summary["version"] = cfg_save["version"];
383  cfg_summary["corrupt"] = "";
384 
385  if(has_snapshot) {
386  cfg_summary["turn"] = cfg_snapshot["turn_at"];
387  if(cfg_snapshot["turns"] != "-1") {
388  cfg_summary["turn"] = cfg_summary["turn"].str() + "/" + cfg_snapshot["turns"].str();
389  }
390  }
391 
392  // Ensure we don't get duplicate [leader] tags
393  cfg_summary.clear_children("leader");
394 
395  // Find the human leaders so we can display their icons and names in the load menu.
396  config leader_config;
397 
398  bool shrouded = false;
399 
400  if(const config& snapshot = *(has_snapshot ? &cfg_snapshot : &cfg_replay_start)) {
401  for(const config& side : snapshot.child_range("side")) {
402  std::string leader;
403  std::string leader_image;
404  std::string leader_image_tc_modifier;
405  std::string leader_name;
406  int gold = side["gold"];
407  int units = 0, recall_units = 0;
408 
409  if(side["controller"] != team::CONTROLLER::enum_to_string(team::CONTROLLER::HUMAN)) {
410  continue;
411  }
412 
413  if(side["shroud"].to_bool()) {
414  shrouded = true;
415  }
416 
417  for(const config& u : side.child_range("unit")) {
418  if(u.has_attribute("x") && u.has_attribute("y")) {
419  units++;
420  } else {
421  recall_units++;
422  }
423 
424  // Only take the first leader
425  if(!leader.empty() || !u["canrecruit"].to_bool()) {
426  continue;
427  }
428 
429  const std::string tc_color = team::get_side_color_id_from_config(side);
430 
431  // Don't count it among the troops
432  units--;
433  leader = u["id"].str();
434  leader_name = u["name"].str();
435  leader_image = u["image"].str();
436  leader_image_tc_modifier = "~RC(" + u["flag_rgb"].str() + ">" + tc_color + ")";
437  }
438 
439  // We need a binary path-independent path to the leader image here so it can be displayed
440  // for campaign-specific units even when the campaign isn't loaded yet.
441  std::string leader_image_path = filesystem::get_independent_image_path(leader_image);
442 
443  // If the image path was found, we append the leader TC modifier. If it's not (such as in
444  // the case where the binary path hasn't been loaded yet, perhaps due to save_index being
445  // deleted), the unaltered image path is used and will be parsed by get_independent_image_path
446  // at runtime.
447  if(!leader_image_path.empty()) {
448  leader_image_path += leader_image_tc_modifier;
449 
450  leader_image = leader_image_path;
451  }
452 
453  leader_config["leader"] = leader;
454  leader_config["leader_name"] = leader_name;
455  leader_config["leader_image"] = leader_image;
456  leader_config["leader_image_tc_modifier"] = leader_image_tc_modifier;
457  leader_config["gold"] = gold;
458  leader_config["units"] = units;
459  leader_config["recall_units"] = recall_units;
460 
461  cfg_summary.add_child("leader", leader_config);
462  }
463  }
464 
465  if(!shrouded) {
466  if(has_snapshot) {
467  if(!cfg_snapshot.find_child("side", "shroud", "yes") && cfg_snapshot.has_attribute("map_data")) {
468  cfg_summary["map_data"] = cfg_snapshot["map_data"].str();
469  } else {
470  ERR_SAVE << "Not saving map because there is shroud" << std::endl;
471  }
472  } else if(has_replay) {
473  if(!cfg_replay_start.find_child("side", "shroud", "yes") && cfg_replay_start.has_attribute("map_data")) {
474  cfg_summary["map_data"] = cfg_replay_start["map_data"];
475  } else {
476  ERR_SAVE << "Not saving map because there is shroud" << std::endl;
477  }
478  }
479  }
480 }
481 
482 } // end namespace savegame
void remove_old_auto_saves(const int autosavemax, const int infinite_auto_saves)
Remove autosaves that are no longer needed (according to the autosave policy in the preferences)...
Definition: save_index.cpp:315
bool empty() const
Tests for an attribute that either was never set or was set to "".
int autosavemax()
Definition: game.cpp:819
config & child(config_key_type key, int n=0)
Returns the nth child with the given key, or a reference to an invalid config if there is none...
Definition: config.cpp:420
void rebuild(const std::string &name)
Definition: save_index.cpp:44
void read_save_file(const std::string &name, config &cfg, std::string *error_log)
Read the complete config information out of a savefile.
Definition: save_index.cpp:274
void clear_children(T... keys)
Definition: config.hpp:477
int dummy
Definition: lstrlib.cpp:1125
bool delete_file(const std::string &filename)
Definition: filesystem.cpp:906
static std::string get_side_color_id_from_config(const config &cfg)
Definition: team.cpp:999
config & find_child(config_key_type key, const std::string &name, const std::string &value)
Returns the first child of tag key with a name attribute containing value.
Definition: config.cpp:836
Variant for storing WML attributes.
bool operator()(const save_info &a, const save_info &b) const
Definition: save_index.cpp:234
void extract_summary_from_config(config &, config &)
Definition: save_index.cpp:352
void read_bz2(config &cfg, std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially bzip2_error.
Definition: parser.cpp:687
Error used when game loading fails.
Definition: game_errors.hpp:30
bool has_attribute(config_key_type key) const
Definition: config.cpp:213
#define a
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:412
void remove(const std::string &name)
Definition: save_index.cpp:70
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:266
child_itors child_range(config_key_type key)
Definition: config.cpp:362
std::vector< save_info > get_saves_list(const std::string *dir, const std::string *filter)
Get a list of available saves.
Definition: save_index.cpp:190
static lg::log_domain log_engine("engine")
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error)
Definition: filesystem.cpp:925
void set_modified(const std::string &name, const std::time_t &modified)
Definition: save_index.cpp:77
void clear()
Definition: config.cpp:863
#define d
Contains the exception interfaces used to signal completion of a scenario, campaign or turn...
void read_gz(config &cfg, std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially a gzip_error.
Definition: parser.cpp:681
-file sdl_utils.hpp
std::string get_saves_dir()
void remove_attribute(config_key_type key)
Definition: config.cpp:235
Definitions for the interface to Wesnoth Markup Language (WML).
const std::string dir
Definition: save_index.hpp:80
const config & summary() const
Definition: save_index.cpp:209
#define ERR_SAVE
Definition: save_index.cpp:35
#define b
std::string strftime(const std::string &format, const std::tm *time)
Definition: gettext.cpp:508
std::string format_time_local() const
Definition: save_index.cpp:214
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
Definition: parser.cpp:762
bool operator()(const std::string &filename) const
Definition: save_index.cpp:180
static UNUSEDNOWARN std::string _(const char *str)
Definition: gettext.hpp:91
static filesystem::scoped_istream find_save_file(const std::string &name, const std::vector< std::string > &suffixes)
Definition: save_index.cpp:258
void write_gz(std::ostream &out, const configr_of &cfg)
Definition: parser.cpp:781
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:625
void delete_game(const std::string &name)
Delete a savegame.
Definition: save_index.cpp:333
std::unique_ptr< std::istream > scoped_istream
Definition: filesystem.hpp:39
save_index_class save_index_manager
Definition: save_index.cpp:170
std::string get_independent_image_path(const std::string &filename)
Returns an image path to filename for binary path-independent use in saved games. ...
std::time_t to_time_t(std::time_t def=0) const
const char * what() const noexcept
Definition: exceptions.hpp:37
const std::string & name() const
Definition: save_index.hpp:36
const std::time_t & modified() const
Definition: save_index.hpp:41
std::unique_ptr< std::ostream > scoped_ostream
Definition: filesystem.hpp:40
bool countdown()
Definition: game.cpp:625
bool is_gzip_file(const std::string &filename)
Returns true if the file ends with &#39;.gz&#39;.
config & get(const std::string &name)
Definition: save_index.cpp:82
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs, file_name_option mode, file_filter_option filter, file_reorder_option reorder, file_tree_checksum *checksum)
Populates &#39;files&#39; with all the files and &#39;dirs&#39; with all the directories in dir.
Definition: filesystem.cpp:352
std::size_t i
Definition: function.cpp:933
logger & err()
Definition: log.cpp:78
An exception object used when an IO error occurs.
Definition: filesystem.hpp:48
#define log_scope(description)
Definition: log.hpp:186
static void fix_leader_image_path(config &data)
Definition: save_index.cpp:160
Declarations for File-IO.
static int sort(lua_State *L)
Definition: ltablib.cpp:411
#define LOG_SAVE
Definition: save_index.cpp:34
save_info operator()(const std::string &filename) const
Definition: save_index.cpp:345
config & add_child(config_key_type key)
Definition: config.cpp:476
compression::format save_compression_format()
Definition: game.cpp:871
bool is_bzip2_file(const std::string &filename)
Returns true if the file ends with &#39;.bz2&#39;.
Filename and modification date for a file list.
Definition: save_index.hpp:24
create_save_info(const std::string *d=nullptr)
Definition: save_index.cpp:340
std::time_t file_modified_time(const std::string &fname)
Get the modification time of a file.
double t
Definition: astarsearch.cpp:64
std::map< std::string, std::time_t > modified_
Definition: save_index.hpp:106
std::string format_time_summary() const
Definition: save_index.cpp:228
filename_filter(const std::string &filter)
Definition: save_index.cpp:175
Standard logging facilities (interface).
std::string message
Definition: exceptions.hpp:31
#define e
const config & child_or_empty(config_key_type key) const
Returns the first child with the given key, or an empty config if there is none.
Definition: config.cpp:453
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:68
static lg::log_domain log_enginerefac("enginerefac")
std::string get_save_index_file()
A structure for comparing to save_info objects based on their modified time.
Definition: save_index.hpp:59
std::string format_time_summary(std::time_t t)
bool use_twelve_hour_clock_format()
Definition: general.cpp:914
std::string::const_iterator iterator
Definition: tokenizer.hpp:24
bool empty() const
Definition: config.cpp:884
filesystem::scoped_ostream ostream_file(const std::string &fname, bool create_directory)
Definition: filesystem.cpp:963