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.}