The Battle for Wesnoth  1.15.0-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 http://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  if(a.modified() > b.modified()) {
237  return true;
238  } else if(a.modified() < b.modified()) {
239  return false;
240  } else if(a.name().find(_(" replay")) == std::string::npos && b.name().find(_(" replay")) != std::string::npos) {
241  // Special funky case; for files created in the same second,
242  // a replay file sorts less than a non-replay file. Prevents
243  // a timing-dependent bug where it may look like, at the end
244  // of a scenario, the replay and the autosave for the next
245  // scenario are displayed in the wrong order.
246  return true;
247  } else if(a.name().find(_(" replay")) != std::string::npos && b.name().find(_(" replay")) == std::string::npos) {
248  return false;
249  } else {
250  return a.name() > b.name();
251  }
252 }
253 
255  const std::string& name, const std::vector<std::string>& suffixes)
256 {
257  for(const std::string& suf : suffixes) {
258  filesystem::scoped_istream file_stream =
260 
261  if(!file_stream->fail()) {
262  return file_stream;
263  }
264  }
265 
266  LOG_SAVE << "Could not open supplied filename '" << name << "'\n";
267  throw game::load_game_failed();
268 }
269 
270 void read_save_file(const std::string& name, config& cfg, std::string* error_log)
271 {
272  static const std::vector<std::string> suffixes{"", ".gz", ".bz2"};
273  filesystem::scoped_istream file_stream = find_save_file(name, suffixes);
274 
275  cfg.clear();
276  try {
277  /*
278  * Test the modified name, since it might use a .gz
279  * file even when not requested.
280  */
281  if(filesystem::is_gzip_file(name)) {
282  read_gz(cfg, *file_stream);
283  } else if(filesystem::is_bzip2_file(name)) {
284  read_bz2(cfg, *file_stream);
285  } else {
286  read(cfg, *file_stream);
287  }
288  } catch(const std::ios_base::failure& e) {
289  LOG_SAVE << e.what();
290 
291  if(error_log) {
292  *error_log += e.what();
293  }
294  throw game::load_game_failed();
295  } catch(const config::error& err) {
296  LOG_SAVE << err.message;
297 
298  if(error_log) {
299  *error_log += err.message;
300  }
301 
302  throw game::load_game_failed();
303  }
304 
305  if(cfg.empty()) {
306  LOG_SAVE << "Could not parse file data into config\n";
307  throw game::load_game_failed();
308  }
309 }
310 
311 void remove_old_auto_saves(const int autosavemax, const int infinite_auto_saves)
312 {
313  const std::string auto_save = _("Auto-Save");
314 
315  int countdown = autosavemax;
316  if(countdown == infinite_auto_saves) {
317  return;
318  }
319 
320  std::vector<save_info> games = get_saves_list(nullptr, &auto_save);
321  for(std::vector<save_info>::iterator i = games.begin(); i != games.end(); ++i) {
322  if(countdown-- <= 0) {
323  LOG_SAVE << "Deleting savegame '" << i->name() << "'\n";
324  delete_game(i->name());
325  }
326  }
327 }
328 
329 void delete_game(const std::string& name)
330 {
332 
333  save_index_manager.remove(name);
334 }
335 
337  : dir(d ? *d : filesystem::get_saves_dir())
338 {
339 }
340 
341 save_info create_save_info::operator()(const std::string& filename) const
342 {
343  std::time_t modified = filesystem::file_modified_time(dir + "/" + filename);
344  save_index_manager.set_modified(filename, modified);
345  return save_info(filename, modified);
346 }
347 
348 void extract_summary_from_config(config& cfg_save, config& cfg_summary)
349 {
350  const config& cfg_snapshot = cfg_save.child("snapshot");
351 
352  // Servergenerated replays contain [scenario] and no [replay_start]
353  const config& cfg_replay_start = cfg_save.child("replay_start")
354  ? cfg_save.child("replay_start")
355  : cfg_save.child("scenario");
356 
357  const config& cfg_replay = cfg_save.child("replay");
358  const bool has_replay = cfg_replay && !cfg_replay.empty();
359  const bool has_snapshot = cfg_snapshot && cfg_snapshot.has_child("side");
360 
361  cfg_summary["replay"] = has_replay;
362  cfg_summary["snapshot"] = has_snapshot;
363 
364  cfg_summary["label"] = cfg_save["label"];
365  cfg_summary["campaign_type"] = cfg_save["campaign_type"];
366 
367  if(cfg_save.has_child("carryover_sides_start")) {
368  cfg_summary["scenario"] = cfg_save.child("carryover_sides_start")["next_scenario"];
369  } else {
370  cfg_summary["scenario"] = cfg_save["scenario"];
371  }
372 
373  cfg_summary["difficulty"] = cfg_save["difficulty"];
374  cfg_summary["random_mode"] = cfg_save["random_mode"];
375 
376  cfg_summary["campaign"] = cfg_save["campaign"];
377  cfg_summary["version"] = cfg_save["version"];
378  cfg_summary["corrupt"] = "";
379 
380  if(has_snapshot) {
381  cfg_summary["turn"] = cfg_snapshot["turn_at"];
382  if(cfg_snapshot["turns"] != "-1") {
383  cfg_summary["turn"] = cfg_summary["turn"].str() + "/" + cfg_snapshot["turns"].str();
384  }
385  }
386 
387  // Ensure we don't get duplicate [leader] tags
388  cfg_summary.clear_children("leader");
389 
390  // Find the human leaders so we can display their icons and names in the load menu.
391  config leader_config;
392 
393  bool shrouded = false;
394 
395  if(const config& snapshot = *(has_snapshot ? &cfg_snapshot : &cfg_replay_start)) {
396  for(const config& side : snapshot.child_range("side")) {
397  std::string leader;
398  std::string leader_image;
399  std::string leader_image_tc_modifier;
400  std::string leader_name;
401  int gold = side["gold"];
402  int units = 0, recall_units = 0;
403 
404  if(side["controller"] != team::CONTROLLER::enum_to_string(team::CONTROLLER::HUMAN)) {
405  continue;
406  }
407 
408  if(side["shroud"].to_bool()) {
409  shrouded = true;
410  }
411 
412  for(const config& u : side.child_range("unit")) {
413  if(u.has_attribute("x") && u.has_attribute("y")) {
414  units++;
415  } else {
416  recall_units++;
417  }
418 
419  // Only take the first leader
420  if(!leader.empty() || !u["canrecruit"].to_bool()) {
421  continue;
422  }
423 
424  const std::string tc_color = team::get_side_color_id_from_config(side);
425 
426  // Don't count it among the troops
427  units--;
428  leader = u["id"].str();
429  leader_name = u["name"].str();
430  leader_image = u["image"].str();
431  leader_image_tc_modifier = "~RC(" + u["flag_rgb"].str() + ">" + tc_color + ")";
432  }
433 
434  // We need a binary path-independent path to the leader image here so it can be displayed
435  // for campaign-specific units even when the campaign isn't loaded yet.
436  std::string leader_image_path = filesystem::get_independent_image_path(leader_image);
437 
438  // If the image path was found, we append the leader TC modifier. If it's not (such as in
439  // the case where the binary path hasn't been loaded yet, perhaps due to save_index being
440  // deleted), the unaltered image path is used and will be parsed by get_independent_image_path
441  // at runtime.
442  if(!leader_image_path.empty()) {
443  leader_image_path += leader_image_tc_modifier;
444 
445  leader_image = leader_image_path;
446  }
447 
448  leader_config["leader"] = leader;
449  leader_config["leader_name"] = leader_name;
450  leader_config["leader_image"] = leader_image;
451  leader_config["leader_image_tc_modifier"] = leader_image_tc_modifier;
452  leader_config["gold"] = gold;
453  leader_config["units"] = units;
454  leader_config["recall_units"] = recall_units;
455 
456  cfg_summary.add_child("leader", leader_config);
457  }
458  }
459 
460  if(!shrouded) {
461  if(has_snapshot) {
462  if(!cfg_snapshot.find_child("side", "shroud", "yes") && cfg_snapshot.has_attribute("map_data")) {
463  cfg_summary["map_data"] = cfg_snapshot["map_data"].str();
464  } else {
465  ERR_SAVE << "Not saving map because there is shroud" << std::endl;
466  }
467  } else if(has_replay) {
468  if(!cfg_replay_start.find_child("side", "shroud", "yes") && cfg_replay_start.has_attribute("map_data")) {
469  cfg_summary["map_data"] = cfg_replay_start["map_data"];
470  } else {
471  ERR_SAVE << "Not saving map because there is shroud" << std::endl;
472  }
473  }
474  }
475 }
476 
477 } // 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:311
bool empty() const
Tests for an attribute that either was never set or was set to "".
int autosavemax()
Definition: game.cpp:809
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:423
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:270
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:875
static std::string get_side_color_id_from_config(const config &cfg)
Definition: team.cpp:977
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:789
Variant for storing WML attributes.
static bool file_exists(const fs::path &fpath)
Definition: filesystem.cpp:300
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:348
void read_bz2(config &cfg, std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially bzip2_error.
Definition: parser.cpp:674
Error used when game loading fails.
Definition: game_errors.hpp:30
bool has_attribute(config_key_type key) const
Definition: config.cpp:217
#define a
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:416
void remove(const std::string &name)
Definition: save_index.cpp:70
child_itors child_range(config_key_type key)
Definition: config.cpp:366
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:894
void set_modified(const std::string &name, const std::time_t &modified)
Definition: save_index.cpp:77
void clear()
Definition: config.cpp:816
#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:668
-file sdl_utils.hpp
std::string get_saves_dir()
void remove_attribute(config_key_type key)
Definition: config.cpp:239
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:749
bool operator()(const std::string &filename) const
Definition: save_index.cpp:180
static UNUSEDNOWARN std::string _(const char *str)
Definition: gettext.hpp:89
static filesystem::scoped_istream find_save_file(const std::string &name, const std::vector< std::string > &suffixes)
Definition: save_index.cpp:254
void write_gz(std::ostream &out, const configr_of &cfg)
Definition: parser.cpp:768
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:612
void delete_game(const std::string &name)
Delete a savegame.
Definition: save_index.cpp:329
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:615
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:386
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:341
config & add_child(config_key_type key)
Definition: config.cpp:479
compression::format save_compression_format()
Definition: game.cpp:872
The paths manager is responsible for recording the various paths that binary files may be located at...
Definition: fs_commit.cpp:37
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:336
std::time_t file_modified_time(const std::string &fname)
Get the modification time of a file.
Definition: filesystem.cpp:994
double t
Definition: astarsearch.cpp:63
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
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:906
std::string::const_iterator iterator
Definition: tokenizer.hpp:24
bool empty() const
Definition: config.cpp:837
filesystem::scoped_ostream ostream_file(const std::string &fname, bool create_directory)
Definition: filesystem.cpp:932