Removed binreloc support, SDL2 provides that functionality now with SDL_GetBasePath()
[supertux.git] / src / supertux / savegame.cpp
1 //  SuperTux
2 //  Copyright (C) 2006 Matthias Braun <matze@braunis.de>
3 //                2014 Ingo Ruhnke <grumbel@gmx.de>
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 "lisp/lisp.hpp"
21 #include "lisp/parser.hpp"
22 #include "lisp/writer.hpp"
23 #include "physfs/ifile_streambuf.hpp"
24 #include "scripting/scripting.hpp"
25 #include "scripting/serialize.hpp"
26 #include "scripting/squirrel_util.hpp"
27 #include "supertux/player_status.hpp"
28 #include "util/file_system.hpp"
29 #include "util/log.hpp"
30 #include "worldmap/worldmap.hpp"
31
32 namespace {
33
34 void get_table_entry(HSQUIRRELVM vm, const std::string& name)
35 {
36   sq_pushstring(vm, name.c_str(), -1);
37   if(SQ_FAILED(sq_get(vm, -2)))
38   {
39     throw std::runtime_error("failed to get '" + name + "' table entry");
40   }
41   else
42   {
43     // successfully placed result on stack
44   }
45 }
46
47 void get_or_create_table_entry(HSQUIRRELVM vm, const std::string& name)
48 {
49   sq_pushstring(vm, name.c_str(), -1);
50   if(SQ_FAILED(sq_get(vm, -2)))
51   {
52     sq_pushstring(vm, name.c_str(), -1);
53     sq_newtable(vm);
54     if(SQ_FAILED(sq_newslot(vm, -3, SQFalse)))
55     {
56       throw std::runtime_error("failed to create '" + name + "' table entry");
57     }
58     else
59     {
60       get_table_entry(vm, name);
61     }
62   }
63   else
64   {
65     // successfully placed result on stack
66   }
67 }
68
69 std::vector<std::string> get_table_keys(HSQUIRRELVM vm)
70 {
71   std::vector<std::string> worlds;
72
73   sq_pushnull(vm);
74   while(SQ_SUCCEEDED(sq_next(vm, -2)))
75   {
76     //here -1 is the value and -2 is the key
77     const char* result;
78     if(SQ_FAILED(sq_getstring(vm, -2, &result)))
79     {
80       std::ostringstream msg;
81       msg << "Couldn't get string value for key";
82       throw scripting::SquirrelError(vm, msg.str());
83     }
84     else
85     {
86       worlds.push_back(result);
87     }
88
89     // pops key and val before the next iteration
90     sq_pop(vm, 2);
91   }
92
93   return worlds;
94 }
95
96 std::vector<LevelState> get_level_states(HSQUIRRELVM vm)
97 {
98   std::vector<LevelState> results;
99
100   sq_pushnull(vm);
101   while(SQ_SUCCEEDED(sq_next(vm, -2)))
102   {
103     //here -1 is the value and -2 is the key
104     const char* result;
105     if(SQ_FAILED(sq_getstring(vm, -2, &result)))
106     {
107       std::ostringstream msg;
108       msg << "Couldn't get string value";
109       throw scripting::SquirrelError(vm, msg.str());
110     }
111     else
112     {
113       LevelState level_state;
114       level_state.filename = result;
115       scripting::get_bool(vm, "solved", level_state.solved);
116       scripting::get_bool(vm, "perfect", level_state.perfect);
117
118       results.push_back(level_state);
119     }
120
121     // pops key and val before the next iteration
122     sq_pop(vm, 2);
123   }
124
125   return results;
126 }
127
128 } // namespace
129
130 void
131 LevelsetState::store_level_state(const LevelState& in_state)
132 {
133   auto it = std::find_if(level_states.begin(), level_states.end(),
134                          [&in_state](const LevelState& state)
135                          {
136                            return state.filename == in_state.filename;
137                          });
138   if (it != level_states.end())
139   {
140     *it = in_state;
141   }
142   else
143   {
144     level_states.push_back(in_state);
145   }
146 }
147
148 LevelState
149 LevelsetState::get_level_state(const std::string& filename)
150 {
151   auto it = std::find_if(level_states.begin(), level_states.end(),
152                          [filename](const LevelState& state)
153                          {
154                            return state.filename == filename;
155                          });
156   if (it != level_states.end())
157   {
158     return *it;
159   }
160   else
161   {
162     log_warning << "failed to retrieve level state for " << filename << std::endl;
163     LevelState state;
164     state.filename = filename;
165     return state;
166   }
167 }
168
169 Savegame::Savegame(const std::string& filename) :
170   m_filename(filename),
171   m_player_status(new PlayerStatus)
172 {
173 }
174
175 Savegame::~Savegame()
176 {
177 }
178
179 void
180 Savegame::load()
181 {
182   if (m_filename.empty())
183   {
184     log_debug << "no filename set for savegame, skipping load" << std::endl;
185     return;
186   }
187
188   clear_state_table();
189
190   if(!PHYSFS_exists(m_filename.c_str()))
191   {
192     log_info << m_filename << ": doesn't exist, not loading state" << std::endl;
193   }
194   else
195   {
196     log_debug << "loading savegame from " << m_filename << std::endl;
197
198     try
199     {
200       HSQUIRRELVM vm = scripting::global_vm;
201
202       lisp::Parser parser;
203       const lisp::Lisp* root = parser.parse(m_filename);
204
205       const lisp::Lisp* lisp = root->get_lisp("supertux-savegame");
206       if(lisp == NULL)
207       {
208         throw std::runtime_error("file is not a supertux-savegame file");
209       }
210       else
211       {
212         int version = 1;
213         lisp->get("version", version);
214         if(version != 1)
215         {
216           throw std::runtime_error("incompatible savegame version");
217         }
218         else
219         {
220           const lisp::Lisp* tux = lisp->get_lisp("tux");
221           if(tux == NULL)
222           {
223             throw std::runtime_error("No tux section in savegame");
224           }
225           {
226             m_player_status->read(*tux);
227           }
228
229           const lisp::Lisp* state = lisp->get_lisp("state");
230           if(state == NULL)
231           {
232             throw std::runtime_error("No state section in savegame");
233           }
234           else
235           {
236             sq_pushroottable(vm);
237             get_table_entry(vm, "state");
238             scripting::load_squirrel_table(vm, -1, *state);
239             sq_pop(vm, 2);
240           }
241         }
242       }
243     }
244     catch(const std::exception& e)
245     {
246       log_fatal << "Couldn't load savegame: " << e.what() << std::endl;
247     }
248   }
249 }
250
251 void
252 Savegame::clear_state_table()
253 {
254   HSQUIRRELVM vm = scripting::global_vm;
255
256   // delete existing state table, if it exists
257   sq_pushroottable(vm);
258   {
259     // create a new empty state table
260     sq_pushstring(vm, "state", -1);
261     sq_newtable(vm);
262     if(SQ_FAILED(sq_newslot(vm, -3, SQFalse)))
263     {
264       throw std::runtime_error("Couldn't create state table");
265     }
266   }
267   sq_pop(vm, 1);
268 }
269
270 void
271 Savegame::save()
272 {
273   if (m_filename.empty())
274   {
275     log_debug << "no filename set for savegame, skipping save" << std::endl;
276     return;
277   }
278
279   log_debug << "saving savegame to " << m_filename << std::endl;
280
281   { // make sure the savegame directory exists
282     std::string dirname = FileSystem::dirname(m_filename);
283     if(!PHYSFS_exists(dirname.c_str()))
284     {
285       if(!PHYSFS_mkdir(dirname.c_str()))
286       {
287         std::ostringstream msg;
288         msg << "Couldn't create directory for savegames '"
289             << dirname << "': " <<PHYSFS_getLastError();
290         throw std::runtime_error(msg.str());
291       }
292     }
293
294     if(!PHYSFS_isDirectory(dirname.c_str()))
295     {
296       std::ostringstream msg;
297       msg << "Savegame path '" << dirname << "' is not a directory";
298       throw std::runtime_error(msg.str());
299     }
300   }
301
302   HSQUIRRELVM vm = scripting::global_vm;
303
304   lisp::Writer writer(m_filename);
305
306   writer.start_list("supertux-savegame");
307   writer.write("version", 1);
308
309   using namespace worldmap;
310   if(WorldMap::current() != NULL)
311   {
312     std::ostringstream title;
313     title << WorldMap::current()->get_title();
314     title << " (" << WorldMap::current()->solved_level_count()
315           << "/" << WorldMap::current()->level_count() << ")";
316     writer.write("title", title.str());
317   }
318
319   writer.start_list("tux");
320   m_player_status->write(writer);
321   writer.end_list("tux");
322
323   writer.start_list("state");
324
325   sq_pushroottable(vm);
326   sq_pushstring(vm, "state", -1);
327   if(SQ_SUCCEEDED(sq_get(vm, -2)))
328   {
329     scripting::save_squirrel_table(vm, -1, writer);
330     sq_pop(vm, 1);
331   }
332   sq_pop(vm, 1);
333   writer.end_list("state");
334
335   writer.end_list("supertux-savegame");
336 }
337
338 std::vector<std::string>
339 Savegame::get_worldmaps()
340 {
341   std::vector<std::string> worlds;
342
343   HSQUIRRELVM vm = scripting::global_vm;
344   int oldtop = sq_gettop(vm);
345
346   try
347   {
348     sq_pushroottable(vm);
349     get_table_entry(vm, "state");
350     get_table_entry(vm, "worlds");
351     worlds = get_table_keys(vm);
352   }
353   catch(const std::exception& err)
354   {
355     log_warning << err.what() << std::endl;
356   }
357
358   sq_settop(vm, oldtop);
359
360   return worlds;
361 }
362
363 WorldmapState
364 Savegame::get_worldmap_state(const std::string& name)
365 {
366   WorldmapState result;
367
368   HSQUIRRELVM vm = scripting::global_vm;
369   int oldtop = sq_gettop(vm);
370
371   try
372   {
373     sq_pushroottable(vm);
374     get_table_entry(vm, "state");
375     get_table_entry(vm, "worlds");
376     get_table_entry(vm, name);
377     get_table_entry(vm, "levels");
378
379     result.level_states = get_level_states(vm);
380   }
381   catch(const std::exception& err)
382   {
383     log_warning << err.what() << std::endl;
384   }
385
386   sq_settop(vm, oldtop);
387
388   return result;
389 }
390
391 std::vector<std::string>
392 Savegame::get_levelsets()
393 {
394   std::vector<std::string> results;
395
396   HSQUIRRELVM vm = scripting::global_vm;
397   int oldtop = sq_gettop(vm);
398
399   try
400   {
401     sq_pushroottable(vm);
402     get_table_entry(vm, "state");
403     get_table_entry(vm, "levelsets");
404     results = get_table_keys(vm);
405   }
406   catch(const std::exception& err)
407   {
408     log_warning << err.what() << std::endl;
409   }
410
411   sq_settop(vm, oldtop);
412
413   return results;
414 }
415
416 LevelsetState
417 Savegame::get_levelset_state(const std::string& basedir)
418 {
419   LevelsetState result;
420
421   HSQUIRRELVM vm = scripting::global_vm;
422   int oldtop = sq_gettop(vm);
423
424   try
425   {
426     sq_pushroottable(vm);
427     get_table_entry(vm, "state");
428     get_table_entry(vm, "levelsets");
429     get_table_entry(vm, basedir);
430     get_table_entry(vm, "levels");
431
432     result.level_states = get_level_states(vm);
433   }
434   catch(const std::exception& err)
435   {
436     log_warning << err.what() << std::endl;
437   }
438
439   sq_settop(vm, oldtop);
440
441   return result;
442 }
443
444 void
445 Savegame::set_levelset_state(const std::string& basedir,
446                              const std::string& level_filename,
447                              bool solved)
448 {
449   LevelsetState state = get_levelset_state(basedir);
450
451   HSQUIRRELVM vm = scripting::global_vm;
452   int oldtop = sq_gettop(vm);
453
454   try
455   {
456     sq_pushroottable(vm);
457     get_table_entry(vm, "state");
458     get_or_create_table_entry(vm, "levelsets");
459     get_or_create_table_entry(vm, basedir);
460     get_or_create_table_entry(vm, "levels");
461     get_or_create_table_entry(vm, level_filename);
462
463     bool old_solved = false;
464     scripting::get_bool(vm, "solved", old_solved);
465     scripting::store_bool(vm, "solved", solved || old_solved);
466   }
467   catch(const std::exception& err)
468   {
469     log_warning << err.what() << std::endl;
470   }
471
472   sq_settop(vm, oldtop);
473 }
474
475 /* EOF */