1 /*
2  * Copyright (C) 2006-2019 Christopho, Solarus - http://www.solarus-games.org
3  *
4  * Solarus is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * Solarus is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17 #include "solarus/core/CurrentQuest.h"
18 #include "solarus/graphics/Surface.h"
19 #include "solarus/lua/ExportableToLuaPtr.h"
20 #include "solarus/lua/LuaContext.h"
21 #include "solarus/lua/LuaTools.h"
22 #include <list>
23 #include <lua.hpp>
24 
25 namespace Solarus {
26 
27 /**
28  * Name of the Lua table representing the menu module.
29  */
30 const std::string LuaContext::menu_module_name = "sol.menu";
31 
32 /**
33  * \brief Initializes the menu features provided to Lua.
34  */
register_menu_module()35 void LuaContext::register_menu_module() {
36 
37   // Functions of sol.menu.
38   std::vector<luaL_Reg> functions = {
39       { "start", menu_api_start },
40       { "stop", menu_api_stop },
41       { "stop_all", menu_api_stop_all },
42       { "is_started", menu_api_is_started },
43   };
44   if (CurrentQuest::is_format_at_least({ 1, 6 })) {
45     functions.insert(functions.end(), {
46         { "bring_to_front", menu_api_bring_to_front },
47         { "bring_to_back", menu_api_bring_to_back },
48     });
49   }
50 
51   register_functions(menu_module_name, functions);
52 }
53 
54 /**
55  * \brief Registers a menu into a context (table or a userdata).
56  *
57  * This function can be called safely even while iterating on the menus list.
58  *
59  * \param menu_ref Lua ref of the menu to add.
60  * \param context_index Index of the table or userdata in the stack.
61  * \param on_top \c true to place this menu on top of existing one in the
62  * same context, \c false to place it behind.
63  */
add_menu(const ScopedLuaRef & menu_ref,int context_index,bool on_top)64 void LuaContext::add_menu(
65     const ScopedLuaRef& menu_ref,
66     int context_index,
67     bool on_top
68 ) {
69 
70   ScopedLuaRef context = LuaTools::create_ref(current_l,context_index);
71   Debug::check_assertion(!context.is_empty(), "creating context with empty context");
72 
73   if(std::count_if(menus.begin(), menus.end(),[&](const LuaMenuData& menu){
74                    return menu.ref == menu_ref;
75   })){
76     LuaTools::error(current_l, "Cannot start an already started menu");
77   }
78 
79   run_on_main([this, on_top, context, menu_ref](lua_State*) {
80     if (on_top) {
81       menus.emplace_back(menu_ref, context);
82     }
83     else {
84       menus.emplace_front(menu_ref, context);
85     }
86     menu_on_started(menu_ref);
87   });
88 }
89 
90 /**
91  * \brief Unregisters all menus associated to a context.
92  *
93  * This function can be called safely even while iterating on the menus list.
94  *
95  * \param context_index Index of a table or userdata containing menus.
96  */
remove_menus(int context_index)97 void LuaContext::remove_menus(int context_index) {
98   // Some menu:on_finished() callbacks may create menus themselves,
99   // and we don't want those new menus to get removed.
100   for (LuaMenuData& menu: menus) {
101     menu.recently_added = false;
102   }
103 
104   for (LuaMenuData& menu: menus) {
105     ScopedLuaRef menu_ref = menu.ref;
106     if (menu.context.equals(current_l,context_index) && !menu.recently_added) {
107       menu.ref.clear();
108       menu.context.clear();
109       menu_on_finished(menu_ref);
110     }
111   }
112 }
113 
114 /**
115  * \brief Unregisters all existing menus.
116  *
117  * This function can be called safely even while iterating on the menus list.
118  */
remove_menus()119 void LuaContext::remove_menus() {
120 
121   // Some menu:on_finished() callbacks may create menus themselves,
122   // and we don't want those new menus to get removed.
123   for (LuaMenuData& menu: menus) {
124     menu.recently_added = false;
125   }
126 
127   for (LuaMenuData& menu: menus) {
128 
129     if (!menu.recently_added) {
130       ScopedLuaRef menu_ref = menu.ref;
131       if (!menu_ref.is_empty()) {
132         menu.ref.clear();
133         menu.context.clear();
134         menu_on_finished(menu_ref);
135       }
136     }
137   }
138 }
139 
140 /**
141  * \brief Destroys immediately all existing menus.
142  */
destroy_menus()143 void LuaContext::destroy_menus() {
144 
145   menus.clear();
146 }
147 
148 /**
149  * \brief Checks all menus and removes the ones that have to be removed.
150  *
151  * Note that the on_update() is called by the context of each menu, not
152  * by this function.
153  */
update_menus()154 void LuaContext::update_menus() {
155 
156   // Destroy the ones that should be removed.
157   for (auto it = menus.begin();
158       it != menus.end();
159       // No ++it (elements may be removed while traversing).
160   ) {
161     it->recently_added = false;
162     if (it->ref.is_empty()) {
163       // Empty ref on a menu means that we should remove.
164       // In this case, context must also be nullptr.
165       Debug::check_assertion(it->context.is_empty(), "Menu with context and no ref");
166       it = menus.erase(it);
167     }
168     else {
169       ++it;
170     }
171   }
172 }
173 
174 /**
175  * \brief Returns whether a value is a menu.
176  * \param l A Lua context.
177  * \param index An index in the stack.
178  * \return \c true if the value at this index is a menu.
179  */
is_menu(lua_State * l,int index)180 bool LuaContext::is_menu(lua_State* l, int index) {
181 
182   index = LuaTools::get_positive_index(l, index);
183 
184   if (!lua_istable(l, index)) {
185     return false;
186   }
187 
188   LuaContext& lua_context = get();
189   for (LuaMenuData& menu: lua_context.menus) {
190     if (menu.ref.is_empty()) {
191       continue;
192     }
193 
194     if(menu.ref.equals(l,index)) {
195       return true;
196     }
197   }
198   return false;
199 }
200 
201 /**
202  * \brief Implementation of sol.menu.start().
203  * \param l The Lua context that is calling this function.
204  * \return Number of values to return to Lua.
205  */
menu_api_start(lua_State * l)206 int LuaContext::menu_api_start(lua_State *l) {
207 
208   return state_boundary_handle(l, [&] {
209     // Parameters: context table.
210     if (lua_type(l, 1) != LUA_TTABLE
211         && lua_type(l, 1) != LUA_TUSERDATA) {
212       LuaTools::type_error(l, 1, "table or userdata");
213     }
214     LuaTools::check_type(l, 2, LUA_TTABLE);
215     bool on_top = LuaTools::opt_boolean(l, 3, true);
216     lua_settop(l, 2);
217 
218     LuaContext& lua_context = get();
219     const ScopedLuaRef& menu_ref = lua_context.create_ref();
220     lua_context.add_menu(menu_ref, 1, on_top);
221 
222     return 0;
223   });
224 }
225 
226 /**
227  * \brief Implementation of sol.menu.stop().
228  * \param l the Lua context that is calling this function
229  * \return number of values to return to Lua
230  */
menu_api_stop(lua_State * l)231 int LuaContext::menu_api_stop(lua_State* l) {
232 
233   return state_boundary_handle(l, [&] {
234     //LuaContext& lua_context = get();
235 
236     LuaTools::check_type(l, 1, LUA_TTABLE);
237     ScopedLuaRef ref = LuaTools::create_ref(l,1);
238     run_on_main([ref](lua_State*){
239       LuaContext& lua_context = get();
240       std::list<LuaMenuData>& menus = lua_context.menus;
241       for (LuaMenuData& menu: menus) {
242         if (menu.ref == ref) {
243           ScopedLuaRef menu_ref = menu.ref;  // Don't erase it immediately since we may be iterating over menus.
244           menu.ref.clear();
245           menu.context.clear();
246           lua_context.menu_on_finished(menu_ref);
247           break;
248         }
249       }
250     });
251     return 0;
252   });
253 }
254 
255 /**
256  * \brief Implementation of sol.menu.stop_all().
257  * \param l the Lua context that is calling this function
258  * \return number of values to return to Lua
259  */
menu_api_stop_all(lua_State * l)260 int LuaContext::menu_api_stop_all(lua_State* l) {
261 
262   return state_boundary_handle(l, [&] {
263     if (lua_type(l, 1) != LUA_TTABLE
264         && lua_type(l, 1) != LUA_TUSERDATA) {
265       LuaTools::type_error(l, 1, "table, game or map");
266     }
267 
268     get().remove_menus(1);
269 
270     return 0;
271   });
272 }
273 
274 /**
275  * \brief Implementation of sol.menu.is_started().
276  * \param l the Lua context that is calling this function
277  * \return number of values to return to Lua
278  */
menu_api_is_started(lua_State * l)279 int LuaContext::menu_api_is_started(lua_State* l) {
280 
281   return state_boundary_handle(l, [&] {
282     LuaContext& lua_context = get();
283 
284     LuaTools::check_type(l, 1, LUA_TTABLE);
285 
286     bool found = false;
287     std::list<LuaMenuData>& menus = lua_context.menus;
288     for (LuaMenuData& menu: menus) {
289       push_ref(l, menu.ref);
290       found = lua_equal(l, 1, -1);
291       lua_pop(l, 1);
292 
293       if (found) {
294         break;
295       }
296     }
297 
298     lua_pushboolean(l, found);
299 
300     return 1;
301   });
302 }
303 
304 /**
305  * \brief Implementation of sol.menu.bring_to_front().
306  * \param l The Lua context that is calling this function.
307  * \return Number of values to return to Lua.
308  */
menu_api_bring_to_front(lua_State * l)309 int LuaContext::menu_api_bring_to_front(lua_State* l) {
310 
311   return state_boundary_handle(l, [&] {
312     LuaContext& lua_context = get();
313 
314     LuaTools::check_type(l, 1, LUA_TTABLE);
315 
316     std::list<LuaMenuData>& menus = lua_context.menus;
317     for (auto it = menus.begin();
318          it != menus.end();
319          ++it
320     ) {
321       LuaMenuData menu = *it;
322       push_ref(l, menu.ref);
323       if (lua_equal(l, 1, -1)) {
324         menus.erase(it);
325         menus.push_back(menu);
326         lua_pop(l, 1);
327         break;
328       }
329       lua_pop(l, 1);
330     }
331 
332     return 0;
333   });
334 }
335 
336 /**
337  * \brief Implementation of sol.menu.bring_to_back().
338  * \param l The Lua context that is calling this function.
339  * \return Number of values to return to Lua.
340  */
menu_api_bring_to_back(lua_State * l)341 int LuaContext::menu_api_bring_to_back(lua_State* l) {
342 
343   return state_boundary_handle(l, [&] {
344     LuaContext& lua_context = get();
345 
346     LuaTools::check_type(l, 1, LUA_TTABLE);
347 
348     std::list<LuaMenuData>& menus = lua_context.menus;
349     for (auto it = menus.begin();
350          it != menus.end();
351          ++it
352     ) {
353       LuaMenuData menu = *it;
354       push_ref(l, menu.ref);
355       if (lua_equal(l, 1, -1)) {
356         menus.erase(it);
357         menus.push_front(menu);
358         lua_pop(l, 1);
359         break;
360       }
361       lua_pop(l, 1);
362     }
363 
364     return 0;
365   });
366 }
367 
368 /**
369  * \brief Calls the on_started() method of a Lua menu.
370  * \param menu_ref A reference to the menu object.
371  */
menu_on_started(const ScopedLuaRef & menu_ref)372 void LuaContext::menu_on_started(const ScopedLuaRef& menu_ref) {
373   check_callback_thread();
374   push_ref(current_l, menu_ref);
375   on_started();
376   lua_pop(current_l, 1);
377 }
378 
379 /**
380  * \brief Calls the on_finished() method of a Lua menu.
381  * \param menu_ref A reference to the menu object.
382  */
menu_on_finished(const ScopedLuaRef & menu_ref)383 void LuaContext::menu_on_finished(const ScopedLuaRef& menu_ref) {
384   check_callback_thread(); //TODO verify if needed
385   push_ref(current_l, menu_ref);
386   remove_menus(-1);  // First, stop children menus if any.
387   on_finished();
388   remove_timers(-1);  // Stop timers associated to this menu.
389   lua_pop(current_l, 1);
390 }
391 
392 /**
393  * \brief Calls the on_update() method of a Lua menu.
394  * \param menu_ref A reference to the menu object.
395  */
menu_on_update(const ScopedLuaRef & menu_ref)396 void LuaContext::menu_on_update(const ScopedLuaRef& menu_ref) {
397   check_callback_thread();
398   push_ref(current_l, menu_ref);
399   on_update();
400   menus_on_update(-1);  // Update children menus if any.
401   lua_pop(current_l, 1);
402 }
403 
404 /**
405  * \brief Calls the on_draw() method of a Lua menu.
406  * \param menu_ref A reference to the menu object.
407  * \param dst_surface The destination surface.
408  */
menu_on_draw(const ScopedLuaRef & menu_ref,const SurfacePtr & dst_surface)409 void LuaContext::menu_on_draw(
410     const ScopedLuaRef& menu_ref,
411     const SurfacePtr& dst_surface
412 ) {
413   push_ref(current_l, menu_ref);
414   on_draw(dst_surface);
415   menus_on_draw(-1, dst_surface);  // Draw children menus if any.
416   lua_pop(current_l, 1);
417 }
418 
419 /**
420  * \brief Calls an input callback method of a Lua menu.
421  * \param menu_ref A reference to the menu object.
422  * \param event The input event to forward.
423  * \return \c true if the event was handled and should stop being propagated.
424  */
menu_on_input(const ScopedLuaRef & menu_ref,const InputEvent & event)425 bool LuaContext::menu_on_input(
426     const ScopedLuaRef& menu_ref,
427     const InputEvent& event
428 ) {
429   // Get the Lua menu.
430   push_ref(current_l, menu_ref);
431 
432   // Send the event to children menus first.
433   bool handled = menus_on_input(-1, event);
434 
435   if (!handled) {
436     // Sent the event to this menu.
437     handled = on_input(event);
438   }
439 
440   // Remove the menu from the stack.
441   lua_pop(current_l, 1);
442 
443   return handled;
444 }
445 
446 /**
447  * \brief Calls the on_command_pressed() method of a Lua menu.
448  * \param menu_ref A reference to the menu object.
449  * \param command The game command just pressed.
450  * \return \c true if the event was handled and should stop being propagated.
451  */
menu_on_command_pressed(const ScopedLuaRef & menu_ref,GameCommand command)452 bool LuaContext::menu_on_command_pressed(
453     const ScopedLuaRef& menu_ref,
454     GameCommand command
455 ) {
456   push_ref(current_l, menu_ref);
457 
458   // Send the event to children menus first.
459   bool handled = menus_on_command_pressed(-1, command);
460 
461   if (!handled) {
462     // Sent the event to this menu.
463     handled = on_command_pressed(command);
464   }
465 
466   lua_pop(current_l, 1);
467 
468   return handled;
469 }
470 
471 /**
472  * \brief Calls the on_command_released() method of a Lua menu.
473  * \param menu_ref A reference to the menu object.
474  * \param command The game command just released.
475  * \return \c true if the event was handled and should stop being propagated.
476  */
menu_on_command_released(const ScopedLuaRef & menu_ref,GameCommand command)477 bool LuaContext::menu_on_command_released(
478     const ScopedLuaRef& menu_ref,
479     GameCommand command
480 ) {
481   push_ref(current_l, menu_ref);
482 
483   // Send the event to children menus first.
484   bool handled = menus_on_command_released(-1, command);
485 
486   if (!handled) {
487     // Sent the event to this menu.
488     handled = on_command_released(command);
489   }
490 
491   lua_pop(current_l, 1);
492 
493   return handled;
494 }
495 
496 /**
497  * \brief Calls the on_update() method of the menus associated to a context.
498  * \param context_index Index of an object with menus.
499  */
menus_on_update(int context_index)500 void LuaContext::menus_on_update(int context_index) {
501   for (LuaMenuData& menu: menus) {
502     if (menu.context.equals(current_l,context_index)) {
503       menu_on_update(menu.ref);
504     }
505   }
506 }
507 
508 /**
509  * \brief Calls the on_draw() method of the menus associated to a context.
510  * \param context_index Index of an object with menus.
511  * \param dst_surface The destination surface to draw.
512  */
menus_on_draw(int context_index,const SurfacePtr & dst_surface)513 void LuaContext::menus_on_draw(int context_index, const SurfacePtr& dst_surface) {
514   for (LuaMenuData& menu: menus) {
515     if (menu.context.equals(current_l,context_index)) {
516       menu_on_draw(menu.ref, dst_surface);
517     }
518   }
519 }
520 
521 /**
522  * \brief Calls the on_input() method of the menus associated to a context.
523  * \param context_index Index of an object with menus.
524  * \param event The input event to handle.
525  * \return \c true if the event was handled and should stop being propagated.
526  */
menus_on_input(int context_index,const InputEvent & event)527 bool LuaContext::menus_on_input(int context_index, const InputEvent& event) {
528   bool handled = false;
529   std::list<LuaMenuData>::reverse_iterator it;
530   for (it = menus.rbegin(); it != menus.rend() && !handled; ++it) {
531     const ScopedLuaRef& menu_ref = it->ref;
532     if (it->context.equals(current_l,context_index)) {
533       handled = menu_on_input(menu_ref, event);
534     }
535   }
536 
537   return handled;
538 }
539 
540 /**
541  * \brief Calls the on_command_pressed() method of the menus associated to a context.
542  * \param context_index Index of an object with menus.
543  * \param command The game command just pressed.
544  * \return \c true if the event was handled and should stop being propagated.
545  */
menus_on_command_pressed(int context_index,GameCommand command)546 bool LuaContext::menus_on_command_pressed(int context_index,
547     GameCommand command) {
548 
549   bool handled = false;
550   std::list<LuaMenuData>::reverse_iterator it;
551   for (it = menus.rbegin(); it != menus.rend() && !handled; ++it) {
552     const ScopedLuaRef& menu_ref = it->ref;
553     if (it->context.equals(current_l,context_index)) {
554       handled = menu_on_command_pressed(menu_ref, command);
555     }
556   }
557 
558   return handled;
559 }
560 
561 /**
562  * \brief Calls the on_command_released() method of the menus associated to a context.
563  * \param context_index Index of an object with menus.
564  * \param command The game command just released.
565  * \return \c true if the event was handled and should stop being propagated.
566  */
menus_on_command_released(int context_index,GameCommand command)567 bool LuaContext::menus_on_command_released(int context_index,
568     GameCommand command) {
569 
570   bool handled = false;
571   std::list<LuaMenuData>::reverse_iterator it;
572   for (it = menus.rbegin(); it != menus.rend() && !handled; ++it) {
573     const ScopedLuaRef& menu_ref = it->ref;
574     if (it->context.equals(current_l,context_index)) {
575       handled = menu_on_command_released(menu_ref, command);
576     }
577   }
578 
579   return handled;
580 }
581 
582 }
583 
584