The Battle for Wesnoth  1.13.11+dev
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
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 "formula/string_utils.hpp"
22 #include "game_end_exceptions.hpp"
23 #include "game_errors.hpp"
24 #include "gettext.hpp"
25 #include "log.hpp"
26 #include "preferences/game.hpp"
28 #include "serialization/parser.hpp"
29 #include "team.hpp"
30 
31 #include <boost/algorithm/string/replace.hpp>
32 #include <boost/iostreams/filter/gzip.hpp>
33 
34 static lg::log_domain log_engine("engine");
35 #define LOG_SAVE LOG_STREAM(info, log_engine)
36 #define ERR_SAVE LOG_STREAM(err, log_engine)
37 
38 static lg::log_domain log_enginerefac("enginerefac");
39 #define LOG_RG LOG_STREAM(info, log_enginerefac)
40 
41 namespace savegame
42 {
44 
46 {
47  time_t modified = filesystem::file_modified_time(filesystem::get_saves_dir() + "/" + name);
48  rebuild(name, modified);
49 }
50 
51 void save_index_class::rebuild(const std::string& name, const time_t& modified)
52 {
53  log_scope("load_summary_from_file");
54 
55  config& summary = data(name);
56 
57  try {
58  config full;
60  read_save_file(name, full, &dummy);
61 
62  extract_summary_from_config(full, summary);
63  } catch(game::load_game_failed&) {
64  summary["corrupt"] = true;
65  }
66 
67  summary["mod_time"] = std::to_string(static_cast<int>(modified));
69 }
70 
72 {
73  config& root = data();
74  root.remove_attribute(name);
76 }
77 
78 void save_index_class::set_modified(const std::string& name, const time_t& modified)
79 {
80  modified_[name] = modified;
81 }
82 
84 {
85  config& result = data(name);
86  time_t m = modified_[name];
87 
88  config::attribute_value& mod_time = result["mod_time"];
89  if(mod_time.empty() || static_cast<time_t>(mod_time.to_int()) != m) {
90  rebuild(name, m);
91  }
92 
93  return result;
94 }
95 
97 {
98  log_scope("write_save_index()");
99 
100  try {
102 
104  // TODO: maybe allow writing this using bz2 too?
105  write_gz(*stream, data());
106  } else {
107  write(*stream, data());
108  }
109  } catch(filesystem::io_exception& e) {
110  ERR_SAVE << "error writing to save index file: '" << e.what() << "'" << std::endl;
111  }
112 }
113 
115  : loaded_(false)
116  , data_()
117  , modified_()
118 {
119 }
120 
122 {
123  config& cfg = data();
124  if(config& sv = cfg.find_child("save", "save", name)) {
126  return sv;
127  }
128 
129  config& res = cfg.add_child("save");
130  res["save"] = name;
131  return res;
132 }
133 
135 {
136  if(loaded_ == false) {
137  try {
139  try {
140  read_gz(data_, *stream);
141  } catch(boost::iostreams::gzip_error&) {
142  stream->seekg(0);
143  read(data_, *stream);
144  }
145  } catch(filesystem::io_exception& e) {
146  ERR_SAVE << "error reading save index: '" << e.what() << "'" << std::endl;
147  } catch(config::error& e) {
148  ERR_SAVE << "error parsing save index config file:\n" << e.message << std::endl;
149  data_.clear();
150  }
151 
152  loaded_ = true;
153  }
154 
155  return data_;
156 }
157 
159 {
160  for(config& leader : data.child_range("leader")) {
161  std::string leader_image = leader["leader_image"];
162  boost::algorithm::replace_all(leader_image, "\\", "/");
163 
164  leader["leader_image"] = leader_image;
165  }
166 }
167 
169 
171 {
172 public:
174  : filter_(filter)
175  {
176  }
177 
178  bool operator()(const std::string& filename) const
179  {
180  return filename.end() == std::search(filename.begin(), filename.end(), filter_.begin(), filter_.end());
181  }
182 
183 private:
185 };
186 
187 /** Get a list of available saves. */
188 std::vector<save_info> get_saves_list(const std::string* dir, const std::string* filter)
189 {
190  create_save_info creator(dir);
191 
192  std::vector<std::string> filenames;
193  filesystem::get_files_in_dir(creator.dir, &filenames);
194 
195  if(filter) {
196  filenames.erase(
197  std::remove_if(filenames.begin(), filenames.end(), filename_filter(*filter)), filenames.end());
198  }
199 
200  std::vector<save_info> result;
201  std::transform(filenames.begin(), filenames.end(), std::back_inserter(result), creator);
202  std::sort(result.begin(), result.end(), save_info_less_time());
203 
204  return result;
205 }
206 
208 {
209  return save_index_manager.get(name());
210 }
211 
213 {
214  if(tm* tm_l = localtime(&modified())) {
216  ? _("%a %b %d %I:%M %p %Y")
217  : _("%a %b %d %H:%M %Y");
218 
219  return translation::strftime(format, tm_l);
220  }
221 
222  LOG_SAVE << "localtime() returned null for time " << this->modified() << ", save " << name();
223  return "";
224 }
225 
227 {
228  time_t t = modified();
229  return utils::format_time_summary(t);
230 }
231 
233 {
234  if(a.modified() > b.modified()) {
235  return true;
236  } else if(a.modified() < b.modified()) {
237  return false;
238  } else if(a.name().find(_(" replay")) == std::string::npos && b.name().find(_(" replay")) != std::string::npos) {
239  // Special funky case; for files created in the same second,
240  // a replay file sorts less than a non-replay file. Prevents
241  // a timing-dependent bug where it may look like, at the end
242  // of a scenario, the replay and the autosave for the next
243  // scenario are displayed in the wrong order.
244  return true;
245  } else if(a.name().find(_(" replay")) != std::string::npos && b.name().find(_(" replay")) == std::string::npos) {
246  return false;
247  } else {
248  return a.name() > b.name();
249  }
250 }
251 
253  const std::string& name, const std::vector<std::string>& suffixes)
254 {
255  for(const std::string& suf : suffixes) {
256  filesystem::scoped_istream file_stream =
258 
259  if(!file_stream->fail()) {
260  return file_stream;
261  }
262  }
263 
264  LOG_SAVE << "Could not open supplied filename '" << name << "'\n";
265  throw game::load_game_failed();
266 }
267 
268 void read_save_file(const std::string& name, config& cfg, std::string* error_log)
269 {
270  static const std::vector<std::string> suffixes{"", ".gz", ".bz2"};
271  filesystem::scoped_istream file_stream = find_save_file(name, suffixes);
272 
273  cfg.clear();
274  try {
275  /*
276  * Test the modified name, since it might use a .gz
277  * file even when not requested.
278  */
279  if(filesystem::is_gzip_file(name)) {
280  read_gz(cfg, *file_stream);
281  } else if(filesystem::is_bzip2_file(name)) {
282  read_bz2(cfg, *file_stream);
283  } else {
284  read(cfg, *file_stream);
285  }
286  } catch(const std::ios_base::failure& e) {
287  LOG_SAVE << e.what();
288 
289  if(error_log) {
290  *error_log += e.what();
291  }
292  throw game::load_game_failed();
293  } catch(const config::error& err) {
294  LOG_SAVE << err.message;
295 
296  if(error_log) {
297  *error_log += err.message;
298  }
299 
300  throw game::load_game_failed();
301  }
302 
303  if(cfg.empty()) {
304  LOG_SAVE << "Could not parse file data into config\n";
305  throw game::load_game_failed();
306  }
307 }
308 
309 void remove_old_auto_saves(const int autosavemax, const int infinite_auto_saves)
310 {
311  const std::string auto_save = _("Auto-Save");
312 
313  int countdown = autosavemax;
314  if(countdown == infinite_auto_saves) {
315  return;
316  }
317 
318  std::vector<save_info> games = get_saves_list(nullptr, &auto_save);
319  for(std::vector<save_info>::iterator i = games.begin(); i != games.end(); ++i) {
320  if(countdown-- <= 0) {
321  LOG_SAVE << "Deleting savegame '" << i->name() << "'\n";
322  delete_game(i->name());
323  }
324  }
325 }
326 
328 {
330 
331  save_index_manager.remove(name);
332 }
333 
335  : dir(d ? *d : filesystem::get_saves_dir())
336 {
337 }
338 
340 {
341  time_t modified = filesystem::file_modified_time(dir + "/" + filename);
342  save_index_manager.set_modified(filename, modified);
343  return save_info(filename, modified);
344 }
345 
346 void extract_summary_from_config(config& cfg_save, config& cfg_summary)
347 {
348  const config& cfg_snapshot = cfg_save.child("snapshot");
349 
350  // Servergenerated replays contain [scenario] and no [replay_start]
351  const config& cfg_replay_start = cfg_save.child("replay_start")
352  ? cfg_save.child("replay_start")
353  : cfg_save.child("scenario");
354 
355  const config& cfg_replay = cfg_save.child("replay");
356  const bool has_replay = cfg_replay && !cfg_replay.empty();
357  const bool has_snapshot = cfg_snapshot && cfg_snapshot.has_child("side");
358 
359  cfg_summary["replay"] = has_replay;
360  cfg_summary["snapshot"] = has_snapshot;
361 
362  cfg_summary["label"] = cfg_save["label"];
363  cfg_summary["campaign_type"] = cfg_save["campaign_type"];
364 
365  if(cfg_save.has_child("carryover_sides_start")) {
366  cfg_summary["scenario"] = cfg_save.child("carryover_sides_start")["next_scenario"];
367  } else {
368  cfg_summary["scenario"] = cfg_save["scenario"];
369  }
370 
371  cfg_summary["difficulty"] = cfg_save["difficulty"];
372  cfg_summary["random_mode"] = cfg_save["random_mode"];
373 
374  cfg_summary["campaign"] = cfg_save["campaign"];
375  cfg_summary["version"] = cfg_save["version"];
376  cfg_summary["corrupt"] = "";
377 
378  if(has_snapshot) {
379  cfg_summary["turn"] = cfg_snapshot["turn_at"];
380  if(cfg_snapshot["turns"] != "-1") {
381  cfg_summary["turn"] = cfg_summary["turn"].str() + "/" + cfg_snapshot["turns"].str();
382  }
383  }
384 
385  // Ensure we don't get duplicate [leader] tags
386  cfg_summary.clear_children("leader");
387 
388  // Find the human leaders so we can display their icons and names in the load menu.
389  config leader_config;
390 
391  bool shrouded = false;
392 
393  if(const config& snapshot = *(has_snapshot ? &cfg_snapshot : &cfg_replay_start)) {
394  for(const config& side : snapshot.child_range("side")) {
395  std::string leader;
396  std::string leader_image;
397  std::string leader_image_tc_modifier;
398  std::string leader_name;
399  int gold = side["gold"];
400  int units = 0, recall_units = 0;
401 
402  if(side["controller"] != team::CONTROLLER::enum_to_string(team::CONTROLLER::HUMAN)) {
403  continue;
404  }
405 
406  if(side["shroud"].to_bool()) {
407  shrouded = true;
408  }
409 
410  for(const config& u : side.child_range("unit")) {
411  if(u.has_attribute("x") && u.has_attribute("y")) {
412  units++;
413  } else {
414  recall_units++;
415  }
416 
417  // Only take the first leader
418  if(!leader.empty() || !u["canrecruit"].to_bool()) {
419  continue;
420  }
421 
422  const std::string tc_color = team::get_side_color_id_from_config(side);
423 
424  // Don't count it among the troops
425  units--;
426  leader = u["id"].str();
427  leader_name = u["name"].str();
428  leader_image = u["image"].str();
429  leader_image_tc_modifier = "~RC(" + u["flag_rgb"].str() + ">" + tc_color + ")";
430  }
431 
432  // We need a binary path-independent path to the leader image here so it can be displayed
433  // for campaign-specific units even when the campaign isn't loaded yet.
434  std::string leader_image_path = filesystem::get_independent_image_path(leader_image);
435 
436  // If the image path was found, we append the leader TC modifier. If it's not (such as in
437  // the case where the binary path hasn't been loaded yet, perhaps due to save_index being
438  // deleted), the unaltered image path is used and will be parsed by get_independent_image_path
439  // at runtime.
440  if(!leader_image_path.empty()) {
441  leader_image_path += leader_image_tc_modifier;
442 
443  leader_image = leader_image_path;
444  }
445 
446  leader_config["leader"] = leader;
447  leader_config["leader_name"] = leader_name;
448  leader_config["leader_image"] = leader_image;
449  leader_config["leader_image_tc_modifier"] = leader_image_tc_modifier;
450  leader_config["gold"] = gold;
451  leader_config["units"] = units;
452  leader_config["recall_units"] = recall_units;
453 
454  cfg_summary.add_child("leader", leader_config);
455  }
456  }
457 
458  if(!shrouded) {
459  if(has_snapshot) {
460  if(!cfg_snapshot.find_child("side", "shroud", "yes") && cfg_snapshot.has_attribute("map_data")) {
461  cfg_summary["map_data"] = cfg_snapshot["map_data"].str();
462  } else {
463  ERR_SAVE << "Not saving map because there is shroud" << std::endl;
464  }
465  } else if(has_replay) {
466  if(!cfg_replay_start.find_child("side", "shroud", "yes") && cfg_replay_start.has_attribute("map_data")) {
467  cfg_summary["map_data"] = cfg_replay_start["map_data"];
468  } else {
469  ERR_SAVE << "Not saving map because there is shroud" << std::endl;
470  }
471  }
472  }
473 }
474 
475 } // 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:309
int autosavemax()
Definition: game.cpp:810
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:419
const char * what() const NOEXCEPT
Definition: exceptions.hpp:37
std::vector< char_t > string
void rebuild(const std::string &name)
Definition: save_index.cpp:45
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:268
int dummy
Definition: lstrlib.cpp:1125
bool delete_file(const std::string &filename)
static std::string get_side_color_id_from_config(const config &cfg)
Definition: team.cpp:968
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:782
Variant for storing WML attributes.
void extract_summary_from_config(config &, config &)
Definition: save_index.cpp:346
bool operator()(const std::string &filename) const
Definition: save_index.cpp:178
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
#define a
void remove(const std::string &name)
Definition: save_index.cpp:71
bool empty() const
Tests for an attribute that either was never set or was set to "".
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:188
static lg::log_domain log_engine("engine")
void clear()
Definition: config.cpp:809
#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
bool empty() const
Definition: config.cpp:830
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
std::string format_time_summary() const
Definition: save_index.cpp:226
#define ERR_SAVE
Definition: save_index.cpp:36
std::string format_time_summary(time_t t)
#define b
std::string strftime(const std::string &format, const std::tm *time)
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
Definition: parser.cpp:749
static UNUSEDNOWARN std::string _(const char *str)
Definition: gettext.hpp:89
const config & summary() const
Definition: save_index.cpp:207
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error=true)
static filesystem::scoped_istream find_save_file(const std::string &name, const std::vector< std::string > &suffixes)
Definition: save_index.cpp:252
void write_gz(std::ostream &out, const configr_of &cfg)
Definition: parser.cpp:768
void clear_children(T...keys)
Definition: config.hpp:510
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:327
std::string format_time_local() const
Definition: save_index.cpp:212
std::unique_ptr< std::istream > scoped_istream
Definition: filesystem.hpp:37
save_index_class save_index_manager
Definition: save_index.cpp:168
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::map< std::string, time_t > modified_
Definition: save_index.hpp:106
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:412
bool has_attribute(config_key_type key) const
Definition: config.cpp:213
std::unique_ptr< std::ostream > scoped_ostream
Definition: filesystem.hpp:38
bool countdown()
Definition: game.cpp:616
bool is_gzip_file(const std::string &filename)
Returns true if the file ends with '.gz'.
time_t file_modified_time(const std::string &fname)
Get the modification time of a file.
filesystem::scoped_ostream ostream_file(const std::string &fname, bool create_directory=true)
config & get(const std::string &name)
Definition: save_index.cpp:83
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs=nullptr, file_name_option mode=FILE_NAME_ONLY, file_filter_option filter=NO_FILTER, file_reorder_option reorder=DONT_REORDER, file_tree_checksum *checksum=nullptr)
Populates 'files' with all the files and 'dirs' with all the directories in dir.
int to_int(int def=0) const
logger & err()
Definition: log.cpp:79
An exception object used when an IO error occurs.
Definition: filesystem.hpp:46
#define log_scope(description)
Definition: log.hpp:186
size_t i
Definition: function.cpp:933
static void fix_leader_image_path(config &data)
Definition: save_index.cpp:158
Declarations for File-IO.
bool operator()(const save_info &a, const save_info &b) const
Definition: save_index.cpp:232
static int sort(lua_State *L)
Definition: ltablib.cpp:411
#define LOG_SAVE
Definition: save_index.cpp:35
void set_modified(const std::string &name, const time_t &modified)
Definition: save_index.cpp:78
config & add_child(config_key_type key)
Definition: config.cpp:475
compression::format save_compression_format()
Definition: game.cpp:873
bool is_bzip2_file(const std::string &filename)
Returns true if the file ends with '.bz2'.
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:334
double t
Definition: astarsearch.cpp:64
const std::string & name() const
Definition: save_index.hpp:36
const time_t & modified() const
Definition: save_index.hpp:41
filename_filter(const std::string &filter)
Definition: save_index.cpp:173
Standard logging facilities (interface).
std::string message
Definition: exceptions.hpp:31
save_info operator()(const std::string &filename) const
Definition: save_index.cpp:339
static const char * name(const std::vector< SDL_Joystick * > &joysticks, const size_t index)
Definition: joystick.cpp:48
#define e
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:93
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
bool use_twelve_hour_clock_format()
Definition: general.cpp:1044
std::string::const_iterator iterator
Definition: tokenizer.hpp:24