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