The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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"
21#include "game_errors.hpp"
22#include "gettext.hpp"
23#include "log.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
33static 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
37static lg::log_domain log_enginerefac("enginerefac");
38#define LOG_RG LOG_STREAM(info, log_enginerefac)
39
40namespace savegame
41{
43
44void save_index_class::rebuild(const std::string& name)
45{
46 auto modified = filesystem::file_modified_time(dir_ + "/" + name);
47 rebuild(name, modified);
48}
49
50void 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
66void save_index_class::remove(const std::string& name)
67{
68 config& root = data();
69 root.remove_children("save", [&name](const config& d) { return d["save"] == name; });
71}
72
73void save_index_class::set_modified(const std::string& name, const std::chrono::system_clock::time_point& modified)
74{
75 modified_[name] = modified;
76}
77
78config& 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
91const 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"].str());
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_) {
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
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
173
174config& 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
202std::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. */
209std::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
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
284static 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 << "'";
298}
299
300config 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";
330 }
331
332 return cfg;
333}
334
335void 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
359void 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
371create_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
383void 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:157
config & add_child(std::string_view key)
Definition config.cpp:436
optional_config_impl< config > optional_child(std::string_view key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition config.cpp:380
child_itors child_range(std::string_view key)
Definition config.cpp:268
const attribute_value * get(std::string_view key) const
Returns a pointer to the attribute with the given key or nullptr if it does not exist.
Definition config.cpp:665
void clear_children(T... keys)
Definition config.hpp:601
optional_config_impl< config > find_child(std::string_view 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:764
std::size_t all_children_count() const
Definition config.cpp:302
bool has_child(std::string_view key) const
Determine whether a config has a child or not.
Definition config.cpp:312
const config & child_or_empty(std::string_view key) const
Returns the first child with the given key, or an empty config if there is none.
Definition config.cpp:390
bool empty() const
Definition config.cpp:823
config & mandatory_child(std::string_view key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition config.cpp:362
void clear()
Definition config.cpp:802
void remove_children(std::string_view key, const std::function< bool(const config &)> &p={})
Removes all children with tag key for which p returns true.
Definition config.cpp:634
static prefs & get()
bool use_twelve_hour_clock_format()
std::shared_ptr< save_index_class > manager_
save_info operator()(const std::string &filename) const
create_save_info(const std::shared_ptr< save_index_class > &)
void clean_up_index()
Deletes non-existent save files from the index.
create_for_default_saves_dir
Syntatic sugar for choosing which constructor to use.
static std::shared_ptr< save_index_class > default_saves_dir()
Returns an instance for managing saves in filesystem::get_saves_dir()
std::vector< save_info > get_saves_list(const std::string *filter=nullptr)
Get a list of available saves.
void remove(const std::string &name)
Delete a savegame from the index, without deleting the underlying file.
bool read_only_
The instance for default_saves_dir() writes a cache file.
void write_save_index()
Sync to disk, no-op if read_only_ is set.
const std::string & dir() const
config & get(const std::string &name)
void set_modified(const std::string &name, const std::chrono::system_clock::time_point &modified)
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).
void delete_game(const std::string &name)
Delete a savegame, including deleting the underlying file.
void rebuild(const std::string &name)
std::map< std::string, std::chrono::system_clock::time_point > modified_
save_index_class(const std::string &dir)
Constructor for a read-only instance.
bool clean_up_index_
Flag to only run the clean_up_index method once.
static void fix_leader_image_path(config &data)
Filename and modification date for a file list.
const config & summary() const
const auto & modified() const
std::string format_time_local() const
std::string format_time_summary() const
std::shared_ptr< save_index_class > save_index_
const std::string & name() const
static std::string get_side_color_id_from_config(const config &cfg)
Definition team.cpp:999
Definitions for the interface to Wesnoth Markup Language (WML).
const config * cfg
Declarations for File-IO.
static std::string _(const char *str)
Definition gettext.hpp:100
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:58
auto parse_timestamp(long long val)
Definition chrono.hpp:48
auto get_local_timestamp(const std::chrono::system_clock::time_point &time)
Definition chrono.hpp:63
bool is_bzip2_file(const std::string &filename)
Returns true if the file ends with '.bz2'.
bool file_exists(const std::string &name)
Returns true if a file or directory with such name already exists.
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs=nullptr, name_mode mode=name_mode::FILE_NAME_ONLY, filter_mode filter=filter_mode::NO_FILTER, reorder_mode reorder=reorder_mode::DONT_REORDER, file_tree_checksum *checksum=nullptr)
Get a list of all files and/or directories in a given directory.
bool delete_file(const std::string &filename)
bool is_gzip_file(const std::string &filename)
Returns true if the file ends with '.gz'.
std::string get_save_index_file()
filesystem::scoped_ostream ostream_file(const std::string &fname, std::ios_base::openmode mode=std::ios_base::binary, bool create_directory=true)
std::unique_ptr< std::istream > scoped_istream
std::unique_ptr< std::ostream > scoped_ostream
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.
std::chrono::system_clock::time_point file_modified_time(const std::string &fname)
Get the modification time of a file.
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error=true)
config read(std::istream &in, abstract_validator *validator)
Definition parser.cpp:610
void write(std::ostream &out, const configr_of &cfg, unsigned int level, bool strong_quotes)
Definition parser.cpp:748
config read_bz2(std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially bzip2_error.
Definition parser.cpp:672
config read_gz(std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially a gzip_error.
Definition parser.cpp:666
void write_gz(std::ostream &out, const configr_of &cfg)
Definition parser.cpp:767
config read_save_file(const std::string &dir, const std::string &name)
Read the complete config information out of a savefile.
static filesystem::scoped_istream find_save_file(const std::string &dir, const std::string &name, const std::vector< std::string > &suffixes)
void extract_summary_from_config(const config &, config &)
std::string translate_timestamp(time_t time, std::string_view format)
Translates the passed timestamp and returns it in the requested format.
Definition gettext.cpp:577
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition general.hpp:88
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:108
std::string_view data
Definition picture.cpp:188
#define LOG_SAVE
static lg::log_domain log_engine("engine")
static lg::log_domain log_enginerefac("enginerefac")
#define ERR_SAVE
std::string filename
Filename.
An exception object used when an IO error occurs.
Error used when game loading fails.
A structure for comparing to save_info objects based on their modified time.
bool operator()(const save_info &a, const save_info &b) const
#define i
#define d
#define e
#define b