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
36namespace {
37
38std::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
72void
73LevelsetState::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
90LevelState
91LevelsetState::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
111std::unique_ptr<Savegame>
112Savegame::from_file(const std::string& filename)
113{
114 std::unique_ptr<Savegame> savegame(new Savegame(filename));
115 savegame->load();
116 return savegame;
117}
118
119Savegame::Savegame(const std::string& filename) :
120 m_filename(filename),
121 m_player_status(new PlayerStatus)
122{
123}
124
125bool
126Savegame::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
132void
133Savegame::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
210void
211Savegame::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
224void
225Savegame::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
295std::vector<std::string>
296Savegame::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
323WorldmapState
324Savegame::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
360std::vector<std::string>
361Savegame::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
385LevelsetState
386Savegame::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
413void
414Savegame::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