1 #include "OptionsDB.h"
2 #include "SaveGamePreviewUtils.h"
3 
4 #include "i18n.h"
5 #include "Directories.h"
6 #include "Logger.h"
7 #include "EnumText.h"
8 #include "Serialize.h"
9 #include "Serialize.ipp"
10 #include "ScopedTimer.h"
11 #include "Version.h"
12 
13 #include <boost/filesystem.hpp>
14 #include <boost/filesystem/operations.hpp>
15 #include <boost/filesystem/fstream.hpp>
16 #include <boost/date_time/posix_time/posix_time.hpp>
17 #include <boost/graph/graph_concepts.hpp>
18 #include <boost/iostreams/filter/zlib.hpp>
19 #include <boost/iostreams/filtering_streambuf.hpp>
20 #include <boost/iostreams/copy.hpp>
21 #include <boost/iostreams/device/back_inserter.hpp>
22 #include <boost/iostreams/stream.hpp>
23 
24 
25 #include <fstream>
26 
27 namespace fs = boost::filesystem;
28 
29 namespace {
30     const std::string UNABLE_TO_OPEN_FILE("Unable to open file");
31     const std::string XML_SAVE_FILE_DESCRIPTION("This is an XML archive FreeOrion saved game. Initial header information is uncompressed. The main gamestate information follows, possibly stored as zlib-comprssed XML archive in the last entry in the main archive.");
32     const std::string BIN_SAVE_FILE_DESCRIPTION("This is binary archive FreeOrion saved game.");
33 
34     const std::string XML_COMPRESSED_MARKER("zlib-xml");
35 
36     /// Splits time and date on separate lines for an ISO datetime string
split_time(const std::string & time)37     std::string split_time(const std::string& time) {
38         std::string result = time;
39         std::string::size_type pos = result.find('T');
40         if (pos != std::string::npos) {
41             result.replace(pos, 1, "\n");
42         }
43         return result;
44     }
45 
46     /// Populates a SaveGamePreviewData from a given file
47     /// returns true on success, false if preview data could not be found
LoadSaveGamePreviewData(const fs::path & path,FullPreview & full)48     bool LoadSaveGamePreviewData(const fs::path& path, FullPreview& full) {
49         if (!fs::exists(path)) {
50             DebugLogger() << "LoadSaveGamePreviewData: Save file note found: " << path.string();
51             return false;
52         }
53 
54         fs::ifstream ifs(path, std::ios_base::binary);
55 
56         full.filename = PathToString(path.filename());
57 
58         if (!ifs)
59             throw std::runtime_error(UNABLE_TO_OPEN_FILE);
60 
61         // alias structs so variable passed into NVP deserialization macro has the
62         // same name as that passed into serialization macro in SaveGame function.
63         SaveGamePreviewData& save_preview_data = full.preview;
64         GalaxySetupData& galaxy_setup_data = full.galaxy;
65 
66         DebugLogger() << "LoadSaveGamePreviewData: Loading preview from: " << path.string();
67         try {
68             // read the first five letters of the stream and check if it is opening an xml file
69             std::string xxx5(5, ' ');
70             ifs.read(&xxx5[0], 5);
71             const std::string xml5{"<?xml"};
72             // reset to start of stream
73             boost::iostreams::seek(ifs, 0, std::ios_base::beg);
74             // binary deserialization iff document is not xml
75             if (xml5 != xxx5) {
76                 // first attempt binary deserialziation
77                 freeorion_bin_iarchive ia(ifs);
78 
79                 ia >> BOOST_SERIALIZATION_NVP(save_preview_data);
80                 ia >> BOOST_SERIALIZATION_NVP(galaxy_setup_data);
81 
82             } else {
83                 freeorion_xml_iarchive ia(ifs);
84                 ia >> BOOST_SERIALIZATION_NVP(save_preview_data);
85 
86                 if (BOOST_VERSION >= 106600 && save_preview_data.save_format_marker == XML_COMPRESSED_MARKER)
87                     throw std::invalid_argument("Save Format Not Compatible with Boost Version " BOOST_LIB_VERSION);
88 
89                 ia >> BOOST_SERIALIZATION_NVP(galaxy_setup_data);
90             }
91 
92             DebugLogger() << "Loaded preview with: " << save_preview_data.number_of_human_players << " human players";
93 
94         } catch (const std::exception& e) {
95             ErrorLogger() << "LoadSaveGamePreviewData: Failed to read preview of " << path.string() << " because: " << e.what();
96             return false;
97         }
98 
99         if (full.preview.Valid()) {
100             DebugLogger() << "LoadSaveGamePreviewData: Successfully loaded preview from: " << path.string();
101             return true;
102         } else {
103             DebugLogger() << "LoadSaveGamePreviewData: Passing save file with no preview: " << path.string();
104             return false;
105         }
106     }
107 }
108 
109 
SaveGamePreviewData()110 SaveGamePreviewData::SaveGamePreviewData() :
111     magic_number(PREVIEW_PRESENT_MARKER),
112     description(),
113     freeorion_version(UserString("UNKNOWN_VALUE_SYMBOL_2")),
114     main_player_name(UserString("UNKNOWN_VALUE_SYMBOL_2")),
115     main_player_empire_name(UserString("UNKNOWN_VALUE_SYMBOL_2")),
116     current_turn(-1),
117     number_of_empires(-1),
118     number_of_human_players(-1),
119     save_format_marker("")
120 {}
121 
Valid() const122 bool SaveGamePreviewData::Valid() const
123 { return magic_number == SaveGamePreviewData::PREVIEW_PRESENT_MARKER && current_turn >= -1; }
124 
SetBinary(bool bin)125 void SaveGamePreviewData::SetBinary(bool bin)
126 { description = bin ? BIN_SAVE_FILE_DESCRIPTION : XML_SAVE_FILE_DESCRIPTION; }
127 
128 template <typename Archive>
serialize(Archive & ar,unsigned int version)129 void SaveGamePreviewData::serialize(Archive& ar, unsigned int version)
130 {
131     if (version >= 2) {
132         if (Archive::is_saving::value) {
133             freeorion_version = FreeOrionVersionString();
134         }
135         ar & BOOST_SERIALIZATION_NVP(description)
136            & BOOST_SERIALIZATION_NVP(freeorion_version);
137         if (version >= 3) {
138             ar & BOOST_SERIALIZATION_NVP(save_format_marker);
139             if (version >= 4) {
140                 ar & BOOST_SERIALIZATION_NVP(uncompressed_text_size)
141                    & BOOST_SERIALIZATION_NVP(compressed_text_size);
142             }
143         }
144     }
145     ar & BOOST_SERIALIZATION_NVP(magic_number)
146        & BOOST_SERIALIZATION_NVP(main_player_name);
147     ar & BOOST_SERIALIZATION_NVP(main_player_empire_name)
148        & BOOST_SERIALIZATION_NVP(main_player_empire_colour)
149        & BOOST_SERIALIZATION_NVP(save_time)
150        & BOOST_SERIALIZATION_NVP(current_turn);
151     if (version > 0) {
152         ar & BOOST_SERIALIZATION_NVP(number_of_empires)
153            & BOOST_SERIALIZATION_NVP(number_of_human_players);
154     }
155 }
156 
157 template void SaveGamePreviewData::serialize<freeorion_bin_oarchive>(freeorion_bin_oarchive&, unsigned int);
158 template void SaveGamePreviewData::serialize<freeorion_bin_iarchive>(freeorion_bin_iarchive&, unsigned int);
159 template void SaveGamePreviewData::serialize<freeorion_xml_oarchive>(freeorion_xml_oarchive&, unsigned int);
160 template void SaveGamePreviewData::serialize<freeorion_xml_iarchive>(freeorion_xml_iarchive&, unsigned int);
161 
162 
163 template <typename Archive>
serialize(Archive & ar,unsigned int version)164 void FullPreview::serialize(Archive& ar, unsigned int version)
165 {
166     ar & BOOST_SERIALIZATION_NVP(filename)
167        & BOOST_SERIALIZATION_NVP(preview)
168        & BOOST_SERIALIZATION_NVP(galaxy);
169 }
170 
171 template void FullPreview::serialize<freeorion_bin_oarchive>(freeorion_bin_oarchive&, unsigned int);
172 template void FullPreview::serialize<freeorion_bin_iarchive>(freeorion_bin_iarchive&, unsigned int);
173 template void FullPreview::serialize<freeorion_xml_oarchive>(freeorion_xml_oarchive&, unsigned int);
174 template void FullPreview::serialize<freeorion_xml_iarchive>(freeorion_xml_iarchive&, unsigned int);
175 
176 
177 template<typename Archive>
serialize(Archive & ar,const unsigned int version)178 void PreviewInformation::serialize(Archive& ar, const unsigned int version)
179 {
180     ar & BOOST_SERIALIZATION_NVP(subdirectories)
181        & BOOST_SERIALIZATION_NVP(folder)
182        & BOOST_SERIALIZATION_NVP(previews);
183 }
184 
185 template void PreviewInformation::serialize<freeorion_bin_oarchive>(freeorion_bin_oarchive&, const unsigned int);
186 template void PreviewInformation::serialize<freeorion_bin_iarchive>(freeorion_bin_iarchive&, const unsigned int);
187 template void PreviewInformation::serialize<freeorion_xml_oarchive>(freeorion_xml_oarchive&, const unsigned int);
188 template void PreviewInformation::serialize<freeorion_xml_iarchive>(freeorion_xml_iarchive&, const unsigned int);
189 
190 
SaveFileWithValidHeader(const boost::filesystem::path & path)191 bool SaveFileWithValidHeader(const boost::filesystem::path& path) {
192     if (!fs::exists(path))
193         return false;
194 
195     fs::ifstream ifs(path, std::ios_base::binary);
196     if (!ifs)
197         return false;
198 
199     // dummy holders for deserialized data
200     SaveGamePreviewData                 ignored_save_preview_data;
201     GalaxySetupData                     ignored_galaxy_setup_data;
202     ServerSaveGameData                  ignored_server_save_game_data;
203     std::vector<PlayerSaveHeaderData>   ignored_player_save_header_data;
204     std::map<int, SaveGameEmpireData>   ignored_empire_save_game_data;
205 
206     DebugLogger() << "SaveFileWithValidHeader: Loading headers from: " << path.string();
207     try {
208         // read the first five letters of the stream and check if it is opening an xml file
209         std::string xxx5(5, ' ');
210         ifs.read(&xxx5[0], 5);
211         const std::string xml5{"<?xml"};
212         // reset to start of stream
213         boost::iostreams::seek(ifs, 0, std::ios_base::beg);
214         // binary deserialization iff document is not xml
215         if (xml5 != xxx5) {
216             ScopedTimer timer("SaveFileWithValidHeader (binary): " + path.string(), true);
217 
218             freeorion_bin_iarchive ia(ifs);
219 
220             ia >> BOOST_SERIALIZATION_NVP(ignored_save_preview_data);
221             ia >> BOOST_SERIALIZATION_NVP(ignored_galaxy_setup_data);
222             ia >> BOOST_SERIALIZATION_NVP(ignored_server_save_game_data);
223             ia >> BOOST_SERIALIZATION_NVP(ignored_player_save_header_data);
224             ia >> BOOST_SERIALIZATION_NVP(ignored_empire_save_game_data);
225         } else {
226             DebugLogger() << "Deserializing XML data";
227             freeorion_xml_iarchive ia(ifs);
228 
229             ia >> BOOST_SERIALIZATION_NVP(ignored_save_preview_data);
230 
231             if (BOOST_VERSION >= 106600 && ignored_save_preview_data.save_format_marker == XML_COMPRESSED_MARKER)
232                 throw std::invalid_argument("Save Format Not Compatible with Boost Version " BOOST_LIB_VERSION);
233 
234             ia >> BOOST_SERIALIZATION_NVP(ignored_galaxy_setup_data);
235             ia >> BOOST_SERIALIZATION_NVP(ignored_server_save_game_data);
236             ia >> BOOST_SERIALIZATION_NVP(ignored_player_save_header_data);
237             ia >> BOOST_SERIALIZATION_NVP(ignored_empire_save_game_data);
238         }
239 
240     } catch (const std::exception& e) {
241         ErrorLogger() << "SaveFileWithValidHeader: Failed to read headers of " << path.string() << " because: " << e.what();
242         return false;
243     }
244     return true;
245 }
246 
ColumnInPreview(const FullPreview & full,const std::string & name,bool thin)247 std::string ColumnInPreview(const FullPreview& full, const std::string& name, bool thin) {
248     if (name == "player") {
249         return full.preview.main_player_name;
250     } else if (name == "empire") {
251         return full.preview.main_player_empire_name;
252     } else if (name == "turn") {
253         return std::to_string(full.preview.current_turn);
254     } else if (name == "time") {
255         if (thin) {
256             return split_time(full.preview.save_time);
257         } else {
258             return full.preview.save_time;
259         }
260     } else if (name == "file") {
261         return full.filename;
262     } else if (name == "galaxy_size") {
263         return std::to_string(full.galaxy.m_size);
264     } else if (name == "seed") {
265         return full.galaxy.m_seed;
266     } else if (name == "galaxy_age") {
267         return TextForGalaxySetupSetting(full.galaxy.m_age);
268     } else if (name == "monster_freq") {
269         return TextForGalaxySetupSetting(full.galaxy.m_monster_freq);
270     } else if (name == "native_freq") {
271         return TextForGalaxySetupSetting(full.galaxy.m_native_freq);
272     } else if (name == "planet_freq") {
273         return TextForGalaxySetupSetting(full.galaxy.m_planet_density);
274     } else if (name == "specials_freq") {
275         return TextForGalaxySetupSetting(full.galaxy.m_specials_freq);
276     } else if (name == "starlane_freq") {
277         return TextForGalaxySetupSetting(full.galaxy.m_starlane_freq);
278     } else if (name == "galaxy_shape") {
279         return TextForGalaxyShape(full.galaxy.m_shape);
280     } else if (name == "ai_aggression") {
281         return TextForAIAggression(full.galaxy.m_ai_aggr);
282     } else if (name == "number_of_empires") {
283         return std::to_string(full.preview.number_of_empires);
284     } else if (name == "number_of_humans") {
285         return std::to_string(full.preview.number_of_human_players);
286     } else {
287         ErrorLogger() << "FullPreview::Value Error: no such preview field: " << name;
288         return "??";
289     }
290 }
291 
LoadSaveGamePreviews(const fs::path & orig_path,const std::string & extension,std::vector<FullPreview> & previews)292 void LoadSaveGamePreviews(const fs::path& orig_path, const std::string& extension, std::vector<FullPreview>& previews) {
293     FullPreview data;
294     fs::directory_iterator end_it;
295 
296     fs::path path = orig_path;
297     // Relative path relative to the save directory
298     if (path.is_relative()) {
299         ErrorLogger() << "LoadSaveGamePreviews: supplied path must not be relative, \"" << path << "\" ";
300         return;
301     }
302 
303     if (!fs::exists(path)) {
304         ErrorLogger() << "LoadSaveGamePreviews: Save Game directory \"" << path << "\" not found";
305         return;
306     }
307     if (!fs::is_directory(path)) {
308         ErrorLogger() << "LoadSaveGamePreviews: Save Game directory \"" << path << "\" was not a directory";
309         return;
310     }
311 
312     for (fs::directory_iterator it(path); it != end_it; ++it) {
313         try {
314             if ((it->path().filename().extension() == extension) && !fs::is_directory(it->path())) {
315                 if (LoadSaveGamePreviewData(*it, data)) {
316                     // Add preview entry to list
317                     previews.push_back(data);
318                 }
319             }
320         } catch (const std::exception& e) {
321             ErrorLogger() << "LoadSaveGamePreviews: Failed loading preview from " << it->path() << " because: " << e.what();
322         }
323     }
324 }
325