From 65e7617ab645b1680a31fbc6d8f231ca7408f05e Mon Sep 17 00:00:00 2001 From: Franklin Wei Date: Mon, 30 Oct 2017 21:25:01 -0400 Subject: [PATCH] puzzles: add an interaction mode to the "Zoom In" feature This makes it possible to play the game while zoomed in. Read the manual entry if you want to know more. Change-Id: Iff8bab12f92ebd2798047c25d1fde7740aa543ce --- apps/plugins/puzzles/rockbox.c | 635 +++++++++++++++++++-------------- manual/plugins/sgt-puzzles.tex | 32 ++ 2 files changed, 390 insertions(+), 277 deletions(-) diff --git a/apps/plugins/puzzles/rockbox.c b/apps/plugins/puzzles/rockbox.c index b79070cba8..529c4ae5b4 100644 --- a/apps/plugins/puzzles/rockbox.c +++ b/apps/plugins/puzzles/rockbox.c @@ -82,9 +82,10 @@ static int help_times = 0; #endif static void fix_size(void); +static int pause_menu(void); static struct viewport clip_rect; -static bool clipped = false, zoom_enabled = false; +static bool clipped = false, zoom_enabled = false, view_mode = true; extern bool audiobuf_available; @@ -1269,33 +1270,47 @@ static void rb_status_bar(void *handle, const char *text) LOGF("game title is %s\n", text); } +static int get_titleheight(void) +{ + return rb->font_get(FONT_UI)->height; +} + static void draw_title(void) { - const char *str = NULL; + const char *base; if(titlebar) - str = titlebar; + base = titlebar; else - str = midend_which_game(me)->name; + base = midend_which_game(me)->name; + + char str[128]; + rb->snprintf(str, sizeof(str), "%s%s", base, zoom_enabled ? (view_mode ? " (viewing)" : " (interaction)") : ""); /* quick hack */ - bool orig_clipped = clipped; - if(orig_clipped) - rb_unclip(NULL); + bool orig_clipped; + if(!zoom_enabled) + { + orig_clipped = clipped; + if(orig_clipped) + rb_unclip(NULL); + } - int h; + int w, h; cur_font = FONT_UI; rb->lcd_setfont(cur_font); - rb->lcd_getstringsize(str, NULL, &h); + rb->lcd_getstringsize(str, &w, &h); rb->lcd_set_foreground(BG_COLOR); - rb->lcd_fillrect(0, LCD_HEIGHT - h, LCD_WIDTH, h); + rb->lcd_fillrect(0, LCD_HEIGHT - h, w, h); rb->lcd_set_foreground(LCD_BLACK); rb->lcd_putsxy(0, LCD_HEIGHT - h, str); - rb->lcd_update_rect(0, LCD_HEIGHT - h, LCD_WIDTH, h); - if(orig_clipped) - rb_clip(NULL, clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height); + if(!zoom_enabled) + { + if(orig_clipped) + rb_clip(NULL, clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height); + } } static char *rb_text_fallback(void *handle, const char *const *strings, @@ -1326,6 +1341,236 @@ const drawing_api rb_drawing = { NULL, }; +static bool want_redraw = true; +static bool accept_input = true; + +/* set do_pausemenu to false to just return -1 on BTN_PAUSE and do + * nothing else. */ +static int process_input(int tmo, bool do_pausemenu) +{ + LOGF("process_input start"); + LOGF("------------------"); + int state = 0; + +#ifdef HAVE_ADJUSTABLE_CPU_FREQ + rb->cpu_boost(false); /* about to block for button input */ +#endif + + int button = rb->button_get_w_tmo(tmo); + + /* weird stuff */ + exit_on_usb(button); + + /* these games require a second input on long-press */ + if(accept_input && (button == (BTN_FIRE | BUTTON_REPEAT)) && + (strcmp("Mines", midend_which_game(me)->name) != 0 || + strcmp("Magnets", midend_which_game(me)->name) != 0)) + { + accept_input = false; + return ' '; + } + + button = rb->button_status(); + +#ifdef HAVE_ADJUSTABLE_CPU_FREQ + rb->cpu_boost(true); +#endif + + if(button == BTN_PAUSE) + { + if(do_pausemenu) + { + want_redraw = false; + /* quick hack to preserve the clipping state */ + bool orig_clipped = clipped; + if(orig_clipped) + rb_unclip(NULL); + + int rc = pause_menu(); + + if(orig_clipped) + rb_clip(NULL, clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height); + + last_keystate = 0; + accept_input = true; + + return rc; + } + else + return -1; + } + + /* these games require, for one reason or another, that events + * fire upon buttons being released rather than when they are + * pressed */ + if(strcmp("Inertia", midend_which_game(me)->name) == 0 || + strcmp("Mines", midend_which_game(me)->name) == 0 || + strcmp("Magnets", midend_which_game(me)->name) == 0 || + strcmp("Map", midend_which_game(me)->name) == 0) + { + LOGF("received button 0x%08x", button); + + unsigned released = ~button & last_keystate; + + last_keystate = button; + + if(!button) + { + if(!accept_input) + { + LOGF("ignoring, all keys released but not accepting input before, can accept input later"); + accept_input = true; + return 0; + } + } + + if(!released || !accept_input) + { + LOGF("released keys detected: 0x%08x", released); + LOGF("ignoring, either no keys released or not accepting input"); + return 0; + } + + if(button) + { + LOGF("ignoring input from now until all released"); + accept_input = false; + } + + button |= released; + LOGF("accepting event 0x%08x", button); + } + /* default is to ignore repeats except for untangle */ + else if(strcmp("Untangle", midend_which_game(me)->name) != 0) + { + /* start accepting input again after a release */ + if(!button) + { + accept_input = true; + return 0; + } + /* ignore repeats */ + /* Untangle gets special treatment */ + if(!accept_input) + return 0; + accept_input = false; + } + + switch(button) + { + case BTN_UP: + state = CURSOR_UP; + break; + case BTN_DOWN: + state = CURSOR_DOWN; + break; + case BTN_LEFT: + state = CURSOR_LEFT; + break; + case BTN_RIGHT: + state = CURSOR_RIGHT; + break; + + /* handle diagonals (mainly for Inertia) */ + case BTN_DOWN | BTN_LEFT: +#ifdef BTN_DOWN_LEFT + case BTN_DOWN_LEFT: +#endif + state = '1' | MOD_NUM_KEYPAD; + break; + case BTN_DOWN | BTN_RIGHT: +#ifdef BTN_DOWN_RIGHT + case BTN_DOWN_RIGHT: +#endif + state = '3' | MOD_NUM_KEYPAD; + break; + case BTN_UP | BTN_LEFT: +#ifdef BTN_UP_LEFT + case BTN_UP_LEFT: +#endif + state = '7' | MOD_NUM_KEYPAD; + break; + case BTN_UP | BTN_RIGHT: +#ifdef BTN_UP_RIGHT + case BTN_UP_RIGHT: +#endif + state = '9' | MOD_NUM_KEYPAD; + break; + + case BTN_FIRE: + if(!strcmp("Fifteen", midend_which_game(me)->name)) + state = 'h'; /* hint */ + else + state = CURSOR_SELECT; + break; + + default: + break; + } + + if(settings.shortcuts) + { + static bool shortcuts_ok = true; + switch(button) + { + case BTN_LEFT | BTN_FIRE: + if(shortcuts_ok) + midend_process_key(me, 0, 0, 'u'); + shortcuts_ok = false; + break; + case BTN_RIGHT | BTN_FIRE: + if(shortcuts_ok) + midend_process_key(me, 0, 0, 'r'); + shortcuts_ok = false; + break; + case 0: + shortcuts_ok = true; + break; + default: + break; + } + } + + LOGF("process_input done"); + LOGF("------------------"); + return state; +} + +static long last_tstamp; + +static void timer_cb(void) +{ +#if LCD_DEPTH != 24 + if(settings.timerflash) + { + static bool what = false; + what = !what; + if(what) + rb->lcd_framebuffer[0] = LCD_BLACK; + else + rb->lcd_framebuffer[0] = LCD_WHITE; + rb->lcd_update(); + } +#endif + + LOGF("timer callback"); + midend_timer(me, ((float)(*rb->current_tick - last_tstamp) / (float)HZ) / settings.slowmo_factor); + last_tstamp = *rb->current_tick; +} + +static volatile bool timer_on = false; + +void activate_timer(frontend *fe) +{ + last_tstamp = *rb->current_tick; + timer_on = true; +} + +void deactivate_timer(frontend *fe) +{ + timer_on = false; +} + /* render to a virtual framebuffer and let the user pan (but not make any moves) */ static void zoom(void) { @@ -1356,54 +1601,106 @@ static void zoom(void) zoom_enabled = true; - /* draws go to the enlarged framebuffer */ + /* draws go to the zoom framebuffer */ midend_force_redraw(me); int x = 0, y = 0; rb->lcd_bitmap_part(zoom_fb, x, y, STRIDE(SCREEN_MAIN, zoom_w, zoom_h), 0, 0, LCD_WIDTH, LCD_HEIGHT); + draw_title(); rb->lcd_update(); + /* Here's how this works: pressing select (or the target's + * equivalent, it's whatever BTN_FIRE is) while in viewing mode + * will toggle the mode to interaction mode. In interaction mode, + * the buttons will behave as normal and be sent to the puzzle, + * except for the pause/quit (BTN_PAUSE) button, which will return + * to view mode. Finally, when in view mode, pause/quit will + * return to the pause menu. */ + + view_mode = true; + /* pan around the image */ while(1) { - int button = rb->button_get(true); - switch(button) + if(view_mode) { - case BTN_UP: - y -= PAN_Y; /* clamped later */ - break; - case BTN_DOWN: - y += PAN_Y; /* clamped later */ - break; - case BTN_LEFT: - x -= PAN_X; /* clamped later */ - break; - case BTN_RIGHT: - x += PAN_X; /* clamped later */ - break; - case BTN_PAUSE: - zoom_enabled = false; - sfree(zoom_fb); - fix_size(); - return; - default: - break; + int button = rb->button_get_w_tmo(timer_on ? TIMER_INTERVAL : -1); + switch(button) + { + case BTN_UP: + y -= PAN_Y; /* clamped later */ + break; + case BTN_DOWN: + y += PAN_Y; /* clamped later */ + break; + case BTN_LEFT: + x -= PAN_X; /* clamped later */ + break; + case BTN_RIGHT: + x += PAN_X; /* clamped later */ + break; + case BTN_PAUSE: + zoom_enabled = false; + sfree(zoom_fb); + fix_size(); + return; + case BTN_FIRE: + view_mode = false; + continue; + default: + break; + } + + if(y < 0) + y = 0; + if(x < 0) + x = 0; + + if(y + LCD_HEIGHT >= zoom_h) + y = zoom_h - LCD_HEIGHT; + if(x + LCD_WIDTH >= zoom_w) + x = zoom_w - LCD_WIDTH; + + if(timer_on) + timer_cb(); + + /* goes to zoom_fb */ + midend_redraw(me); + + rb->lcd_bitmap_part(zoom_fb, x, y, STRIDE(SCREEN_MAIN, zoom_w, zoom_h), + 0, 0, LCD_WIDTH, LCD_HEIGHT); + draw_title(); + rb->lcd_update(); + rb->yield(); } + else + { + /* basically a copy-pasta'd main loop */ + int button = process_input(timer_on ? TIMER_INTERVAL : -1, false); - if(y < 0) - y = 0; - if(x < 0) - x = 0; - if(y + LCD_HEIGHT >= zoom_h) - y = zoom_h - LCD_HEIGHT; - if(x + LCD_WIDTH >= zoom_w) - x = zoom_w - LCD_WIDTH; + if(button < 0) + { + view_mode = true; + continue; + } - rb->lcd_bitmap_part(zoom_fb, x, y, STRIDE(SCREEN_MAIN, zoom_w, zoom_h), - 0, 0, LCD_WIDTH, LCD_HEIGHT); - rb->lcd_update(); + if(button) + midend_process_key(me, 0, 0, button); + + if(timer_on) + timer_cb(); + + if(want_redraw) + midend_redraw(me); + + rb->lcd_bitmap_part(zoom_fb, x, y, STRIDE(SCREEN_MAIN, zoom_w, zoom_h), + 0, 0, LCD_WIDTH, LCD_HEIGHT); + draw_title(); + rb->lcd_update(); + rb->yield(); + } } } @@ -2163,229 +2460,6 @@ static int pause_menu(void) return 0; } -static bool want_redraw = true; -static bool accept_input = true; - -static int process_input(int tmo) -{ - LOGF("process_input start"); - LOGF("------------------"); - int state = 0; - -#ifdef HAVE_ADJUSTABLE_CPU_FREQ - rb->cpu_boost(false); /* about to block for button input */ -#endif - - int button = rb->button_get_w_tmo(tmo); - - /* weird stuff */ - exit_on_usb(button); - - /* these games require a second input on long-press */ - if(accept_input && (button == (BTN_FIRE | BUTTON_REPEAT)) && - (strcmp("Mines", midend_which_game(me)->name) != 0 || - strcmp("Magnets", midend_which_game(me)->name) != 0)) - { - accept_input = false; - return ' '; - } - - button = rb->button_status(); - -#ifdef HAVE_ADJUSTABLE_CPU_FREQ - rb->cpu_boost(true); -#endif - - if(button == BTN_PAUSE) - { - want_redraw = false; - /* quick hack to preserve the clipping state */ - bool orig_clipped = clipped; - if(orig_clipped) - rb_unclip(NULL); - - int rc = pause_menu(); - - if(orig_clipped) - rb_clip(NULL, clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height); - - last_keystate = 0; - accept_input = true; - - return rc; - } - - /* these games require, for one reason or another, that events - * fire upon buttons being released rather than when they are - * pressed */ - if(strcmp("Inertia", midend_which_game(me)->name) == 0 || - strcmp("Mines", midend_which_game(me)->name) == 0 || - strcmp("Magnets", midend_which_game(me)->name) == 0 || - strcmp("Map", midend_which_game(me)->name) == 0) - { - LOGF("received button 0x%08x", button); - - unsigned released = ~button & last_keystate; - - last_keystate = button; - - if(!button) - { - if(!accept_input) - { - LOGF("ignoring, all keys released but not accepting input before, can accept input later"); - accept_input = true; - return 0; - } - } - - if(!released || !accept_input) - { - LOGF("released keys detected: 0x%08x", released); - LOGF("ignoring, either no keys released or not accepting input"); - return 0; - } - - if(button) - { - LOGF("ignoring input from now until all released"); - accept_input = false; - } - - button |= released; - LOGF("accepting event 0x%08x", button); - } - /* default is to ignore repeats except for untangle */ - else if(strcmp("Untangle", midend_which_game(me)->name) != 0) - { - /* start accepting input again after a release */ - if(!button) - { - accept_input = true; - return 0; - } - /* ignore repeats */ - /* Untangle gets special treatment */ - if(!accept_input) - return 0; - accept_input = false; - } - - switch(button) - { - case BTN_UP: - state = CURSOR_UP; - break; - case BTN_DOWN: - state = CURSOR_DOWN; - break; - case BTN_LEFT: - state = CURSOR_LEFT; - break; - case BTN_RIGHT: - state = CURSOR_RIGHT; - break; - - /* handle diagonals (mainly for Inertia) */ - case BTN_DOWN | BTN_LEFT: -#ifdef BTN_DOWN_LEFT - case BTN_DOWN_LEFT: -#endif - state = '1' | MOD_NUM_KEYPAD; - break; - case BTN_DOWN | BTN_RIGHT: -#ifdef BTN_DOWN_RIGHT - case BTN_DOWN_RIGHT: -#endif - state = '3' | MOD_NUM_KEYPAD; - break; - case BTN_UP | BTN_LEFT: -#ifdef BTN_UP_LEFT - case BTN_UP_LEFT: -#endif - state = '7' | MOD_NUM_KEYPAD; - break; - case BTN_UP | BTN_RIGHT: -#ifdef BTN_UP_RIGHT - case BTN_UP_RIGHT: -#endif - state = '9' | MOD_NUM_KEYPAD; - break; - - case BTN_FIRE: - if(!strcmp("Fifteen", midend_which_game(me)->name)) - state = 'h'; /* hint */ - else - state = CURSOR_SELECT; - break; - - default: - break; - } - - if(settings.shortcuts) - { - static bool shortcuts_ok = true; - switch(button) - { - case BTN_LEFT | BTN_FIRE: - if(shortcuts_ok) - midend_process_key(me, 0, 0, 'u'); - shortcuts_ok = false; - break; - case BTN_RIGHT | BTN_FIRE: - if(shortcuts_ok) - midend_process_key(me, 0, 0, 'r'); - shortcuts_ok = false; - break; - case 0: - shortcuts_ok = true; - break; - default: - break; - } - } - - LOGF("process_input done"); - LOGF("------------------"); - return state; -} - -static long last_tstamp; - -static void timer_cb(void) -{ -#if LCD_DEPTH != 24 - if(settings.timerflash) - { - static bool what = false; - what = !what; - if(what) - rb->lcd_framebuffer[0] = LCD_BLACK; - else - rb->lcd_framebuffer[0] = LCD_WHITE; - rb->lcd_update(); - } -#endif - - LOGF("timer callback"); - midend_timer(me, ((float)(*rb->current_tick - last_tstamp) / (float)HZ) / settings.slowmo_factor); - last_tstamp = *rb->current_tick; -} - -static volatile bool timer_on = false; - -void activate_timer(frontend *fe) -{ - last_tstamp = *rb->current_tick; - timer_on = true; -} - -void deactivate_timer(frontend *fe) -{ - timer_on = false; -} - /* points to pluginbuf */ char *giant_buffer = NULL; static size_t giant_buffer_len = 0; /* set on start */ @@ -2795,6 +2869,7 @@ enum plugin_status plugin_start(const void *param) bool quit = false; int sel = 0; + while(!quit) { switch(rb->do_menu(&menu, &sel, NULL, false)) @@ -2866,9 +2941,11 @@ enum plugin_status plugin_start(const void *param) { want_redraw = true; + int theight = get_titleheight(); draw_title(); + rb->lcd_update_rect(0, LCD_HEIGHT - theight, LCD_WIDTH, theight); - int button = process_input(timer_on ? TIMER_INTERVAL : -1); + int button = process_input(timer_on ? TIMER_INTERVAL : -1, true); if(button < 0) { @@ -2881,35 +2958,39 @@ enum plugin_status plugin_start(const void *param) titlebar = NULL; } - if(button == -1) + switch(button) { + case -1: /* new game */ midend_free(me); break; - } - else if(button == -2) - { + case -2: /* quit without saving */ midend_free(me); sfree(colors); exit(PLUGIN_OK); - } - else if(button == -3) - { + case -3: /* save and quit */ save_game(); midend_free(me); sfree(colors); exit(PLUGIN_OK); + default: + break; } } if(button) midend_process_key(me, 0, 0, button); + draw_title(); /* will draw to fb */ + if(want_redraw) midend_redraw(me); + /* push title to screen as well */ + rb->lcd_update_rect(0, LCD_HEIGHT - theight, LCD_WIDTH, theight); + if(timer_on) timer_cb(); diff --git a/manual/plugins/sgt-puzzles.tex b/manual/plugins/sgt-puzzles.tex index 5001a39aaf..0de99a1238 100644 --- a/manual/plugins/sgt-puzzles.tex +++ b/manual/plugins/sgt-puzzles.tex @@ -6,6 +6,10 @@ The games that begin with the ``sgt-'' prefix are ports of certain puzzles from Simon Tatham's Portable Puzzle Collection, an open source collection of single-player puzzle games. +\note{Certain puzzles may crash when run with demanding + configurations. To prevent this, avoid setting extreme configuration + values.} + \subsubsection{Puzzle Documentation} For documentation on the games included, please see the ``Extensive Help'' menu option from inside the plugin to read puzzle-specific @@ -27,3 +31,31 @@ whenever needed. \note{On hard disk-based devices, this may cause a slight delay as the disk spins up to load the fonts when a puzzle is first started, and after using the ``Extensive Help'' feature.} + +\subsubsection{``Zoom In'' Feature} +The ``Zoom In'' feature is available as an option from the pause +menu. It has two modes: viewing mode, and interaction mode. The +current mode is indicated in the title bar at the bottom of the +screen. This feature is most useful with low-resolution devices and +large puzzles. + +Viewing mode is entered when the ``Zoom In'' option is selected, or +when {\PluginCancel} is pressed in interaction mode. It allows you to +pan around an enlarged version of the game. The directional keys pan +the image by a small amount in their respective directions, and +{\PluginSelect} should toggle interaction mode. To return to the pause +menu from viewing mode, press {\PluginCancel}. + +In interaction mode, activated from viewer mode by pressing +{\PluginSelect}, your device's buttons all function as they do in the +normal gameplay mode, with the exception of {\PluginCancel}, which +returns the game to viewing mode, whereas in the normal gameplay mode +it would return directly to the pause menu. To return to the pause +menu from interaction mode, press {\PluginCancel} twice. + +\note{Using certain features such as the ``Zoom In'' option may stop + audio playback. This is normal, as the game requires additional + memory from the system, which will automatically stop playback. The + ``Playback Control'' menu will be hidden whenever this + happens. Exiting the game will allow the resumption of audio + playback.}