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