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