1 | // SuperTux |
2 | // Copyright (C) 2006 Matthias Braun <matze@braunis.de> |
3 | // 2014 Ingo Ruhnke <grumbel@gmail.com> |
4 | // |
5 | // This program is free software: you can redistribute it and/or modify |
6 | // it under the terms of the GNU General Public License as published by |
7 | // the Free Software Foundation, either version 3 of the License, or |
8 | // (at your option) any later version. |
9 | // |
10 | // This program is distributed in the hope that it will be useful, |
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | // GNU General Public License for more details. |
14 | // |
15 | // You should have received a copy of the GNU General Public License |
16 | // along with this program. If not, see <http://www.gnu.org/licenses/>. |
17 | |
18 | #include "supertux/savegame.hpp" |
19 | |
20 | #include <algorithm> |
21 | #include <physfs.h> |
22 | |
23 | #include "physfs/physfs_file_system.hpp" |
24 | #include "physfs/util.hpp" |
25 | #include "squirrel/serialize.hpp" |
26 | #include "squirrel/squirrel_util.hpp" |
27 | #include "squirrel/squirrel_virtual_machine.hpp" |
28 | #include "supertux/player_status.hpp" |
29 | #include "util/file_system.hpp" |
30 | #include "util/log.hpp" |
31 | #include "util/reader_document.hpp" |
32 | #include "util/reader_mapping.hpp" |
33 | #include "util/writer.hpp" |
34 | #include "worldmap/worldmap.hpp" |
35 | |
36 | namespace { |
37 | |
38 | std::vector<LevelState> get_level_states(SquirrelVM& vm) |
39 | { |
40 | std::vector<LevelState> results; |
41 | |
42 | sq_pushnull(vm.get_vm()); |
43 | while (SQ_SUCCEEDED(sq_next(vm.get_vm(), -2))) |
44 | { |
45 | //here -1 is the value and -2 is the key |
46 | const char* result; |
47 | if (SQ_FAILED(sq_getstring(vm.get_vm(), -2, &result))) |
48 | { |
49 | std::ostringstream msg; |
50 | msg << "Couldn't get string value" ; |
51 | throw SquirrelError(vm.get_vm(), msg.str()); |
52 | } |
53 | else |
54 | { |
55 | LevelState level_state; |
56 | level_state.filename = result; |
57 | vm.get_bool("solved" , level_state.solved); |
58 | vm.get_bool("perfect" , level_state.perfect); |
59 | |
60 | results.push_back(level_state); |
61 | } |
62 | |
63 | // pops key and val before the next iteration |
64 | sq_pop(vm.get_vm(), 2); |
65 | } |
66 | |
67 | return results; |
68 | } |
69 | |
70 | } // namespace |
71 | |
72 | void |
73 | LevelsetState::store_level_state(const LevelState& in_state) |
74 | { |
75 | auto it = std::find_if(level_states.begin(), level_states.end(), |
76 | [&in_state](const LevelState& state) |
77 | { |
78 | return state.filename == in_state.filename; |
79 | }); |
80 | if (it != level_states.end()) |
81 | { |
82 | *it = in_state; |
83 | } |
84 | else |
85 | { |
86 | level_states.push_back(in_state); |
87 | } |
88 | } |
89 | |
90 | LevelState |
91 | LevelsetState::get_level_state(const std::string& filename) const |
92 | { |
93 | auto it = std::find_if(level_states.begin(), level_states.end(), |
94 | [filename](const LevelState& state) |
95 | { |
96 | return state.filename == filename; |
97 | }); |
98 | if (it != level_states.end()) |
99 | { |
100 | return *it; |
101 | } |
102 | else |
103 | { |
104 | log_info << "creating new level state for " << filename << std::endl; |
105 | LevelState state; |
106 | state.filename = filename; |
107 | return state; |
108 | } |
109 | } |
110 | |
111 | std::unique_ptr<Savegame> |
112 | Savegame::from_file(const std::string& filename) |
113 | { |
114 | std::unique_ptr<Savegame> savegame(new Savegame(filename)); |
115 | savegame->load(); |
116 | return savegame; |
117 | } |
118 | |
119 | Savegame::Savegame(const std::string& filename) : |
120 | m_filename(filename), |
121 | m_player_status(new PlayerStatus) |
122 | { |
123 | } |
124 | |
125 | bool |
126 | Savegame::is_title_screen() const |
127 | { |
128 | // bit of a hack, TileScreen uses a dummy savegame without a filename |
129 | return m_filename.empty(); |
130 | } |
131 | |
132 | void |
133 | Savegame::load() |
134 | { |
135 | if (m_filename.empty()) |
136 | { |
137 | log_debug << "no filename set for savegame, skipping load" << std::endl; |
138 | return; |
139 | } |
140 | |
141 | clear_state_table(); |
142 | |
143 | if (!PHYSFS_exists(m_filename.c_str())) |
144 | { |
145 | log_info << m_filename << " doesn't exist, not loading state" << std::endl; |
146 | } |
147 | else |
148 | { |
149 | if (physfsutil::is_directory(m_filename)) |
150 | { |
151 | log_info << m_filename << " is a directory, not loading state" << std::endl; |
152 | return; |
153 | } |
154 | log_debug << "loading savegame from " << m_filename << std::endl; |
155 | |
156 | try |
157 | { |
158 | SquirrelVM& vm = SquirrelVirtualMachine::current()->get_vm(); |
159 | |
160 | auto doc = ReaderDocument::from_file(m_filename); |
161 | auto root = doc.get_root(); |
162 | |
163 | if (root.get_name() != "supertux-savegame" ) |
164 | { |
165 | throw std::runtime_error("file is not a supertux-savegame file" ); |
166 | } |
167 | else |
168 | { |
169 | auto mapping = root.get_mapping(); |
170 | |
171 | int version = 1; |
172 | mapping.get("version" , version); |
173 | if (version != 1) |
174 | { |
175 | throw std::runtime_error("incompatible savegame version" ); |
176 | } |
177 | else |
178 | { |
179 | boost::optional<ReaderMapping> tux; |
180 | if (!mapping.get("tux" , tux)) |
181 | { |
182 | throw std::runtime_error("No tux section in savegame" ); |
183 | } |
184 | { |
185 | m_player_status->read(*tux); |
186 | } |
187 | |
188 | boost::optional<ReaderMapping> state; |
189 | if (!mapping.get("state" , state)) |
190 | { |
191 | throw std::runtime_error("No state section in savegame" ); |
192 | } |
193 | else |
194 | { |
195 | sq_pushroottable(vm.get_vm()); |
196 | vm.get_table_entry("state" ); |
197 | load_squirrel_table(vm.get_vm(), -1, *state); |
198 | sq_pop(vm.get_vm(), 2); |
199 | } |
200 | } |
201 | } |
202 | } |
203 | catch(const std::exception& e) |
204 | { |
205 | log_fatal << "Couldn't load savegame: " << e.what() << std::endl; |
206 | } |
207 | } |
208 | } |
209 | |
210 | void |
211 | Savegame::clear_state_table() |
212 | { |
213 | SquirrelVM& vm = SquirrelVirtualMachine::current()->get_vm(); |
214 | |
215 | // delete existing state table, if it exists |
216 | sq_pushroottable(vm.get_vm()); |
217 | { |
218 | // create a new empty state table |
219 | vm.create_empty_table("state" ); |
220 | } |
221 | sq_pop(vm.get_vm(), 1); |
222 | } |
223 | |
224 | void |
225 | Savegame::save() |
226 | { |
227 | if (m_filename.empty()) |
228 | { |
229 | log_debug << "no filename set for savegame, skipping save" << std::endl; |
230 | return; |
231 | } |
232 | |
233 | log_debug << "saving savegame to " << m_filename << std::endl; |
234 | |
235 | { // make sure the savegame directory exists |
236 | std::string dirname = FileSystem::dirname(m_filename); |
237 | if (!PHYSFS_exists(dirname.c_str())) |
238 | { |
239 | if (!PHYSFS_mkdir(dirname.c_str())) |
240 | { |
241 | std::ostringstream msg; |
242 | msg << "Couldn't create directory for savegames '" |
243 | << dirname << "': " <<PHYSFS_getLastErrorCode(); |
244 | throw std::runtime_error(msg.str()); |
245 | } |
246 | } |
247 | |
248 | if (!physfsutil::is_directory(dirname)) |
249 | { |
250 | std::ostringstream msg; |
251 | msg << "Savegame path '" << dirname << "' is not a directory" ; |
252 | throw std::runtime_error(msg.str()); |
253 | } |
254 | } |
255 | |
256 | SquirrelVM& vm = SquirrelVirtualMachine::current()->get_vm(); |
257 | |
258 | Writer writer(m_filename); |
259 | |
260 | writer.start_list("supertux-savegame" ); |
261 | writer.write("version" , 1); |
262 | |
263 | using namespace worldmap; |
264 | if (WorldMap::current() != nullptr) |
265 | { |
266 | std::ostringstream title; |
267 | title << WorldMap::current()->get_title(); |
268 | title << " (" << WorldMap::current()->solved_level_count() |
269 | << "/" << WorldMap::current()->level_count() << ")" ; |
270 | writer.write("title" , title.str()); |
271 | } |
272 | |
273 | writer.start_list("tux" ); |
274 | m_player_status->write(writer); |
275 | writer.end_list("tux" ); |
276 | |
277 | writer.start_list("state" ); |
278 | |
279 | sq_pushroottable(vm.get_vm()); |
280 | try |
281 | { |
282 | vm.get_table_entry("state" ); // Push "state" |
283 | save_squirrel_table(vm.get_vm(), -1, writer); |
284 | sq_pop(vm.get_vm(), 1); // Pop "state" |
285 | } |
286 | catch(const std::exception&) |
287 | { |
288 | } |
289 | sq_pop(vm.get_vm(), 1); // Pop root table |
290 | writer.end_list("state" ); |
291 | |
292 | writer.end_list("supertux-savegame" ); |
293 | } |
294 | |
295 | std::vector<std::string> |
296 | Savegame::get_worldmaps() |
297 | { |
298 | std::vector<std::string> worlds; |
299 | |
300 | SquirrelVM& vm = SquirrelVirtualMachine::current()->get_vm(); |
301 | SQInteger oldtop = sq_gettop(vm.get_vm()); |
302 | |
303 | try |
304 | { |
305 | sq_pushroottable(vm.get_vm()); |
306 | vm.get_table_entry("state" ); |
307 | vm.get_or_create_table_entry("worlds" ); |
308 | worlds = vm.get_table_keys(); |
309 | } |
310 | catch(const std::exception& err) |
311 | { |
312 | log_warning << err.what() << std::endl; |
313 | } |
314 | |
315 | sq_settop(vm.get_vm(), oldtop); |
316 | |
317 | // ensure that the loaded worldmap names have their canonical form |
318 | std::transform(worlds.begin(), worlds.end(), worlds.begin(), physfsutil::realpath); |
319 | |
320 | return worlds; |
321 | } |
322 | |
323 | WorldmapState |
324 | Savegame::get_worldmap_state(const std::string& name) |
325 | { |
326 | WorldmapState result; |
327 | |
328 | SquirrelVM& vm = SquirrelVirtualMachine::current()->get_vm(); |
329 | SQInteger oldtop = sq_gettop(vm.get_vm()); |
330 | |
331 | try |
332 | { |
333 | sq_pushroottable(vm.get_vm()); |
334 | vm.get_table_entry("state" ); |
335 | vm.get_or_create_table_entry("worlds" ); |
336 | |
337 | // if a non-canonical entry is present, replace them with a canonical one |
338 | if (name != "/levels/world2/worldmap.stwm" ) { |
339 | std::string old_map_filename = name.substr(1); |
340 | if (vm.has_property(old_map_filename.c_str())) { |
341 | vm.rename_table_entry(old_map_filename.c_str(), name.c_str()); |
342 | } |
343 | } |
344 | |
345 | vm.get_or_create_table_entry(name); |
346 | vm.get_or_create_table_entry("levels" ); |
347 | |
348 | result.level_states = get_level_states(vm); |
349 | } |
350 | catch(const std::exception& err) |
351 | { |
352 | log_warning << err.what() << std::endl; |
353 | } |
354 | |
355 | sq_settop(vm.get_vm(), oldtop); |
356 | |
357 | return result; |
358 | } |
359 | |
360 | std::vector<std::string> |
361 | Savegame::get_levelsets() |
362 | { |
363 | std::vector<std::string> results; |
364 | |
365 | SquirrelVM& vm = SquirrelVirtualMachine::current()->get_vm(); |
366 | SQInteger oldtop = sq_gettop(vm.get_vm()); |
367 | |
368 | try |
369 | { |
370 | sq_pushroottable(vm.get_vm()); |
371 | vm.get_table_entry("state" ); |
372 | vm.get_or_create_table_entry("levelsets" ); |
373 | results = vm.get_table_keys(); |
374 | } |
375 | catch(const std::exception& err) |
376 | { |
377 | log_warning << err.what() << std::endl; |
378 | } |
379 | |
380 | sq_settop(vm.get_vm(), oldtop); |
381 | |
382 | return results; |
383 | } |
384 | |
385 | LevelsetState |
386 | Savegame::get_levelset_state(const std::string& basedir) |
387 | { |
388 | LevelsetState result; |
389 | |
390 | SquirrelVM& vm = SquirrelVirtualMachine::current()->get_vm(); |
391 | SQInteger oldtop = sq_gettop(vm.get_vm()); |
392 | |
393 | try |
394 | { |
395 | sq_pushroottable(vm.get_vm()); |
396 | vm.get_table_entry("state" ); |
397 | vm.get_or_create_table_entry("levelsets" ); |
398 | vm.get_or_create_table_entry(basedir); |
399 | vm.get_or_create_table_entry("levels" ); |
400 | |
401 | result.level_states = get_level_states(vm); |
402 | } |
403 | catch(const std::exception& err) |
404 | { |
405 | log_warning << err.what() << std::endl; |
406 | } |
407 | |
408 | sq_settop(vm.get_vm(), oldtop); |
409 | |
410 | return result; |
411 | } |
412 | |
413 | void |
414 | Savegame::set_levelset_state(const std::string& basedir, |
415 | const std::string& level_filename, |
416 | bool solved) |
417 | { |
418 | LevelsetState state = get_levelset_state(basedir); |
419 | |
420 | SquirrelVM& vm = SquirrelVirtualMachine::current()->get_vm(); |
421 | SQInteger oldtop = sq_gettop(vm.get_vm()); |
422 | |
423 | try |
424 | { |
425 | sq_pushroottable(vm.get_vm()); |
426 | vm.get_table_entry("state" ); |
427 | vm.get_or_create_table_entry("levelsets" ); |
428 | vm.get_or_create_table_entry(basedir); |
429 | vm.get_or_create_table_entry("levels" ); |
430 | vm.get_or_create_table_entry(level_filename); |
431 | |
432 | bool old_solved = false; |
433 | vm.get_bool("solved" , old_solved); |
434 | vm.store_bool("solved" , solved || old_solved); |
435 | } |
436 | catch(const std::exception& err) |
437 | { |
438 | log_warning << err.what() << std::endl; |
439 | } |
440 | |
441 | sq_settop(vm.get_vm(), oldtop); |
442 | } |
443 | |
444 | /* EOF */ |
445 | |