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  : data_()
142  , modified_()
143  , dir_(dir)
144  , read_only_(true)
145  , clean_up_index_(true)
146 {
147  const std::string si_file = filesystem::get_save_index_file();
148 
149  // Don't try to load the file if it doesn't exist.
150  if(filesystem::file_exists(si_file)) {
151  try {
153  try {
154  data_ = io::read_gz(*stream);
155  } catch(const boost::iostreams::gzip_error&) {
156  stream->seekg(0);
157  data_ = io::read(*stream);
158  }
159  } catch(const filesystem::io_exception& e) {
160  ERR_SAVE << "error reading save index: '" << e.what() << "'";
161  } catch(const config::error& e) {
162  ERR_SAVE << "error parsing save index config file:\n" << e.message;
163  data_.clear();
164  }
165  }
166 }
167 
170 {
171  read_only_ = false;
172 }
173 
174 config& save_index_class::data(const std::string& name)
175 {
176  config& cfg = data();
177  if(auto sv = cfg.find_child("save", "save", name)) {
179  return *sv;
180  }
181 
182  config& res = cfg.add_child("save");
183  res["save"] = name;
184  return res;
185 }
186 
188 {
189  return data_;
190 }
191 
193 {
194  for(config& leader : data.child_range("leader")) {
195  std::string leader_image = leader["leader_image"];
196  boost::algorithm::replace_all(leader_image, "\\", "/");
197 
198  leader["leader_image"] = leader_image;
199  }
200 }
201 
202 std::shared_ptr<save_index_class> save_index_class::default_saves_dir()
203 {
204  static auto instance = std::make_shared<save_index_class>(create_for_default_saves_dir::yes);
205  return instance;
206 }
207 
208 /** Get a list of available saves. */
209 std::vector<save_info> save_index_class::get_saves_list(const std::string* filter)
210 {
211  create_save_info creator(shared_from_this());
212 
213  std::vector<std::string> filenames;
214  filesystem::get_files_in_dir(dir(), &filenames);
215 
216  utils::erase_if(filenames, [filter](const std::string& filename) {
217  // Steam documentation indicates games can ignore their auto-generated 'steam_autocloud.vdf'.
218  // Reference: https://partner.steamgames.com/doc/features/cloud (under Steam Auto-Cloud section as of September 2021)
219  static const std::vector<std::string> to_ignore {"steam_autocloud.vdf"};
220 
221  if(utils::contains(to_ignore, filename)) {
222  return true;
223  } else if(filter) {
224  return filename.end() == std::search(filename.begin(), filename.end(), filter->begin(), filter->end());
225  }
226 
227  return false;
228  });
229 
230  std::vector<save_info> result;
231  std::transform(filenames.begin(), filenames.end(), std::back_inserter(result), creator);
232  std::sort(result.begin(), result.end(), save_info_less_time());
233 
234  return result;
235 }
236 
238 {
239  return save_index_->get(name());
240 }
241 
242 std::string save_info::format_time_local() const
243 {
244  const std::string format = prefs::get().use_twelve_hour_clock_format()
245  // TRANSLATORS: Day of week + month + day of month + year + 12-hour time, eg 'Tue Nov 02 2021, 1:59 PM'.
246  // Format for your locale.
247  ? _("%a %b %d %Y, %I:%M %p")
248  // TRANSLATORS: Day of week + month + day of month + year + 24-hour time, eg 'Tue Nov 02 2021, 13:59'.
249  // Format for your locale.
250  : _("%a %b %d %Y, %H:%M");
251 
253 }
254 
256 {
258 }
259 
261 {
262  // This translatable string must be same one as in replay_savegame::create_initial_filename.
263  // TODO: we really shouldn't be relying on translatable strings like this, especially since
264  // old savefiles may have been created in a different language than the current UI language
265  const std::string replay_str = " " + _("replay");
266  if(a.modified() > b.modified()) {
267  return true;
268  } else if(a.modified() < b.modified()) {
269  return false;
270  } else if(a.name().find(replay_str) == std::string::npos && b.name().find(replay_str) != std::string::npos) {
271  // Special funky case; for files created in the same second,
272  // a replay file sorts less than a non-replay file. Prevents
273  // a timing-dependent bug where it may look like, at the end
274  // of a scenario, the replay and the autosave for the next
275  // scenario are displayed in the wrong order.
276  return true;
277  } else if(a.name().find(replay_str) != std::string::npos && b.name().find(replay_str) == std::string::npos) {
278  return false;
279  } else {
280  return a.name() > b.name();
281  }
282 }
283 
284 static filesystem::scoped_istream find_save_file(const std::string& dir,
285  const std::string& name, const std::vector<std::string>& suffixes)
286 {
287  for(const std::string& suf : suffixes) {
288  filesystem::scoped_istream file_stream =
289  filesystem::istream_file(dir + "/" + name + suf);
290 
291  if(!file_stream->fail()) {
292  return file_stream;
293  }
294  }
295 
296  LOG_SAVE << "Could not open supplied filename '" << name << "'";
297  throw game::load_game_failed();
298 }
299 
300 config read_save_file(const std::string& dir, const std::string& name)
301 {
302  static const std::vector<std::string> suffixes{"", ".gz", ".bz2"};
303  filesystem::scoped_istream file_stream = find_save_file(dir, name, suffixes);
304  config cfg;
305 
306  try {
307  /*
308  * Test the modified name, since it might use a .gz
309  * file even when not requested.
310  */
311  if(filesystem::is_gzip_file(name)) {
312  cfg = io::read_gz(*file_stream);
313  } else if(filesystem::is_bzip2_file(name)) {
314  cfg = io::read_bz2(*file_stream);
315  } else {
316  cfg = io::read(*file_stream);
317  }
318  } catch(const std::ios_base::failure& e) {
319  LOG_SAVE << e.what();
320  throw game::load_game_failed(e.what());
321 
322  } catch(const config::error& err) {
323  LOG_SAVE << err.message;
324  throw game::load_game_failed(err.message);
325  }
326 
327  if(cfg.empty()) {
328  LOG_SAVE << "Could not parse file data into config";
329  throw game::load_game_failed();
330  }
331 
332  return cfg;
333 }
334 
335 void save_index_class::delete_old_auto_saves(const int autosavemax, const int infinite_auto_saves)
336 {
337  log_scope("delete_old_auto_saves()");
338  if(read_only_) {
339  LOG_SAVE << "no-op: read_only instance";
340  return;
341  }
342 
343  const std::string auto_save = _("Auto-Save");
344 
345  int countdown = autosavemax;
346  if(countdown == infinite_auto_saves) {
347  return;
348  }
349 
350  std::vector<save_info> games = get_saves_list(&auto_save);
351  for(std::vector<save_info>::iterator i = games.begin(); i != games.end(); ++i) {
352  if(countdown-- <= 0) {
353  LOG_SAVE << "Deleting savegame '" << i->name() << "'";
354  delete_game(i->name());
355  }
356  }
357 }
358 
359 void save_index_class::delete_game(const std::string& name)
360 {
361  if(read_only_) {
362  log_scope("delete_game()");
363  LOG_SAVE << "no-op: read_only instance";
364  return;
365  }
366 
367  filesystem::delete_file(dir() + "/" + name);
368  remove(name);
369 }
370 
371 create_save_info::create_save_info(const std::shared_ptr<save_index_class>& manager)
372  : manager_(manager)
373 {
374 }
375 
377 {
378  auto modified = filesystem::file_modified_time(manager_->dir() + "/" + filename);
379  manager_->set_modified(filename, modified);
380  return save_info(filename, manager_, modified);
381 }
382 
383 void extract_summary_from_config(const config& cfg_save, config& cfg_summary)
384 {
385  auto cfg_snapshot = cfg_save.optional_child("snapshot");
386 
387  // Servergenerated replays contain [scenario] and no [replay_start]
388  auto cfg_replay_start = cfg_save.has_child("replay_start")
389  ? cfg_save.optional_child("replay_start")
390  : cfg_save.optional_child("scenario");
391 
392  auto cfg_replay = cfg_save.optional_child("replay");
393  const bool has_replay = cfg_replay && !cfg_replay->empty();
394  const bool has_snapshot = cfg_snapshot && cfg_snapshot->has_child("side");
395 
396  cfg_summary["replay"] = has_replay;
397  cfg_summary["snapshot"] = has_snapshot;
398 
399  cfg_summary["label"] = cfg_save["label"];
400  cfg_summary["campaign_type"] = cfg_save["campaign_type"];
401 
402  if(cfg_save.has_child("carryover_sides_start")) {
403  cfg_summary["scenario"] = cfg_save.mandatory_child("carryover_sides_start")["next_scenario"];
404  } else {
405  cfg_summary["scenario"] = cfg_save["scenario"];
406  }
407 
408  cfg_summary["difficulty"] = cfg_save["difficulty"];
409  cfg_summary["random_mode"] = cfg_save["random_mode"];
410 
411  cfg_summary["active_mods"] = cfg_save.child_or_empty("multiplayer")["active_mods"];
412  cfg_summary["campaign"] = cfg_save["campaign"];
413  cfg_summary["version"] = cfg_save["version"];
414  cfg_summary["corrupt"] = "";
415 
416  if(has_snapshot) {
417  cfg_summary["turn"] = cfg_snapshot["turn_at"];
418  if(cfg_snapshot["turns"] != "-1") {
419  cfg_summary["turn"] = cfg_summary["turn"].str() + "/" + cfg_snapshot["turns"].str();
420  }
421  }
422 
423  // Ensure we don't get duplicate [leader] tags
424  cfg_summary.clear_children("leader");
425 
426  // Find the human leaders so we can display their icons and names in the load menu.
427  config leader_config;
428 
429  bool shrouded = false;
430 
431  if(auto snapshot = (has_snapshot ? cfg_snapshot : cfg_replay_start)) {
432  for(const config& side : snapshot->child_range("side")) {
433  std::string leader;
434  std::string leader_image;
435  std::string leader_image_tc_modifier;
436  std::string leader_name;
437  int gold = side["gold"].to_int();
438  int units = 0, recall_units = 0;
439 
440  if(side["controller"] != side_controller::human) {
441  continue;
442  }
443 
444  if(side["shroud"].to_bool()) {
445  shrouded = true;
446  }
447 
448  for(const config& u : side.child_range("unit")) {
449  if(u.has_attribute("x") && u.has_attribute("y")) {
450  units++;
451  } else {
452  recall_units++;
453  }
454 
455  // Only take the first leader
456  if(!leader.empty() || !u["canrecruit"].to_bool()) {
457  continue;
458  }
459 
460  const std::string tc_color = team::get_side_color_id_from_config(side);
461 
462  // Don't count it among the troops
463  units--;
464  leader = u["id"].str();
465  leader_name = u["name"].str();
466  leader_image = u["image"].str();
467  leader_image_tc_modifier = "~RC(" + u["flag_rgb"].str() + ">" + tc_color + ")";
468  }
469 
470  // We need a binary path-independent path to the leader image here so it can be displayed
471  // for campaign-specific units even when the campaign isn't loaded yet.
472  auto leader_image_path = filesystem::get_independent_binary_file_path("images", leader_image);
473 
474  // If the image path was found, we append the leader TC modifier. If it's not (such as in
475  // the case where the binary path hasn't been loaded yet, perhaps due to save_index being
476  // deleted), the unaltered image path is used and will be parsed by get_independent_binary_file_path
477  // at runtime.
478  if(leader_image_path) {
479  leader_image = leader_image_path.value() + leader_image_tc_modifier;
480  }
481 
482  leader_config["leader"] = leader;
483  leader_config["leader_name"] = leader_name;
484  leader_config["leader_image"] = leader_image;
485  leader_config["leader_image_tc_modifier"] = leader_image_tc_modifier;
486  leader_config["gold"] = gold;
487  leader_config["units"] = units;
488  leader_config["recall_units"] = recall_units;
489 
490  cfg_summary.add_child("leader", leader_config);
491  }
492  }
493 
494  if(!shrouded) {
495  if(has_snapshot) {
496  if(!cfg_snapshot->find_child("side", "shroud", "yes") && cfg_snapshot->has_attribute("map_data")) {
497  cfg_summary["map_data"] = cfg_snapshot["map_data"].str();
498  } else {
499  ERR_SAVE << "Not saving map because there is shroud";
500  }
501  } else if(has_replay) {
502  if(!cfg_replay_start->find_child("side", "shroud", "yes") && cfg_replay_start->has_attribute("map_data")) {
503  cfg_summary["map_data"] = cfg_replay_start["map_data"];
504  } else {
505  ERR_SAVE << "Not saving map because there is shroud";
506  }
507  }
508  }
509 }
510 
511 } // 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:376
create_save_info(const std::shared_ptr< save_index_class > &)
Definition: save_index.cpp:371
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:202
std::vector< save_info > get_saves_list(const std::string *filter=nullptr)
Get a list of available saves.
Definition: save_index.cpp:209
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:140
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:135
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:335
void delete_game(const std::string &name)
Delete a savegame, including deleting the underlying file.
Definition: save_index.cpp:359
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:134
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:142
static void fix_leader_image_path(config &data)
Definition: save_index.cpp:192
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:237
std::string format_time_local() const
Definition: save_index.cpp:242
std::string format_time_summary() const
Definition: save_index.cpp:255
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:300
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:284
void extract_summary_from_config(const config &, config &)
Definition: save_index.cpp:383
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:260
#define d
#define e
#define b