The Battle for Wesnoth  1.19.0-dev
save_index.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2024
3  by Jörg Hinrichs, 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_errors.hpp"
22 #include "gettext.hpp"
23 #include "log.hpp"
24 #include "preferences/game.hpp"
25 #include "serialization/parser.hpp"
26 #include "team.hpp"
27 
28 #include <boost/algorithm/string/replace.hpp>
29 #include <boost/iostreams/filter/gzip.hpp>
30 
31 static lg::log_domain log_engine("engine");
32 #define LOG_SAVE LOG_STREAM(info, log_engine)
33 #define ERR_SAVE LOG_STREAM(err, log_engine)
34 
35 static lg::log_domain log_enginerefac("enginerefac");
36 #define LOG_RG LOG_STREAM(info, log_enginerefac)
37 
38 namespace savegame
39 {
41 
42 void save_index_class::rebuild(const std::string& name)
43 {
44  std::time_t modified = filesystem::file_modified_time(dir_ + "/" + name);
45  rebuild(name, modified);
46 }
47 
48 void save_index_class::rebuild(const std::string& name, const std::time_t& modified)
49 {
50  log_scope("load_summary_from_file");
51 
52  config& summary = data(name);
53 
54  try {
55  config full;
56  std::string dummy;
57  read_save_file(dir_, name, full, &dummy);
58 
59  extract_summary_from_config(full, summary);
60  } catch(const game::load_game_failed&) {
61  summary["corrupt"] = true;
62  }
63 
64  summary["mod_time"] = std::to_string(static_cast<int>(modified));
66 }
67 
68 void save_index_class::remove(const std::string& name)
69 {
70  config& root = data();
71  root.remove_children("save", [&name](const config& d) { return name == d["save"]; });
73 }
74 
75 void save_index_class::set_modified(const std::string& name, const std::time_t& modified)
76 {
77  modified_[name] = modified;
78 }
79 
80 config& save_index_class::get(const std::string& name)
81 {
82  config& result = data(name);
83  std::time_t m = modified_[name];
84 
85  config::attribute_value& mod_time = result["mod_time"];
86  if(mod_time.empty() || mod_time.to_time_t() != m) {
87  rebuild(name, m);
88  }
89 
90  return result;
91 }
92 
93 const std::string& save_index_class::dir() const
94 {
95  return dir_;
96 }
97 
99 {
100  config &root = data();
101 
102  std::vector<std::string> filenames;
103  filesystem::get_files_in_dir(dir(), &filenames);
104 
105  if(root.all_children_count() > filenames.size()) {
106  root.remove_children("save", [&filenames](const config& d)
107  {
108  return std::find(filenames.begin(), filenames.end(), d["save"]) == filenames.end();
109  }
110  );
111  }
112 }
113 
115 {
116  log_scope("write_save_index()");
117 
118  if(read_only_) {
119  LOG_SAVE << "no-op: read_only instance";
120  return;
121  }
122 
123  if(clean_up_index_) {
124  clean_up_index();
125  clean_up_index_ = false;
126  }
127 
128  try {
130 
132  // TODO: maybe allow writing this using bz2 too?
133  write_gz(*stream, data());
134  } else {
135  write(*stream, data());
136  }
137  } catch(const filesystem::io_exception& e) {
138  ERR_SAVE << "error writing to save index file: '" << e.what() << "'";
139  }
140 }
141 
142 save_index_class::save_index_class(const std::string& dir)
143  : loaded_(false)
144  , data_()
145  , modified_()
146  , dir_(dir)
147  , read_only_(true)
148  , clean_up_index_(true)
149 {
150 }
151 
154 {
155  read_only_ = false;
156 }
157 
158 config& save_index_class::data(const std::string& name)
159 {
160  config& cfg = data();
161  if(auto sv = cfg.find_child("save", "save", name)) {
163  return *sv;
164  }
165 
166  config& res = cfg.add_child("save");
167  res["save"] = name;
168  return res;
169 }
170 
172 {
173  const std::string si_file = filesystem::get_save_index_file();
174 
175  // Don't try to load the file if it doesn't exist.
176  if(loaded_ == false && filesystem::file_exists(si_file)) {
177  try {
179  try {
180  read_gz(data_, *stream);
181  } catch(const boost::iostreams::gzip_error&) {
182  stream->seekg(0);
183  read(data_, *stream);
184  }
185  } catch(const filesystem::io_exception& e) {
186  ERR_SAVE << "error reading save index: '" << e.what() << "'";
187  } catch(const config::error& e) {
188  ERR_SAVE << "error parsing save index config file:\n" << e.message;
189  data_.clear();
190  }
191 
192  loaded_ = true;
193  }
194 
195  return data_;
196 }
197 
199 {
200  for(config& leader : data.child_range("leader")) {
201  std::string leader_image = leader["leader_image"];
202  boost::algorithm::replace_all(leader_image, "\\", "/");
203 
204  leader["leader_image"] = leader_image;
205  }
206 }
207 
208 std::shared_ptr<save_index_class> save_index_class::default_saves_dir()
209 {
210  static auto instance = std::make_shared<save_index_class>(create_for_default_saves_dir::yes);
211  return instance;
212 }
213 
214 /** Get a list of available saves. */
215 std::vector<save_info> save_index_class::get_saves_list(const std::string* filter)
216 {
217  create_save_info creator(shared_from_this());
218 
219  std::vector<std::string> filenames;
220  filesystem::get_files_in_dir(dir(), &filenames);
221 
222  const auto should_remove = [filter](const std::string& filename) {
223  // Steam documentation indicates games can ignore their auto-generated 'steam_autocloud.vdf'.
224  // Reference: https://partner.steamgames.com/doc/features/cloud (under Steam Auto-Cloud section as of September 2021)
225  static const std::vector<std::string> to_ignore {"steam_autocloud.vdf"};
226 
227  if(std::find(to_ignore.begin(), to_ignore.end(), filename) != to_ignore.end()) {
228  return true;
229  } else if(filter) {
230  return filename.end() == std::search(filename.begin(), filename.end(), filter->begin(), filter->end());
231  }
232 
233  return false;
234  };
235 
236  filenames.erase(std::remove_if(filenames.begin(), filenames.end(), should_remove), filenames.end());
237 
238  std::vector<save_info> result;
239  std::transform(filenames.begin(), filenames.end(), std::back_inserter(result), creator);
240  std::sort(result.begin(), result.end(), save_info_less_time());
241 
242  return result;
243 }
244 
246 {
247  return save_index_->get(name());
248 }
249 
250 std::string save_info::format_time_local() const
251 {
252  if(std::tm* tm_l = std::localtime(&modified())) {
254  // TRANSLATORS: Day of week + month + day of month + year + 12-hour time, eg 'Tue Nov 02 2021, 1:59 PM'. Format for your locale.
255  ? _("%a %b %d %Y, %I:%M %p")
256  // TRANSLATORS: Day of week + month + day of month + year + 24-hour time, eg 'Tue Nov 02 2021, 13:59'. Format for your locale.
257  : _("%a %b %d %Y, %H:%M");
258 
259  return translation::strftime(format, tm_l);
260  }
261 
262  LOG_SAVE << "localtime() returned null for time " << this->modified() << ", save " << name();
263  return "";
264 }
265 
267 {
268  std::time_t t = modified();
270 }
271 
273 {
274  // This translatable string must be same one as in replay_savegame::create_initial_filename.
275  // TODO: we really shouldn't be relying on translatable strings like this, especially since
276  // old savefiles may have been created in a different language than the current UI language
277  const std::string replay_str = " " + _("replay");
278  if(a.modified() > b.modified()) {
279  return true;
280  } else if(a.modified() < b.modified()) {
281  return false;
282  } else if(a.name().find(replay_str) == std::string::npos && b.name().find(replay_str) != std::string::npos) {
283  // Special funky case; for files created in the same second,
284  // a replay file sorts less than a non-replay file. Prevents
285  // a timing-dependent bug where it may look like, at the end
286  // of a scenario, the replay and the autosave for the next
287  // scenario are displayed in the wrong order.
288  return true;
289  } else if(a.name().find(replay_str) != std::string::npos && b.name().find(replay_str) == std::string::npos) {
290  return false;
291  } else {
292  return a.name() > b.name();
293  }
294 }
295 
296 static filesystem::scoped_istream find_save_file(const std::string& dir,
297  const std::string& name, const std::vector<std::string>& suffixes)
298 {
299  for(const std::string& suf : suffixes) {
300  filesystem::scoped_istream file_stream =
301  filesystem::istream_file(dir + "/" + name + suf);
302 
303  if(!file_stream->fail()) {
304  return file_stream;
305  }
306  }
307 
308  LOG_SAVE << "Could not open supplied filename '" << name << "'";
309  throw game::load_game_failed();
310 }
311 
312 void read_save_file(const std::string& dir, const std::string& name, config& cfg, std::string* error_log)
313 {
314  static const std::vector<std::string> suffixes{"", ".gz", ".bz2"};
315  filesystem::scoped_istream file_stream = find_save_file(dir, name, suffixes);
316 
317  cfg.clear();
318  try {
319  /*
320  * Test the modified name, since it might use a .gz
321  * file even when not requested.
322  */
323  if(filesystem::is_gzip_file(name)) {
324  read_gz(cfg, *file_stream);
325  } else if(filesystem::is_bzip2_file(name)) {
326  read_bz2(cfg, *file_stream);
327  } else {
328  read(cfg, *file_stream);
329  }
330  } catch(const std::ios_base::failure& e) {
331  LOG_SAVE << e.what();
332 
333  if(error_log) {
334  *error_log += e.what();
335  }
336  throw game::load_game_failed();
337  } catch(const config::error& err) {
338  LOG_SAVE << err.message;
339 
340  if(error_log) {
341  *error_log += err.message;
342  }
343 
344  throw game::load_game_failed();
345  }
346 
347  if(cfg.empty()) {
348  LOG_SAVE << "Could not parse file data into config";
349  throw game::load_game_failed();
350  }
351 }
352 
353 void save_index_class::delete_old_auto_saves(const int autosavemax, const int infinite_auto_saves)
354 {
355  log_scope("delete_old_auto_saves()");
356  if(read_only_) {
357  LOG_SAVE << "no-op: read_only instance";
358  return;
359  }
360 
361  const std::string auto_save = _("Auto-Save");
362 
363  int countdown = autosavemax;
364  if(countdown == infinite_auto_saves) {
365  return;
366  }
367 
368  std::vector<save_info> games = get_saves_list(&auto_save);
369  for(std::vector<save_info>::iterator i = games.begin(); i != games.end(); ++i) {
370  if(countdown-- <= 0) {
371  LOG_SAVE << "Deleting savegame '" << i->name() << "'";
372  delete_game(i->name());
373  }
374  }
375 }
376 
377 void save_index_class::delete_game(const std::string& name)
378 {
379  if(read_only_) {
380  log_scope("delete_game()");
381  LOG_SAVE << "no-op: read_only instance";
382  return;
383  }
384 
385  filesystem::delete_file(dir() + "/" + name);
386  remove(name);
387 }
388 
389 create_save_info::create_save_info(const std::shared_ptr<save_index_class>& manager)
390  : manager_(manager)
391 {
392 }
393 
394 save_info create_save_info::operator()(const std::string& filename) const
395 {
396  std::time_t modified = filesystem::file_modified_time(manager_->dir() + "/" + filename);
397  manager_->set_modified(filename, modified);
398  return save_info(filename, manager_, modified);
399 }
400 
401 void extract_summary_from_config(config& cfg_save, config& cfg_summary)
402 {
403  auto cfg_snapshot = cfg_save.optional_child("snapshot");
404 
405  // Servergenerated replays contain [scenario] and no [replay_start]
406  auto cfg_replay_start = cfg_save.has_child("replay_start")
407  ? cfg_save.optional_child("replay_start")
408  : cfg_save.optional_child("scenario");
409 
410  auto cfg_replay = cfg_save.optional_child("replay");
411  const bool has_replay = cfg_replay && !cfg_replay->empty();
412  const bool has_snapshot = cfg_snapshot && cfg_snapshot->has_child("side");
413 
414  cfg_summary["replay"] = has_replay;
415  cfg_summary["snapshot"] = has_snapshot;
416 
417  cfg_summary["label"] = cfg_save["label"];
418  cfg_summary["campaign_type"] = cfg_save["campaign_type"];
419 
420  if(cfg_save.has_child("carryover_sides_start")) {
421  cfg_summary["scenario"] = cfg_save.mandatory_child("carryover_sides_start")["next_scenario"];
422  } else {
423  cfg_summary["scenario"] = cfg_save["scenario"];
424  }
425 
426  cfg_summary["difficulty"] = cfg_save["difficulty"];
427  cfg_summary["random_mode"] = cfg_save["random_mode"];
428 
429  cfg_summary["active_mods"] = cfg_save.child_or_empty("multiplayer")["active_mods"];
430  cfg_summary["campaign"] = cfg_save["campaign"];
431  cfg_summary["version"] = cfg_save["version"];
432  cfg_summary["corrupt"] = "";
433 
434  if(has_snapshot) {
435  cfg_summary["turn"] = cfg_snapshot["turn_at"];
436  if(cfg_snapshot["turns"] != "-1") {
437  cfg_summary["turn"] = cfg_summary["turn"].str() + "/" + cfg_snapshot["turns"].str();
438  }
439  }
440 
441  // Ensure we don't get duplicate [leader] tags
442  cfg_summary.clear_children("leader");
443 
444  // Find the human leaders so we can display their icons and names in the load menu.
445  config leader_config;
446 
447  bool shrouded = false;
448 
449  if(auto snapshot = (has_snapshot ? cfg_snapshot : cfg_replay_start)) {
450  for(const config& side : snapshot->child_range("side")) {
451  std::string leader;
452  std::string leader_image;
453  std::string leader_image_tc_modifier;
454  std::string leader_name;
455  int gold = side["gold"];
456  int units = 0, recall_units = 0;
457 
458  if(side["controller"] != side_controller::human) {
459  continue;
460  }
461 
462  if(side["shroud"].to_bool()) {
463  shrouded = true;
464  }
465 
466  for(const config& u : side.child_range("unit")) {
467  if(u.has_attribute("x") && u.has_attribute("y")) {
468  units++;
469  } else {
470  recall_units++;
471  }
472 
473  // Only take the first leader
474  if(!leader.empty() || !u["canrecruit"].to_bool()) {
475  continue;
476  }
477 
478  const std::string tc_color = team::get_side_color_id_from_config(side);
479 
480  // Don't count it among the troops
481  units--;
482  leader = u["id"].str();
483  leader_name = u["name"].str();
484  leader_image = u["image"].str();
485  leader_image_tc_modifier = "~RC(" + u["flag_rgb"].str() + ">" + tc_color + ")";
486  }
487 
488  // We need a binary path-independent path to the leader image here so it can be displayed
489  // for campaign-specific units even when the campaign isn't loaded yet.
490  std::string leader_image_path = filesystem::get_independent_binary_file_path("images", leader_image);
491 
492  // If the image path was found, we append the leader TC modifier. If it's not (such as in
493  // the case where the binary path hasn't been loaded yet, perhaps due to save_index being
494  // deleted), the unaltered image path is used and will be parsed by get_independent_binary_file_path
495  // at runtime.
496  if(!leader_image_path.empty()) {
497  leader_image_path += leader_image_tc_modifier;
498 
499  leader_image = leader_image_path;
500  }
501 
502  leader_config["leader"] = leader;
503  leader_config["leader_name"] = leader_name;
504  leader_config["leader_image"] = leader_image;
505  leader_config["leader_image_tc_modifier"] = leader_image_tc_modifier;
506  leader_config["gold"] = gold;
507  leader_config["units"] = units;
508  leader_config["recall_units"] = recall_units;
509 
510  cfg_summary.add_child("leader", leader_config);
511  }
512  }
513 
514  if(!shrouded) {
515  if(has_snapshot) {
516  if(!cfg_snapshot->find_child("side", "shroud", "yes") && cfg_snapshot->has_attribute("map_data")) {
517  cfg_summary["map_data"] = cfg_snapshot["map_data"].str();
518  } else {
519  ERR_SAVE << "Not saving map because there is shroud";
520  }
521  } else if(has_replay) {
522  if(!cfg_replay_start->find_child("side", "shroud", "yes") && cfg_replay_start->has_attribute("map_data")) {
523  cfg_summary["map_data"] = cfg_replay_start["map_data"];
524  } else {
525  ERR_SAVE << "Not saving map because there is shroud";
526  }
527  }
528  }
529 }
530 
531 } // end namespace savegame
double t
Definition: astarsearch.cpp:63
Variant for storing WML attributes.
std::time_t to_time_t(std::time_t def=0) const
bool empty() const
Tests for an attribute that either was never set or was set to "".
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:159
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:395
config & mandatory_child(config_key_type key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition: config.cpp:367
optional_config_impl< 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:787
void clear_children(T... keys)
Definition: config.hpp:642
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:317
child_itors child_range(config_key_type key)
Definition: config.cpp:273
std::size_t all_children_count() const
Definition: config.cpp:307
void remove_children(config_key_type key, std::function< bool(const config &)> p=[](config){return true;})
Removes all children with tag key for which p returns true.
Definition: config.cpp:656
bool empty() const
Definition: config.cpp:852
void clear()
Definition: config.cpp:831
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Euivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:385
config & add_child(config_key_type key)
Definition: config.cpp:441
std::shared_ptr< save_index_class > manager_
Definition: save_index.hpp:75
save_info operator()(const std::string &filename) const
Definition: save_index.cpp:394
create_save_info(const std::shared_ptr< save_index_class > &)
Definition: save_index.cpp:389
void clean_up_index()
Deletes non-existent save files from the index.
Definition: save_index.cpp:98
create_for_default_saves_dir
Syntatic sugar for choosing which constructor to use.
Definition: save_index.hpp:86
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:208
std::vector< save_info > get_saves_list(const std::string *filter=nullptr)
Get a list of available saves.
Definition: save_index.cpp:215
void remove(const std::string &name)
Delete a savegame from the index, without deleting the underlying file.
Definition: save_index.cpp:68
bool read_only_
The instance for default_saves_dir() writes a cache file.
Definition: save_index.hpp:137
void write_save_index()
Sync to disk, no-op if read_only_ is set.
Definition: save_index.cpp:114
const std::string & dir() const
Definition: save_index.cpp:93
void set_modified(const std::string &name, const std::time_t &modified)
Definition: save_index.cpp:75
config & get(const std::string &name)
Definition: save_index.cpp:80
const std::string dir_
Definition: save_index.hpp:132
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:353
std::map< std::string, std::time_t > modified_
Definition: save_index.hpp:131
void delete_game(const std::string &name)
Delete a savegame, including deleting the underlying file.
Definition: save_index.cpp:377
void rebuild(const std::string &name)
Definition: save_index.cpp:42
save_index_class(const std::string &dir)
Constructor for a read-only instance.
Definition: save_index.cpp:142
bool clean_up_index_
Flag to only run the clean_up_index method once.
Definition: save_index.hpp:139
static void fix_leader_image_path(config &data)
Definition: save_index.cpp:198
Filename and modification date for a file list.
Definition: save_index.hpp:26
const std::string & name() const
Definition: save_index.hpp:38
const config & summary() const
Definition: save_index.cpp:245
std::string format_time_local() const
Definition: save_index.cpp:250
std::string format_time_summary() const
Definition: save_index.cpp:266
std::shared_ptr< save_index_class > save_index_
Definition: save_index.hpp:54
const std::time_t & modified() const
Definition: save_index.hpp:43
static std::string get_side_color_id_from_config(const config &cfg)
Definition: team.cpp:1010
Declarations for File-IO.
std::size_t i
Definition: function.cpp:968
static std::string _(const char *str)
Definition: gettext.hpp:93
Standard logging facilities (interface).
#define log_scope(description)
Definition: log.hpp:274
std::time_t file_modified_time(const std::string &fname)
Get the modification time of a file.
bool is_bzip2_file(const std::string &filename)
Returns true if the file ends with '.bz2'.
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error)
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)
Get a list of all files and/or directories in a given directory.
Definition: filesystem.cpp:404
bool delete_file(const std::string &filename)
bool is_gzip_file(const std::string &filename)
Returns true if the file ends with '.gz'.
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:318
std::string get_saves_dir()
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::string get_save_index_file()
filesystem::scoped_ostream ostream_file(const std::string &fname, std::ios_base::openmode mode, bool create_directory)
std::unique_ptr< std::istream > scoped_istream
Definition: filesystem.hpp:50
std::unique_ptr< std::ostream > scoped_ostream
Definition: filesystem.hpp:51
logger & err()
Definition: log.cpp:302
bool use_twelve_hour_clock_format()
Definition: general.cpp:966
int autosavemax()
Definition: game.cpp:788
compression::format save_compression_format()
Definition: game.cpp:840
bool countdown()
Definition: game.cpp:598
void extract_summary_from_config(config &, config &)
Definition: save_index.cpp:401
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:296
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:312
std::string strftime(const std::string &format, const std::tm *time)
Definition: gettext.cpp:555
std::string format_time_summary(std::time_t t)
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
std::string_view data
Definition: picture.cpp:194
#define LOG_SAVE
Definition: save_index.cpp:32
static lg::log_domain log_engine("engine")
static lg::log_domain log_enginerefac("enginerefac")
#define ERR_SAVE
Definition: save_index.cpp:33
void write_gz(std::ostream &out, const configr_of &cfg)
Definition: parser.cpp:783
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:627
void read_bz2(config &cfg, std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially bzip2_error.
Definition: parser.cpp:689
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
Definition: parser.cpp:764
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:683
An exception object used when an IO error occurs.
Definition: filesystem.hpp:64
Error used when game loading fails.
Definition: game_errors.hpp:31
A structure for comparing to save_info objects based on their modified time.
Definition: save_index.hpp:63
bool operator()(const save_info &a, const save_info &b) const
Definition: save_index.cpp:272
#define d
#define e
#define a
#define b