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 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  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["active_mods"] = cfg_save.child_or_empty("multiplayer")["active_mods"];
377  cfg_summary["campaign"] = cfg_save["campaign"];
378  cfg_summary["version"] = cfg_save["version"];
379  cfg_summary["corrupt"] = "";
380 
381  if(has_snapshot) {
382  cfg_summary["turn"] = cfg_snapshot["turn_at"];
383  if(cfg_snapshot["turns"] != "-1") {
384  cfg_summary["turn"] = cfg_summary["turn"].str() + "/" + cfg_snapshot["turns"].str();
385  }
386  }
387 
388  // Ensure we don't get duplicate [leader] tags
389  cfg_summary.clear_children("leader");
390 
391  // Find the human leaders so we can display their icons and names in the load menu.
392  config leader_config;
393 
394  bool shrouded = false;
395 
396  if(const config& snapshot = *(has_snapshot ? &cfg_snapshot : &cfg_replay_start)) {
397  for(const config& side : snapshot.child_range("side")) {
398  std::string leader;
399  std::string leader_image;
400  std::string leader_image_tc_modifier;
401  std::string leader_name;
402  int gold = side["gold"];
403  int units = 0, recall_units = 0;
404 
405  if(side["controller"] != team::CONTROLLER::enum_to_string(team::CONTROLLER::HUMAN)) {
406  continue;
407  }
408 
409  if(side["shroud"].to_bool()) {
410  shrouded = true;
411  }
412 
413  for(const config& u : side.child_range("unit")) {
414  if(u.has_attribute("x") && u.has_attribute("y")) {
415  units++;
416  } else {
417  recall_units++;
418  }
419 
420  // Only take the first leader
421  if(!leader.empty() || !u["canrecruit"].to_bool()) {
422  continue;
423  }
424 
425  const std::string tc_color = team::get_side_color_id_from_config(side);
426 
427  // Don't count it among the troops
428  units--;
429  leader = u["id"].str();
430  leader_name = u["name"].str();
431  leader_image = u["image"].str();
432  leader_image_tc_modifier = "~RC(" + u["flag_rgb"].str() + ">" + tc_color + ")";
433  }
434 
435  // We need a binary path-independent path to the leader image here so it can be displayed
436  // for campaign-specific units even when the campaign isn't loaded yet.
437  std::string leader_image_path = filesystem::get_independent_image_path(leader_image);
438 
439  // If the image path was found, we append the leader TC modifier. If it's not (such as in
440  // the case where the binary path hasn't been loaded yet, perhaps due to save_index being
441  // deleted), the unaltered image path is used and will be parsed by get_independent_image_path
442  // at runtime.
443  if(!leader_image_path.empty()) {
444  leader_image_path += leader_image_tc_modifier;
445 
446  leader_image = leader_image_path;
447  }
448 
449  leader_config["leader"] = leader;
450  leader_config["leader_name"] = leader_name;
451  leader_config["leader_image"] = leader_image;
452  leader_config["leader_image_tc_modifier"] = leader_image_tc_modifier;
453  leader_config["gold"] = gold;
454  leader_config["units"] = units;
455  leader_config["recall_units"] = recall_units;
456 
457  cfg_summary.add_child("leader", leader_config);
458  }
459  }
460 
461  if(!shrouded) {
462  if(has_snapshot) {
463  if(!cfg_snapshot.find_child("side", "shroud", "yes") && cfg_snapshot.has_attribute("map_data")) {
464  cfg_summary["map_data"] = cfg_snapshot["map_data"].str();
465  } else {
466  ERR_SAVE << "Not saving map because there is shroud" << std::endl;
467  }
468  } else if(has_replay) {
469  if(!cfg_replay_start.find_child("side", "shroud", "yes") && cfg_replay_start.has_attribute("map_data")) {
470  cfg_summary["map_data"] = cfg_replay_start["map_data"];
471  } else {
472  ERR_SAVE << "Not saving map because there is shroud" << std::endl;
473  }
474  }
475  }
476 }
477 
478 } // 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:808
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:509
int dummy
Definition: lstrlib.cpp:1125
bool delete_file(const std::string &filename)
Definition: filesystem.cpp:863
static std::string get_side_color_id_from_config(const config &cfg)
Definition: team.cpp:979
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:838
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: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:677
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
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:280
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:882
void set_modified(const std::string &name, const std::time_t &modified)
Definition: save_index.cpp:77
void clear()
Definition: config.cpp:865
#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:671
-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
static const char * name(const std::vector< SDL_Joystick *> &joysticks, const std::size_t index)
Definition: joystick.cpp:48
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:752
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:771
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:615
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:614
bool is_gzip_file(const std::string &filename)
Returns true if the file ends with &#39;.gz&#39;.
Definition: filesystem.cpp:993
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:366
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:871
bool is_bzip2_file(const std::string &filename)
Returns true if the file ends with &#39;.bz2&#39;.
Definition: filesystem.cpp:998
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:982
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:456
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:92
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:1042
std::string::const_iterator iterator
Definition: tokenizer.hpp:24
bool empty() const
Definition: config.cpp:886
filesystem::scoped_ostream ostream_file(const std::string &fname, bool create_directory)
Definition: filesystem.cpp:920