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