The Battle for Wesnoth  1.15.12+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(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(dir_, 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_children("save", [&name](const config& d) { return name == d["save"]; });
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 
95 const std::string& save_index_class::dir() const
96 {
97  return dir_;
98 }
99 
101 {
102  config &root = data();
103 
104  std::vector<std::string> filenames;
105  filesystem::get_files_in_dir(dir(), &filenames);
106 
107  if(root.all_children_count() > filenames.size()) {
108  root.remove_children("save", [&filenames](const config& d)
109  {
110  return std::find(filenames.begin(), filenames.end(), d["save"]) == filenames.end();
111  }
112  );
113  }
114 }
115 
117 {
118  log_scope("write_save_index()");
119 
120  if(read_only_) {
121  LOG_SAVE << "no-op: read_only instance";
122  return;
123  }
124 
125  if(clean_up_index_) {
126  clean_up_index();
127  clean_up_index_ = false;
128  }
129 
130  try {
132 
134  // TODO: maybe allow writing this using bz2 too?
135  write_gz(*stream, data());
136  } else {
137  write(*stream, data());
138  }
139  } catch(const filesystem::io_exception& e) {
140  ERR_SAVE << "error writing to save index file: '" << e.what() << "'" << std::endl;
141  }
142 }
143 
145  : loaded_(false)
146  , data_()
147  , modified_()
148  , dir_(dir)
149  , read_only_(true)
150  , clean_up_index_(true)
151 {
152 }
153 
156 {
157  read_only_ = false;
158 }
159 
160 config& save_index_class::data(const std::string& name)
161 {
162  config& cfg = data();
163  if(config& sv = cfg.find_child("save", "save", name)) {
165  return sv;
166  }
167 
168  config& res = cfg.add_child("save");
169  res["save"] = name;
170  return res;
171 }
172 
174 {
175  const std::string si_file = filesystem::get_save_index_file();
176 
177  // Don't try to load the file if it doesn't exist.
178  if(loaded_ == false && filesystem::file_exists(si_file)) {
179  try {
181  try {
182  read_gz(data_, *stream);
183  } catch(const boost::iostreams::gzip_error&) {
184  stream->seekg(0);
185  read(data_, *stream);
186  }
187  } catch(const filesystem::io_exception& e) {
188  ERR_SAVE << "error reading save index: '" << e.what() << "'" << std::endl;
189  } catch(const config::error& e) {
190  ERR_SAVE << "error parsing save index config file:\n" << e.message << std::endl;
191  data_.clear();
192  }
193 
194  loaded_ = true;
195  }
196 
197  return data_;
198 }
199 
201 {
202  for(config& leader : data.child_range("leader")) {
203  std::string leader_image = leader["leader_image"];
204  boost::algorithm::replace_all(leader_image, "\\", "/");
205 
206  leader["leader_image"] = leader_image;
207  }
208 }
209 
210 std::shared_ptr<save_index_class> save_index_class::default_saves_dir()
211 {
212  static auto instance = std::make_shared<save_index_class>(create_for_default_saves_dir::yes);
213  return instance;
214 }
215 
217 {
218 public:
219  filename_filter(const std::string& filter)
220  : filter_(filter)
221  {
222  }
223 
224  bool operator()(const std::string& filename) const
225  {
226  return filename.end() == std::search(filename.begin(), filename.end(), filter_.begin(), filter_.end());
227  }
228 
229 private:
230  std::string filter_;
231 };
232 
233 /** Get a list of available saves. */
234 std::vector<save_info> save_index_class::get_saves_list(const std::string* filter)
235 {
236  create_save_info creator(shared_from_this());
237 
238  std::vector<std::string> filenames;
239  filesystem::get_files_in_dir(dir(), &filenames);
240 
241  if(filter) {
242  filenames.erase(
243  std::remove_if(filenames.begin(), filenames.end(), filename_filter(*filter)), filenames.end());
244  }
245 
246  std::vector<save_info> result;
247  std::transform(filenames.begin(), filenames.end(), std::back_inserter(result), creator);
248  std::sort(result.begin(), result.end(), save_info_less_time());
249 
250  return result;
251 }
252 
254 {
255  return save_index_->get(name());
256 }
257 
258 std::string save_info::format_time_local() const
259 {
260  if(std::tm* tm_l = std::localtime(&modified())) {
262  ? _("%a %b %d %I:%M %p %Y")
263  : _("%a %b %d %H:%M %Y");
264 
265  return translation::strftime(format, tm_l);
266  }
267 
268  LOG_SAVE << "localtime() returned null for time " << this->modified() << ", save " << name();
269  return "";
270 }
271 
273 {
274  std::time_t t = modified();
275  return utils::format_time_summary(t);
276 }
277 
279 {
280  // This translatable string must be same one as in replay_savegame::create_initial_filename.
281  // TODO: we really shouldn't be relying on translatable strings like this, especially since
282  // old savefiles may have been created in a different language than the current UI language
283  const std::string replay_str = " " + _("replay");
284  if(a.modified() > b.modified()) {
285  return true;
286  } else if(a.modified() < b.modified()) {
287  return false;
288  } else if(a.name().find(replay_str) == std::string::npos && b.name().find(replay_str) != std::string::npos) {
289  // Special funky case; for files created in the same second,
290  // a replay file sorts less than a non-replay file. Prevents
291  // a timing-dependent bug where it may look like, at the end
292  // of a scenario, the replay and the autosave for the next
293  // scenario are displayed in the wrong order.
294  return true;
295  } else if(a.name().find(replay_str) != std::string::npos && b.name().find(replay_str) == std::string::npos) {
296  return false;
297  } else {
298  return a.name() > b.name();
299  }
300 }
301 
303  const std::string& name, const std::vector<std::string>& suffixes)
304 {
305  for(const std::string& suf : suffixes) {
306  filesystem::scoped_istream file_stream =
307  filesystem::istream_file(dir + "/" + name + suf);
308 
309  if(!file_stream->fail()) {
310  return file_stream;
311  }
312  }
313 
314  LOG_SAVE << "Could not open supplied filename '" << name << "'\n";
315  throw game::load_game_failed();
316 }
317 
318 void read_save_file(const std::string& dir, const std::string& name, config& cfg, std::string* error_log)
319 {
320  static const std::vector<std::string> suffixes{"", ".gz", ".bz2"};
321  filesystem::scoped_istream file_stream = find_save_file(dir, name, suffixes);
322 
323  cfg.clear();
324  try {
325  /*
326  * Test the modified name, since it might use a .gz
327  * file even when not requested.
328  */
329  if(filesystem::is_gzip_file(name)) {
330  read_gz(cfg, *file_stream);
331  } else if(filesystem::is_bzip2_file(name)) {
332  read_bz2(cfg, *file_stream);
333  } else {
334  read(cfg, *file_stream);
335  }
336  } catch(const std::ios_base::failure& e) {
337  LOG_SAVE << e.what();
338 
339  if(error_log) {
340  *error_log += e.what();
341  }
342  throw game::load_game_failed();
343  } catch(const config::error& err) {
344  LOG_SAVE << err.message;
345 
346  if(error_log) {
347  *error_log += err.message;
348  }
349 
350  throw game::load_game_failed();
351  }
352 
353  if(cfg.empty()) {
354  LOG_SAVE << "Could not parse file data into config\n";
355  throw game::load_game_failed();
356  }
357 }
358 
359 void save_index_class::delete_old_auto_saves(const int autosavemax, const int infinite_auto_saves)
360 {
361  log_scope("delete_old_auto_saves()");
362  if(read_only_) {
363  LOG_SAVE << "no-op: read_only instance";
364  return;
365  }
366 
367  const std::string auto_save = _("Auto-Save");
368 
369  int countdown = autosavemax;
370  if(countdown == infinite_auto_saves) {
371  return;
372  }
373 
374  std::vector<save_info> games = get_saves_list(&auto_save);
375  for(std::vector<save_info>::iterator i = games.begin(); i != games.end(); ++i) {
376  if(countdown-- <= 0) {
377  LOG_SAVE << "Deleting savegame '" << i->name() << "'\n";
378  delete_game(i->name());
379  }
380  }
381 }
382 
383 void save_index_class::delete_game(const std::string& name)
384 {
385  if(read_only_) {
386  log_scope("delete_game()");
387  LOG_SAVE << "no-op: read_only instance";
388  return;
389  }
390 
391  filesystem::delete_file(dir() + "/" + name);
392  remove(name);
393 }
394 
395 create_save_info::create_save_info(const std::shared_ptr<save_index_class>& manager)
396  : manager_(manager)
397 {
398 }
399 
400 save_info create_save_info::operator()(const std::string& filename) const
401 {
402  std::time_t modified = filesystem::file_modified_time(manager_->dir() + "/" + filename);
403  manager_->set_modified(filename, modified);
404  return save_info(filename, manager_, modified);
405 }
406 
407 void extract_summary_from_config(config& cfg_save, config& cfg_summary)
408 {
409  const config& cfg_snapshot = cfg_save.child("snapshot");
410 
411  // Servergenerated replays contain [scenario] and no [replay_start]
412  const config& cfg_replay_start = cfg_save.child("replay_start")
413  ? cfg_save.child("replay_start")
414  : cfg_save.child("scenario");
415 
416  const config& cfg_replay = cfg_save.child("replay");
417  const bool has_replay = cfg_replay && !cfg_replay.empty();
418  const bool has_snapshot = cfg_snapshot && cfg_snapshot.has_child("side");
419 
420  cfg_summary["replay"] = has_replay;
421  cfg_summary["snapshot"] = has_snapshot;
422 
423  cfg_summary["label"] = cfg_save["label"];
424  cfg_summary["campaign_type"] = cfg_save["campaign_type"];
425 
426  if(cfg_save.has_child("carryover_sides_start")) {
427  cfg_summary["scenario"] = cfg_save.child("carryover_sides_start")["next_scenario"];
428  } else {
429  cfg_summary["scenario"] = cfg_save["scenario"];
430  }
431 
432  cfg_summary["difficulty"] = cfg_save["difficulty"];
433  cfg_summary["random_mode"] = cfg_save["random_mode"];
434 
435  cfg_summary["active_mods"] = cfg_save.child_or_empty("multiplayer")["active_mods"];
436  cfg_summary["campaign"] = cfg_save["campaign"];
437  cfg_summary["version"] = cfg_save["version"];
438  cfg_summary["corrupt"] = "";
439 
440  if(has_snapshot) {
441  cfg_summary["turn"] = cfg_snapshot["turn_at"];
442  if(cfg_snapshot["turns"] != "-1") {
443  cfg_summary["turn"] = cfg_summary["turn"].str() + "/" + cfg_snapshot["turns"].str();
444  }
445  }
446 
447  // Ensure we don't get duplicate [leader] tags
448  cfg_summary.clear_children("leader");
449 
450  // Find the human leaders so we can display their icons and names in the load menu.
451  config leader_config;
452 
453  bool shrouded = false;
454 
455  if(const config& snapshot = *(has_snapshot ? &cfg_snapshot : &cfg_replay_start)) {
456  for(const config& side : snapshot.child_range("side")) {
457  std::string leader;
458  std::string leader_image;
459  std::string leader_image_tc_modifier;
460  std::string leader_name;
461  int gold = side["gold"];
462  int units = 0, recall_units = 0;
463 
464  if(side["controller"] != team::CONTROLLER::enum_to_string(team::CONTROLLER::HUMAN)) {
465  continue;
466  }
467 
468  if(side["shroud"].to_bool()) {
469  shrouded = true;
470  }
471 
472  for(const config& u : side.child_range("unit")) {
473  if(u.has_attribute("x") && u.has_attribute("y")) {
474  units++;
475  } else {
476  recall_units++;
477  }
478 
479  // Only take the first leader
480  if(!leader.empty() || !u["canrecruit"].to_bool()) {
481  continue;
482  }
483 
484  const std::string tc_color = team::get_side_color_id_from_config(side);
485 
486  // Don't count it among the troops
487  units--;
488  leader = u["id"].str();
489  leader_name = u["name"].str();
490  leader_image = u["image"].str();
491  leader_image_tc_modifier = "~RC(" + u["flag_rgb"].str() + ">" + tc_color + ")";
492  }
493 
494  // We need a binary path-independent path to the leader image here so it can be displayed
495  // for campaign-specific units even when the campaign isn't loaded yet.
496  std::string leader_image_path = filesystem::get_independent_binary_file_path("images", leader_image);
497 
498  // If the image path was found, we append the leader TC modifier. If it's not (such as in
499  // the case where the binary path hasn't been loaded yet, perhaps due to save_index being
500  // deleted), the unaltered image path is used and will be parsed by get_independent_binary_file_path
501  // at runtime.
502  if(!leader_image_path.empty()) {
503  leader_image_path += leader_image_tc_modifier;
504 
505  leader_image = leader_image_path;
506  }
507 
508  leader_config["leader"] = leader;
509  leader_config["leader_name"] = leader_name;
510  leader_config["leader_image"] = leader_image;
511  leader_config["leader_image_tc_modifier"] = leader_image_tc_modifier;
512  leader_config["gold"] = gold;
513  leader_config["units"] = units;
514  leader_config["recall_units"] = recall_units;
515 
516  cfg_summary.add_child("leader", leader_config);
517  }
518  }
519 
520  if(!shrouded) {
521  if(has_snapshot) {
522  if(!cfg_snapshot.find_child("side", "shroud", "yes") && cfg_snapshot.has_attribute("map_data")) {
523  cfg_summary["map_data"] = cfg_snapshot["map_data"].str();
524  } else {
525  ERR_SAVE << "Not saving map because there is shroud" << std::endl;
526  }
527  } else if(has_replay) {
528  if(!cfg_replay_start.find_child("side", "shroud", "yes") && cfg_replay_start.has_attribute("map_data")) {
529  cfg_summary["map_data"] = cfg_replay_start["map_data"];
530  } else {
531  ERR_SAVE << "Not saving map because there is shroud" << std::endl;
532  }
533  }
534  }
535 }
536 
537 } // end namespace savegame
bool clean_up_index_
Flag to only run the clean_up_index method once.
Definition: save_index.hpp:140
bool empty() const
Tests for an attribute that either was never set or was set to "".
int autosavemax()
Definition: game.cpp:799
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:414
void delete_game(const std::string &name)
Delete a savegame, including deleting the underlying file.
Definition: save_index.cpp:383
save_index_class(const std::string &dir)
Constructor for a read-only instance.
Definition: save_index.cpp:144
void rebuild(const std::string &name)
Definition: save_index.cpp:44
void clear_children(T... keys)
Definition: config.hpp:526
int dummy
Definition: lstrlib.cpp:1347
bool delete_file(const std::string &filename)
Definition: filesystem.cpp:984
static std::string get_side_color_id_from_config(const config &cfg)
Definition: team.cpp:998
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:860
Variant for storing WML attributes.
bool operator()(const save_info &a, const save_info &b) const
Definition: save_index.cpp:278
void extract_summary_from_config(config &, config &)
Definition: save_index.cpp:407
void read_bz2(config &cfg, std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially bzip2_error.
Definition: parser.cpp:688
Error used when game loading fails.
Definition: game_errors.hpp:30
bool has_attribute(config_key_type key) const
Definition: config.cpp:207
#define a
static std::shared_ptr< save_index_class > default_saves_dir()
Returns an instance for managing saves in filesystem::get_saves_dir()
Definition: save_index.cpp:210
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:406
void remove(const std::string &name)
Delete a savegame from the index, without deleting the underlying file.
Definition: save_index.cpp:70
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:263
void write_save_index()
Sync to disk, no-op if read_only_ is set.
Definition: save_index.cpp:116
child_itors child_range(config_key_type key)
Definition: config.cpp:356
static lg::log_domain log_engine("engine")
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error)
const attribute_value * get(config_key_type key) const
Returns a pointer to the attribute with the given key or nullptr if it does not exist.
Definition: config.cpp:766
std::vector< save_info > get_saves_list(const std::string *filter=nullptr)
Get a list of available saves.
Definition: save_index.cpp:234
void set_modified(const std::string &name, const std::time_t &modified)
Definition: save_index.cpp:77
void clear()
Definition: config.cpp:895
#define d
Contains the exception interfaces used to signal completion of a scenario, campaign or turn...
const std::string dir_
Definition: save_index.hpp:133
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:682
static std::string _(const char *str)
Definition: gettext.hpp:92
std::string get_saves_dir()
Definitions for the interface to Wesnoth Markup Language (WML).
void read_save_file(const std::string &dir, const std::string &name, config &cfg, std::string *error_log)
Read the complete config information out of a savefile.
Definition: save_index.cpp:318
filesystem::scoped_ostream ostream_file(const std::string &fname, std::ios_base::openmode mode, bool create_directory)
const config & summary() const
Definition: save_index.cpp:253
std::string get_independent_binary_file_path(const std::string &type, const std::string &filename)
Returns an asset path to filename for binary path-independent use in saved games. ...
std::shared_ptr< save_index_class > manager_
Definition: save_index.hpp:76
#define ERR_SAVE
Definition: save_index.cpp:35
#define b
std::string strftime(const std::string &format, const std::tm *time)
Definition: gettext.cpp:506
std::string format_time_local() const
Definition: save_index.cpp:258
create_for_default_saves_dir
Syntatic sugar for choosing which constructor to use.
Definition: save_index.hpp:87
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
Definition: parser.cpp:763
bool operator()(const std::string &filename) const
Definition: save_index.cpp:224
void clean_up_index()
Deletes non-existent save files from the index.
Definition: save_index.cpp:100
void write_gz(std::ostream &out, const configr_of &cfg)
Definition: parser.cpp:782
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:626
bool read_only_
The instance for default_saves_dir() writes a cache file.
Definition: save_index.hpp:138
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs, name_mode mode, filter_mode filter, reorder_mode 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:349
unsigned all_children_count() const
Definition: config.cpp:396
std::unique_ptr< std::istream > scoped_istream
Definition: filesystem.hpp:37
std::time_t to_time_t(std::time_t def=0) const
const char * what() const noexcept
Definition: exceptions.hpp:35
const std::string & name() const
Definition: save_index.hpp:39
static filesystem::scoped_istream find_save_file(const std::string &dir, const std::string &name, const std::vector< std::string > &suffixes)
Definition: save_index.cpp:302
const std::time_t & modified() const
Definition: save_index.hpp:44
std::unique_ptr< std::ostream > scoped_ostream
Definition: filesystem.hpp:38
bool countdown()
Definition: game.cpp:609
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
std::size_t i
Definition: function.cpp:940
logger & err()
Definition: log.cpp:76
const std::string & dir() const
Definition: save_index.cpp:95
An exception object used when an IO error occurs.
Definition: filesystem.hpp:46
void delete_old_auto_saves(const int autosavemax, const int infinite_auto_saves)
Delete autosaves that are no longer needed (according to the autosave policy in the preferences)...
Definition: save_index.cpp:359
#define log_scope(description)
Definition: log.hpp:206
static void fix_leader_image_path(config &data)
Definition: save_index.cpp:200
Declarations for File-IO.
static int sort(lua_State *L)
Definition: ltablib.cpp:397
#define LOG_SAVE
Definition: save_index.cpp:34
save_info operator()(const std::string &filename) const
Definition: save_index.cpp:400
config & add_child(config_key_type key)
Definition: config.cpp:500
compression::format save_compression_format()
Definition: game.cpp:851
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:26
create_save_info(const std::shared_ptr< save_index_class > &)
Definition: save_index.cpp:395
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:132
std::string format_time_summary() const
Definition: save_index.cpp:272
filename_filter(const std::string &filter)
Definition: save_index.cpp:219
Standard logging facilities (interface).
std::string message
Definition: exceptions.hpp:29
#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:477
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:59
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:63
std::string format_time_summary(std::time_t t)
bool use_twelve_hour_clock_format()
Definition: general.cpp:928
std::string::const_iterator iterator
Definition: tokenizer.hpp:24
bool empty() const
Definition: config.cpp:916
void remove_children(config_key_type key, std::function< bool(const config &)> p)
Removes all children with tag key for which p returns true.
Definition: config.cpp:731