/*************************************************************************** * __________ __ ___. * Open \______ \ ____ ____ | | _\_ |__ _______ ___ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ * $Id$ * * Copyright (C) 2017 Franklin Wei * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY * KIND, either express or implied. * ***************************************************************************/ /* rockbox frontend for puzzles */ /* This file contains the majority of the rockbox-specific code for * the sgt-puzzles port. It implements a set of functions for the * backend to call to actually run the games, as well as rockbox UI * code (menus, input, etc). For a good overview of the rest of the * puzzles code, see: * * . */ #include "plugin.h" #include "help.h" #include "lz4tiny.h" #include "src/puzzles.h" #include "lib/keymaps.h" #include "lib/playback_control.h" #include "lib/simple_viewer.h" #include "lib/xlcd.h" #include "fixedpoint.h" /* how many ticks between timer callbacks */ #define TIMER_INTERVAL (HZ / 50) /* no c200v2 */ #if PLUGIN_BUFFER_SIZE > 0x14000 #define DEBUG_MENU #define FONT_CACHING #endif /* background color (mimicking from the JS frontend) */ #define BG_R .9f /* very light gray */ #define BG_G .9f #define BG_B .9f #define BG_COLOR LCD_RGBPACK((int)(255*BG_R), (int)(255*BG_G), (int)(255*BG_B)) #define ERROR_COLOR LCD_RGBPACK(255, 0, 0) #define MAX_FONTS (MAXUSERFONTS - 2) #define FONT_TABLE PLUGIN_GAMES_DATA_DIR "/.sgt-puzzles.fnttab" /* font bundle size range */ #define BUNDLE_MIN 7 #define BUNDLE_MAX 36 #define BUNDLE_COUNT (BUNDLE_MAX - BUNDLE_MIN + 1) /* max length of C_STRING config vals */ #define MAX_STRLEN 128 /* try to increment a numeric config value up to this much */ #define CHOOSER_MAX_INCR 2 /* max font table line */ #define MAX_LINE 128 /* Sorry. */ #define MURICA #ifdef MURICA #define midend_serialize midend_serialise #define midend_deserialize midend_deserialise #define frontend_default_color frontend_default_colour #define midend_colors midend_colours #endif /* zoom stuff */ #define ZOOM_FACTOR 3 #define PAN_X (MIN(LCD_HEIGHT, LCD_WIDTH) / 4) #define PAN_Y (MIN(LCD_HEIGHT, LCD_WIDTH) / 4) /* utility macros */ #undef ABS #define ABS(a) ((a)<0?-(a):(a)) #define SWAP(a, b, t) do { t = a; a = b; b = t; } while(0); #define fp_fpart(f, bits) ((f) & ((1 << (bits)) - 1)) #define fp_rfpart(f, bits) ((1 << (bits)) - fp_fpart(f, bits)) #define FRACBITS 16 static void fix_size(void); static int pause_menu(void); static inline void plot(fb_data *fb, int w, int h, unsigned x, unsigned y, unsigned long a, unsigned long r1, unsigned long g1, unsigned long b1, unsigned cl, unsigned cr, unsigned cu, unsigned cd); static midend *me = NULL; static unsigned *colors = NULL; static int ncolors = 0; static long last_keystate = 0; #if defined(FOR_REAL) && defined(DEBUG_MENU) /* the "debug menu" is hidden by default in order to avoid the * naturally ensuing complaints from users */ static bool debug_mode = false; static int help_times = 0; #endif /* clipping stuff */ static struct viewport clip_rect; static bool clipped = false, zoom_enabled = false, view_mode = true; extern bool audiobuf_available; /* defined in rbmalloc.c */ static fb_data *zoom_fb; /* dynamically allocated */ static int zoom_w, zoom_h, zoom_clipu, zoom_clipd, zoom_clipl, zoom_clipr; static int cur_font = FONT_UI; static bool need_draw_update = false; static int ud_l = 0, ud_u = 0, ud_r = LCD_WIDTH, ud_d = LCD_HEIGHT; static char *titlebar = NULL; static bool want_redraw = true, accept_input = true; /* last timer call */ static long last_tstamp; static volatile bool timer_on = false; static bool load_success; /* debug settings */ /* did I mention there's a secret debug menu? */ static struct settings_t { int slowmo_factor; bool timerflash, clipoff, shortcuts, no_aa, polyanim; } settings; /* re-implementations of many rockbox primitives, adapted to draw into * a custom framebuffer. */ static void zoom_drawpixel(int x, int y) { if(y < zoom_clipu || y >= zoom_clipd) return; if(x < zoom_clipl || x >= zoom_clipr) return; #if LCD_DEPTH == 24 /* I hate these */ unsigned int pix = rb->lcd_get_foreground(); zoom_fb[y * zoom_w + x].b = RGB_UNPACK_BLUE(pix); zoom_fb[y * zoom_w + x].g = RGB_UNPACK_GREEN(pix); zoom_fb[y * zoom_w + x].r = RGB_UNPACK_RED(pix); #else zoom_fb[y * zoom_w + x] = rb->lcd_get_foreground(); #endif } static void zoom_hline(int l, int r, int y) { if(y < zoom_clipu || y >= zoom_clipd) return; if(l > r) { int t = l; l = r; r = t; } if(l < zoom_clipl) l = zoom_clipl; if(r >= zoom_clipr) r = zoom_clipr; #if LCD_DEPTH == 24 fb_data pixel = { RGB_UNPACK_BLUE(rb->lcd_get_foreground()), RGB_UNPACK_GREEN(rb->lcd_get_foreground()), RGB_UNPACK_RED(rb->lcd_get_foreground()) }; #else fb_data pixel = rb->lcd_get_foreground(); #endif fb_data *ptr = zoom_fb + y * zoom_w + l; for(int i = 0; i < r - l; ++i) *ptr++ = pixel; } static void zoom_fillcircle(int cx, int cy, int radius) { /* copied straight from xlcd_draw.c */ int d = 3 - (radius * 2); int x = 0; int y = radius; while(x <= y) { zoom_hline(cx - x, cx + x, cy + y); zoom_hline(cx - x, cx + x, cy - y); zoom_hline(cx - y, cx + y, cy + x); zoom_hline(cx - y, cx + y, cy - x); if(d < 0) { d += (x * 4) + 6; } else { d += ((x - y) * 4) + 10; --y; } ++x; } } static void zoom_drawcircle(int cx, int cy, int radius) { int d = 3 - (radius * 2); int x = 0; int y = radius; while(x <= y) { zoom_drawpixel(cx + x, cy + y); zoom_drawpixel(cx - x, cy + y); zoom_drawpixel(cx + x, cy - y); zoom_drawpixel(cx - x, cy - y); zoom_drawpixel(cx + y, cy + x); zoom_drawpixel(cx - y, cy + x); zoom_drawpixel(cx + y, cy - x); zoom_drawpixel(cx - y, cy - x); if(d < 0) { d += (x * 4) + 6; } else { d += ((x - y) * 4) + 10; --y; } ++x; } } /* This format is pretty crazy: each byte holds the states of 8 pixels * in a column, with bit 0 being the topmost pixel. Who needs cache * efficiency? */ static void zoom_mono_bitmap(const unsigned char *bits, int x, int y, int w, int h) { unsigned int pix = rb->lcd_get_foreground(); for(int i = 0; i < h / 8 + 1; ++i) { for(int j = 0; j < w; ++j) { unsigned char column = bits[i * w + j]; for(int dy = 0; dy < 8; ++dy) { if(column & 1) { #if LCD_DEPTH == 24 zoom_fb[(y + i * 8 + dy) * zoom_w + x + j].b = RGB_UNPACK_BLUE(pix); zoom_fb[(y + i * 8 + dy) * zoom_w + x + j].g = RGB_UNPACK_GREEN(pix); zoom_fb[(y + i * 8 + dy) * zoom_w + x + j].r = RGB_UNPACK_RED(pix); #else zoom_fb[(y + i * 8 + dy) * zoom_w + x + j] = pix; #endif } column >>= 1; } } } } /* Rockbox's alpha format is actually pretty sane: each byte holds * alpha values for two horizontally adjacent pixels. Low half is * leftmost pixel. See lcd-16bit-common.c for more info. */ static void zoom_alpha_bitmap(const unsigned char *bits, int x, int y, int w, int h) { const unsigned char *ptr = bits; unsigned char buf; int n_read = 0; /* how many 4-bit nibbles we've read (read new when even) */ unsigned int pix = rb->lcd_get_foreground(); unsigned int r = RGB_UNPACK_RED(pix), g = RGB_UNPACK_GREEN(pix), b = RGB_UNPACK_BLUE(pix); for(int i = 0; i < h; ++i) { for(int j = 0; j < w; ++j) { if(n_read % 2 == 0) { buf = *ptr++; } int pix_alpha = (n_read++ % 2) ? buf >> 4 : buf & 0xF; /* in reverse order */ pix_alpha = 15 - pix_alpha; /* correct order now, still 0-F */ int plot_alpha = (pix_alpha << 4) | pix_alpha; /* so 0xF -> 0xFF, 0x1 -> 0x11, etc. */ plot(zoom_fb, zoom_w, zoom_h, x + j, y + i, plot_alpha, r, g, b, 0, zoom_w, 0, zoom_h); } } } /* font management */ static struct bundled_font { int status; /* -3 = never tried loading, or unloaded, -2 = failed to load, >= -1: loaded successfully */ int last_use; } *loaded_fonts = NULL; /* monospace are first, then proportional */ static int n_fonts, access_counter = -1; /* called on exit and before entering help viewer (workaround for a possible bug in simple_viewer) */ static void unload_fonts(void) { for(int i = 0; i < 2 * BUNDLE_COUNT; ++i) if(loaded_fonts[i].status > 0) /* don't unload FONT_UI */ { rb->font_unload(loaded_fonts[i].status); loaded_fonts[i].status = -3; } access_counter = -1; cur_font = FONT_UI; rb->lcd_setfont(cur_font); } static void init_fonttab(void) { loaded_fonts = smalloc(2 * BUNDLE_COUNT * sizeof(struct bundled_font)); for(int i = 0; i < 2 * BUNDLE_COUNT; ++i) loaded_fonts[i].status = -3; access_counter = 0; n_fonts = 0; } static void font_path(char *buf, int type, int size) { if(size < 10) /* Deja Vu only goes down to 10px, below that it's a giant blob */ { if(size < 7) size = 7; /* we're not going to force anyone to read 05-Tiny :P */ /* we also don't care about monospace/proportional at this point */ switch(size) { case 7: rb->snprintf(buf, MAX_PATH, FONT_DIR "/07-Fixed.fnt"); break; case 8: rb->snprintf(buf, MAX_PATH, FONT_DIR "/08-Rockfont.fnt"); break; case 9: rb->snprintf(buf, MAX_PATH, FONT_DIR "/09-Fixed.fnt"); break; default: assert(false); } } else rb->snprintf(buf, MAX_PATH, FONT_DIR "/%02d-%s.fnt", size, type == FONT_FIXED ? "DejaVuSansMono" : "DejaVuSans"); } static void rb_setfont(int type, int size) { /* out of range (besides, no puzzle should ever need this large of a font, anyways) */ if(size > BUNDLE_MAX) size = BUNDLE_MAX; if(size < 10) { if(size < 7) /* no teeny-tiny fonts */ size = 7; /* assume monospace for these */ type = FONT_FIXED; } int font_idx = (type == FONT_FIXED ? 0 : BUNDLE_COUNT) + size - BUNDLE_MIN; switch(loaded_fonts[font_idx].status) { case -3: { /* never loaded */ char buf[MAX_PATH]; font_path(buf, type, size); if(n_fonts >= MAX_FONTS) /* safety margin, FIXME */ { /* unload an old font */ int oldest_use = -1, oldest_idx = -1; for(int i = 0; i < 2 * BUNDLE_COUNT; ++i) { if((loaded_fonts[i].status >= 0 && loaded_fonts[i].last_use < oldest_use) || oldest_use < 0) { oldest_use = loaded_fonts[i].last_use; oldest_idx = i; } } assert(oldest_idx >= 0); rb->font_unload(loaded_fonts[oldest_idx].status); loaded_fonts[oldest_idx].status = -3; n_fonts--; } loaded_fonts[font_idx].status = rb->font_load(buf); if(loaded_fonts[font_idx].status < 0) goto fallback; loaded_fonts[font_idx].last_use = access_counter++; n_fonts++; cur_font = loaded_fonts[font_idx].status; rb->lcd_setfont(cur_font); break; } case -2: case -1: goto fallback; default: loaded_fonts[font_idx].last_use = access_counter++; cur_font = loaded_fonts[font_idx].status; rb->lcd_setfont(cur_font); break; } return; fallback: cur_font = type == FONT_FIXED ? FONT_SYSFIXED : FONT_UI; rb->lcd_setfont(cur_font); return; } /*** Drawing API ***/ static void offset_coords(int *x, int *y) { if(clipped) { *x -= clip_rect.x; *y -= clip_rect.y; } } static void rb_color(int n) { if(n < 0) { fatal("bad color %d", n); return; } rb->lcd_set_foreground(colors[n]); } /* clipping is implemented through viewports and offsetting * coordinates */ static void rb_clip(void *handle, int x, int y, int w, int h) { if(!zoom_enabled) { if(!settings.clipoff) { LOGF("rb_clip(%d %d %d %d)", x, y, w, h); clip_rect.x = MAX(0, x); clip_rect.y = MAX(0, y); clip_rect.width = MIN(LCD_WIDTH, w); clip_rect.height = MIN(LCD_HEIGHT, h); clip_rect.font = FONT_UI; clip_rect.drawmode = DRMODE_SOLID; #if LCD_DEPTH > 1 clip_rect.fg_pattern = LCD_DEFAULT_FG; clip_rect.bg_pattern = LCD_DEFAULT_BG; #endif rb->lcd_set_viewport(&clip_rect); clipped = true; } } else { zoom_clipu = y; zoom_clipd = y + h; zoom_clipl = x; zoom_clipr = x + w; } } static void rb_unclip(void *handle) { if(!zoom_enabled) { LOGF("rb_unclip"); rb->lcd_set_viewport(NULL); clipped = false; } else { zoom_clipu = 0; zoom_clipd = zoom_h; zoom_clipl = 0; zoom_clipr = zoom_w; } } static void rb_draw_text(void *handle, int x, int y, int fonttype, int fontsize, int align, int color, const char *text) { (void) fontsize; if(!zoom_enabled) { LOGF("rb_draw_text(%d %d %s)", x, y, text); offset_coords(&x, &y); rb_setfont(fonttype, fontsize); int w, h; rb->lcd_getstringsize(text, &w, &h); if(align & ALIGN_VNORMAL) y -= h; else if(align & ALIGN_VCENTRE) y -= h / 2; if(align & ALIGN_HCENTRE) x -= w / 2; else if(align & ALIGN_HRIGHT) x -= w; rb_color(color); rb->lcd_set_drawmode(DRMODE_FG); rb->lcd_putsxy(x, y, text); rb->lcd_set_drawmode(DRMODE_SOLID); } else { rb_color(color); rb_setfont(fonttype, fontsize); /* size will be clamped if too large */ int w, h; rb->lcd_getstringsize(text, &w, &h); if(align & ALIGN_VNORMAL) y -= h; else if(align & ALIGN_VCENTRE) y -= h / 2; if(align & ALIGN_HCENTRE) x -= w / 2; else if(align & ALIGN_HRIGHT) x -= w; /* we need to access the font bitmap directly */ struct font *pf = rb->font_get(cur_font); while(*text) { /* still only reads 1 byte */ unsigned short c = *text++; const unsigned char *bits = rb->font_get_bits(pf, c); int width = rb->font_get_width(pf, c); /* straight from lcd-bitmap-common.c */ #if defined(HAVE_LCD_COLOR) if (pf->depth) { /* lcd_alpha_bitmap_part isn't exported directly. However, * we can create a null bitmap struct with only an alpha * channel to make lcd_bmp_part call it for us. */ zoom_alpha_bitmap(bits, x, y, width, pf->height); } else #endif zoom_mono_bitmap(bits, x, y, width, pf->height); x += width; } } } static void rb_draw_rect(void *handle, int x, int y, int w, int h, int color) { if(!zoom_enabled) { LOGF("rb_draw_rect(%d, %d, %d, %d, %d)", x, y, w, h, color); rb_color(color); offset_coords(&x, &y); rb->lcd_fillrect(x, y, w, h); } else { /* TODO: clipping */ rb_color(color); for(int i = y; i < y + h; ++i) { zoom_hline(x, x + w, i); } } } /* a goes from 0-255, with a = 255 being fully opaque and a = 0 transparent */ static inline void plot(fb_data *fb, int w, int h, unsigned x, unsigned y, unsigned long a, unsigned long r1, unsigned long g1, unsigned long b1, unsigned cl, unsigned cr, unsigned cu, unsigned cd) { /* This is really quite possibly the least efficient way of doing this. A better way would be in draw_antialiased_line(), but the problem with that is that the algorithms I investigated at least were incorrect at least part of the time and didn't make drawing much faster overall. */ if(!(cl <= x && x < cr && cu <= y && y < cd)) return; fb_data *ptr = fb + y * w + x; fb_data orig = *ptr; unsigned long r2, g2, b2; #if LCD_DEPTH != 24 r2 = RGB_UNPACK_RED(orig); g2 = RGB_UNPACK_GREEN(orig); b2 = RGB_UNPACK_BLUE(orig); #else r2 = orig.r; g2 = orig.g; b2 = orig.b; #endif unsigned long r, g, b; r = ((r1 * a) + (r2 * (256 - a))) >> 8; g = ((g1 * a) + (g2 * (256 - a))) >> 8; b = ((b1 * a) + (b2 * (256 - a))) >> 8; #if LCD_DEPTH != 24 *ptr = LCD_RGBPACK(r, g, b); #else *ptr = (fb_data) {b, g, r}; #endif } /* speed benchmark: 34392 lines/sec vs 112687 non-antialiased * lines/sec at full optimization on ipod6g */ /* expects UN-OFFSET coordinates, directly access framebuffer */ static void draw_antialiased_line(fb_data *fb, int w, int h, int x0, int y0, int x1, int y1) { /* fixed-point Wu's algorithm, modified for integer-only endpoints */ /* passed to plot() to avoid re-calculation */ unsigned short l = 0, r = w, u = 0, d = h; if(!zoom_enabled) { if(clipped) { l = clip_rect.x; r = clip_rect.x + clip_rect.width; u = clip_rect.y; d = clip_rect.y + clip_rect.height; } } else { l = zoom_clipl; r = zoom_clipr; u = zoom_clipu; d = zoom_clipd; } bool steep = ABS(y1 - y0) > ABS(x1 - x0); int tmp; if(steep) { SWAP(x0, y0, tmp); SWAP(x1, y1, tmp); } if(x0 > x1) { SWAP(x0, x1, tmp); SWAP(y0, y1, tmp); } int dx, dy; dx = x1 - x0; dy = y1 - y0; if(!(dx << FRACBITS)) return; /* bail out */ long gradient = fp_div(dy << FRACBITS, dx << FRACBITS, FRACBITS); long intery = (y0 << FRACBITS); unsigned color = rb->lcd_get_foreground(); unsigned long r1, g1, b1; r1 = RGB_UNPACK_RED(color); g1 = RGB_UNPACK_GREEN(color); b1 = RGB_UNPACK_BLUE(color); /* main loop */ if(steep) { for(int x = x0; x <= x1; ++x, intery += gradient) { unsigned y = intery >> FRACBITS; unsigned alpha = fp_fpart(intery, FRACBITS) >> (FRACBITS - 8); plot(fb, w, h, y, x, (1 << 8) - alpha, r1, g1, b1, l, r, u, d); plot(fb, w, h, y + 1, x, alpha, r1, g1, b1, l, r, u, d); } } else { for(int x = x0; x <= x1; ++x, intery += gradient) { unsigned y = intery >> FRACBITS; unsigned alpha = fp_fpart(intery, FRACBITS) >> (FRACBITS - 8); plot(fb, w, h, x, y, (1 << 8) - alpha, r1, g1, b1, l, r, u, d); plot(fb, w, h, x, y + 1, alpha, r1, g1, b1, l, r, u, d); } } } static void rb_draw_line(void *handle, int x1, int y1, int x2, int y2, int color) { if(!zoom_enabled) { LOGF("rb_draw_line(%d, %d, %d, %d, %d)", x1, y1, x2, y2, color); rb_color(color); if(settings.no_aa) { offset_coords(&x1, &y1); offset_coords(&x2, &y2); rb->lcd_drawline(x1, y1, x2, y2); } else draw_antialiased_line(rb->lcd_framebuffer, LCD_WIDTH, LCD_HEIGHT, x1, y1, x2, y2); } else { /* draw_antialiased_line uses rb->lcd_get_foreground() to get * the color */ rb_color(color); draw_antialiased_line(zoom_fb, zoom_w, zoom_h, x1, y1, x2, y2); } } #if 0 /* * draw filled polygon * originally by Sebastian Leonhardt (ulmutul) * 'count' : number of coordinate pairs * 'pxy': array of coordinates. pxy[0]=x0,pxy[1]=y0,... * note: provide space for one extra coordinate, because the starting point * will automatically be inserted as end point. */ /* * helper function: * find points of intersection between polygon and scanline */ #define MAX_INTERSECTION 32 static void fill_poly_line(int scanline, int count, int *pxy) { int i; int j; int num_of_intersects; int direct, old_direct; //intersections of every line with scanline (y-coord) int intersection[MAX_INTERSECTION]; /* add starting point as ending point */ pxy[count*2] = pxy[0]; pxy[count*2+1] = pxy[1]; old_direct=0; num_of_intersects=0; for (i=0; i y2) continue; } else { if (scanline < y2 || scanline > y1) continue; } // calculate x-coord of intersection if (y1==y2) { direct=0; } else { direct = y1>y2 ? 1 : -1; // omit double intersections, if both lines lead in the same direction intersection[num_of_intersects] = x1+((scanline-y1)*(x2-x1))/(y2-y1); if ( (direct!=old_direct) || (intersection[num_of_intersects] != intersection[num_of_intersects-1]) ) ++num_of_intersects; } old_direct = direct; } // sort points of intersection for (i=0; ilcd_hline(intersection[i], intersection[i+1], scanline); } } /* two extra elements at end of pxy needed */ static void v_fillarea(int count, int *pxy) { int i; int y1, y2; // find min and max y coords y1=y2=pxy[1]; for (i=3; i y2) y2 = pxy[i]; } for (i=y1; i<=y2; ++i) { fill_poly_line(i, count, pxy); } } #endif /* I'm a horrible person: this was copy-pasta'd straight from * xlcd_draw.c */ /* sort the given coordinates by increasing x value */ static void sort_points_by_increasing_x(int* x1, int* y1, int* x2, int* y2, int* x3, int* y3) { int x, y; if (*x1 > *x3) { if (*x2 < *x3) /* x2 < x3 < x1 */ { x = *x1; *x1 = *x2; *x2 = *x3; *x3 = x; y = *y1; *y1 = *y2; *y2 = *y3; *y3 = y; } else if (*x2 > *x1) /* x3 < x1 < x2 */ { x = *x1; *x1 = *x3; *x3 = *x2; *x2 = x; y = *y1; *y1 = *y3; *y3 = *y2; *y2 = y; } else /* x3 <= x2 <= x1 */ { x = *x1; *x1 = *x3; *x3 = x; y = *y1; *y1 = *y3; *y3 = y; } } else { if (*x2 < *x1) /* x2 < x1 <= x3 */ { x = *x1; *x1 = *x2; *x2 = x; y = *y1; *y1 = *y2; *y2 = y; } else if (*x2 > *x3) /* x1 <= x3 < x2 */ { x = *x2; *x2 = *x3; *x3 = x; y = *y2; *y2 = *y3; *y3 = y; } /* else already sorted */ } } #define sort_points_by_increasing_y(x1, y1, x2, y2, x3, y3) \ sort_points_by_increasing_x(y1, x1, y2, x2, y3, x3) /* draw a filled triangle, using horizontal lines for speed */ static void zoom_filltriangle(int x1, int y1, int x2, int y2, int x3, int y3) { long fp_x1, fp_x2, fp_dx1, fp_dx2; int y; sort_points_by_increasing_y(&x1, &y1, &x2, &y2, &x3, &y3); if (y1 < y3) /* draw */ { fp_dx1 = ((x3 - x1) << 16) / (y3 - y1); fp_x1 = (x1 << 16) + (1<<15) + (fp_dx1 >> 1); if (y1 < y2) /* first part */ { fp_dx2 = ((x2 - x1) << 16) / (y2 - y1); fp_x2 = (x1 << 16) + (1<<15) + (fp_dx2 >> 1); for (y = y1; y < y2; y++) { zoom_hline(fp_x1 >> 16, fp_x2 >> 16, y); fp_x1 += fp_dx1; fp_x2 += fp_dx2; } } if (y2 < y3) /* second part */ { fp_dx2 = ((x3 - x2) << 16) / (y3 - y2); fp_x2 = (x2 << 16) + (1<<15) + (fp_dx2 >> 1); for (y = y2; y < y3; y++) { zoom_hline(fp_x1 >> 16, fp_x2 >> 16, y); fp_x1 += fp_dx1; fp_x2 += fp_dx2; } } } } /* Should probably refactor this */ static void rb_draw_poly(void *handle, int *coords, int npoints, int fillcolor, int outlinecolor) { if(!zoom_enabled) { LOGF("rb_draw_poly"); if(fillcolor >= 0) { rb_color(fillcolor); #if 1 /* serious hack: draw a bunch of triangles between adjacent points */ /* this generally works, even with some concave polygons */ for(int i = 2; i < npoints; ++i) { int x1, y1, x2, y2, x3, y3; x1 = coords[0]; y1 = coords[1]; x2 = coords[(i - 1) * 2]; y2 = coords[(i - 1) * 2 + 1]; x3 = coords[i * 2]; y3 = coords[i * 2 + 1]; offset_coords(&x1, &y1); offset_coords(&x2, &y2); offset_coords(&x3, &y3); xlcd_filltriangle(x1, y1, x2, y2, x3, y3); #ifdef DEBUG_MENU if(settings.polyanim) { rb->lcd_update(); rb->sleep(HZ/4); } #endif #if 0 /* debug code */ rb->lcd_set_foreground(LCD_RGBPACK(255,0,0)); rb->lcd_drawpixel(x1, y1); rb->lcd_drawpixel(x2, y2); rb->lcd_drawpixel(x3, y3); rb->lcd_update(); rb->sleep(HZ); rb_color(fillcolor); rb->lcd_drawpixel(x1, y1); rb->lcd_drawpixel(x2, y2); rb->lcd_drawpixel(x3, y3); rb->lcd_update(); #endif } #else int *pxy = smalloc(sizeof(int) * 2 * npoints + 2); /* copy points, offsetted */ for(int i = 0; i < npoints; ++i) { pxy[2 * i + 0] = coords[2 * i + 0]; pxy[2 * i + 1] = coords[2 * i + 1]; offset_coords(&pxy[2*i+0], &pxy[2*i+1]); } v_fillarea(npoints, pxy); sfree(pxy); #endif } /* draw outlines last so they're not covered by the fill */ assert(outlinecolor >= 0); rb_color(outlinecolor); for(int i = 1; i < npoints; ++i) { int x1, y1, x2, y2; x1 = coords[2 * (i - 1)]; y1 = coords[2 * (i - 1) + 1]; x2 = coords[2 * i]; y2 = coords[2 * i + 1]; if(settings.no_aa) { offset_coords(&x1, &y1); offset_coords(&x2, &y2); rb->lcd_drawline(x1, y1, x2, y2); } else draw_antialiased_line(rb->lcd_framebuffer, LCD_WIDTH, LCD_HEIGHT, x1, y1, x2, y2); #ifdef DEBUG_MENU if(settings.polyanim) { rb->lcd_update(); rb->sleep(HZ/4); } #endif } int x1, y1, x2, y2; x1 = coords[0]; y1 = coords[1]; x2 = coords[2 * (npoints - 1)]; y2 = coords[2 * (npoints - 1) + 1]; if(settings.no_aa) { offset_coords(&x1, &y1); offset_coords(&x2, &y2); rb->lcd_drawline(x1, y1, x2, y2); } else draw_antialiased_line(rb->lcd_framebuffer, LCD_WIDTH, LCD_HEIGHT, x1, y1, x2, y2); } else { LOGF("rb_draw_poly"); if(fillcolor >= 0) { rb_color(fillcolor); /* serious hack: draw a bunch of triangles between adjacent points */ /* this generally works, even with some concave polygons */ for(int i = 2; i < npoints; ++i) { int x1, y1, x2, y2, x3, y3; x1 = coords[0]; y1 = coords[1]; x2 = coords[(i - 1) * 2]; y2 = coords[(i - 1) * 2 + 1]; x3 = coords[i * 2]; y3 = coords[i * 2 + 1]; zoom_filltriangle(x1, y1, x2, y2, x3, y3); } } /* draw outlines last so they're not covered by the fill */ assert(outlinecolor >= 0); rb_color(outlinecolor); for(int i = 1; i < npoints; ++i) { int x1, y1, x2, y2; x1 = coords[2 * (i - 1)]; y1 = coords[2 * (i - 1) + 1]; x2 = coords[2 * i]; y2 = coords[2 * i + 1]; draw_antialiased_line(zoom_fb, zoom_w, zoom_h, x1, y1, x2, y2); } int x1, y1, x2, y2; x1 = coords[0]; y1 = coords[1]; x2 = coords[2 * (npoints - 1)]; y2 = coords[2 * (npoints - 1) + 1]; draw_antialiased_line(zoom_fb, zoom_w, zoom_h, x1, y1, x2, y2); } } static void rb_draw_circle(void *handle, int cx, int cy, int radius, int fillcolor, int outlinecolor) { if(!zoom_enabled) { LOGF("rb_draw_circle(%d, %d, %d)", cx, cy, radius); offset_coords(&cx, &cy); if(fillcolor >= 0) { rb_color(fillcolor); xlcd_fillcircle(cx, cy, radius - 1); } assert(outlinecolor >= 0); rb_color(outlinecolor); xlcd_drawcircle(cx, cy, radius - 1); } else { if(fillcolor >= 0) { rb_color(fillcolor); zoom_fillcircle(cx, cy, radius - 1); } assert(outlinecolor >= 0); rb_color(outlinecolor); zoom_drawcircle(cx, cy, radius - 1); } } struct blitter { bool have_data; int x, y; struct bitmap bmp; }; /* originally from emcc.c */ static void trim_rect(int *x, int *y, int *w, int *h) { int x0, x1, y0, y1; /* * Reduce the size of the copied rectangle to stop it going * outside the bounds of the canvas. */ /* Transform from x,y,w,h form into coordinates of all edges */ x0 = *x; y0 = *y; x1 = *x + *w; y1 = *y + *h; int screenw = zoom_enabled ? zoom_w : LCD_WIDTH; int screenh = zoom_enabled ? zoom_h : LCD_HEIGHT; /* Clip each coordinate at both extremes of the canvas */ x0 = (x0 < 0 ? 0 : x0 > screenw - 1 ? screenw - 1: x0); x1 = (x1 < 0 ? 0 : x1 > screenw - 1 ? screenw - 1: x1); y0 = (y0 < 0 ? 0 : y0 > screenh - 1 ? screenh - 1: y0); y1 = (y1 < 0 ? 0 : y1 > screenh - 1 ? screenh - 1: y1); /* Transform back into x,y,w,h to return */ *x = x0; *y = y0; *w = x1 - x0; *h = y1 - y0; } static blitter *rb_blitter_new(void *handle, int w, int h) { LOGF("rb_blitter_new"); blitter *b = snew(blitter); b->bmp.width = w; b->bmp.height = h; b->bmp.data = smalloc(w * h * sizeof(fb_data)); b->have_data = false; return b; } static void rb_blitter_free(void *handle, blitter *bl) { LOGF("rb_blitter_free"); sfree(bl->bmp.data); sfree(bl); return; } /* copy a section of the framebuffer */ static void rb_blitter_save(void *handle, blitter *bl, int x, int y) { /* no viewport offset */ #if defined(LCD_STRIDEFORMAT) && (LCD_STRIDEFORMAT == VERTICAL_STRIDE) #error no vertical stride #else if(bl->bmp.data) { int w = bl->bmp.width, h = bl->bmp.height; int screen_w = zoom_enabled ? zoom_w : LCD_WIDTH; trim_rect(&x, &y, &w, &h); fb_data *fb = zoom_enabled ? zoom_fb : rb->lcd_framebuffer; LOGF("rb_blitter_save(%d, %d, %d, %d)", x, y, w, h); for(int i = 0; i < h; ++i) { /* copy line-by-line */ rb->memcpy(bl->bmp.data + sizeof(fb_data) * i * w, fb + (y + i) * screen_w + x, w * sizeof(fb_data)); } bl->x = x; bl->y = y; bl->have_data = true; } #endif } static void rb_blitter_load(void *handle, blitter *bl, int x, int y) { LOGF("rb_blitter_load"); if(!bl->have_data) return; int w = bl->bmp.width, h = bl->bmp.height; if(x == BLITTER_FROMSAVED) x = bl->x; if(y == BLITTER_FROMSAVED) y = bl->y; if(!zoom_enabled) offset_coords(&x, &y); trim_rect(&x, &y, &w, &h); if(!zoom_enabled) { rb->lcd_bitmap((fb_data*)bl->bmp.data, x, y, w, h); } else { for(int i = 0; i < h; ++i) { rb->memcpy(zoom_fb + (y + i) * zoom_w + x, bl->bmp.data + sizeof(fb_data) * i * w, w * sizeof(fb_data)); } } } static void rb_draw_update(void *handle, int x, int y, int w, int h) { LOGF("rb_draw_update(%d, %d, %d, %d)", x, y, w, h); /* It seems that the puzzles use a different definition of * "updating" the display than Rockbox does; by calling this * function, it tells us that it has either already drawn to the * updated area (as rockbox assumes), or that it WILL draw to the * said area. Thus we simply remember a rectangle that contains * all the updated regions and update it at the very end. */ /* adapted from gtk.c */ if (!need_draw_update || ud_l > x ) ud_l = x; if (!need_draw_update || ud_r < x+w) ud_r = x+w; if (!need_draw_update || ud_u > y ) ud_u = y; if (!need_draw_update || ud_d < y+h) ud_d = y+h; need_draw_update = true; } static void rb_start_draw(void *handle) { (void) handle; /* ... mumble mumble ... not ... reentrant ... mumble mumble ... */ need_draw_update = false; ud_l = 0; ud_r = LCD_WIDTH; ud_u = 0; ud_d = LCD_HEIGHT; } static void rb_end_draw(void *handle) { (void) handle; if(!zoom_enabled) { LOGF("rb_end_draw"); if(need_draw_update) rb->lcd_update_rect(MAX(0, ud_l), MAX(0, ud_u), MIN(LCD_WIDTH, ud_r - ud_l), MIN(LCD_HEIGHT, ud_d - ud_u)); } else { /* stubbed */ } } static void rb_status_bar(void *handle, const char *text) { if(titlebar) sfree(titlebar); titlebar = dupstr(text); LOGF("game title is %s\n", text); } static int get_titleheight(void) { return rb->font_get(FONT_UI)->height; } static void draw_title(bool clear_first) { const char *base; if(titlebar) base = titlebar; else 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; if(!zoom_enabled) { orig_clipped = clipped; if(orig_clipped) rb_unclip(NULL); } int w, h; cur_font = FONT_UI; rb->lcd_setfont(cur_font); rb->lcd_getstringsize(str, &w, &h); rb->lcd_set_foreground(BG_COLOR); rb->lcd_fillrect(0, LCD_HEIGHT - h, clear_first ? LCD_WIDTH : w, h); rb->lcd_set_drawmode(DRMODE_FG); rb->lcd_set_foreground(LCD_BLACK); rb->lcd_putsxy(0, LCD_HEIGHT - h, str); 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, int nstrings) { return dupstr(strings[0]); } const drawing_api rb_drawing = { rb_draw_text, rb_draw_rect, rb_draw_line, rb_draw_poly, rb_draw_circle, rb_draw_update, rb_clip, rb_unclip, rb_start_draw, rb_end_draw, rb_status_bar, rb_blitter_new, rb_blitter_free, rb_blitter_save, rb_blitter_load, NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */ NULL, NULL, /* line_width, line_dotted */ rb_text_fallback, NULL, }; /** functions exported to puzzles code **/ void fatal(const char *fmt, ...) { va_list ap; rb->splash(HZ, "FATAL"); va_start(ap, fmt); char buf[80]; rb->vsnprintf(buf, 80, fmt, ap); rb->splash(HZ * 2, buf); va_end(ap); exit(1); } void get_random_seed(void **randseed, int *randseedsize) { *randseed = snew(long); long seed = *rb->current_tick; rb->memcpy(*randseed, &seed, sizeof(seed)); *randseedsize = sizeof(long); } 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; } void activate_timer(frontend *fe) { last_tstamp = *rb->current_tick; timer_on = true; } void deactivate_timer(frontend *fe) { timer_on = false; } void frontend_default_color(frontend *fe, float *out) { *out++ = BG_R; *out++ = BG_G; *out++ = BG_B; } /** frontend code -- mostly UI stuff **/ /* 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; } /* either pan around a zoomed-in image or play zoomed-in */ static void zoom(void) { rb->splash(0, "Please wait..."); zoom_w = LCD_WIDTH * ZOOM_FACTOR, zoom_h = LCD_HEIGHT * ZOOM_FACTOR; zoom_clipu = 0; zoom_clipd = zoom_h; zoom_clipl = 0; zoom_clipr = zoom_w; midend_size(me, &zoom_w, &zoom_h, TRUE); /* Allocating the framebuffer will mostly likely grab the * audiobuffer, which will make impossible to load new fonts, and * lead to zoomed puzzles being drawn with the default fallback * fonts. As a semi-workaround, we go ahead and load the biggest available * monospace and proportional fonts. */ rb_setfont(FONT_FIXED, BUNDLE_MAX); rb_setfont(FONT_VARIABLE, BUNDLE_MAX); zoom_fb = smalloc(zoom_w * zoom_h * sizeof(fb_data)); if(!zoom_fb) { rb->splashf(HZ * 2, "Not enough memory to allocate %d KB framebuffer!", zoom_w * zoom_h * sizeof(fb_data) / 1024); return; } zoom_enabled = true; /* 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(false); /* false since we don't want to use more screen space than we need. */ 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) { if(view_mode) { 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(false); rb->lcd_update(); rb->yield(); } else { /* basically a copy-pasta'd main loop */ int button = process_input(timer_on ? TIMER_INTERVAL : -1, false); if(button < 0) { view_mode = true; continue; } 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(false); rb->lcd_update(); rb->yield(); } } } static const char *config_choices_formatter(int sel, void *data, char *buf, size_t len) { /* we can't rely on being called in any particular order */ char *list = dupstr(data); char delimbuf[2] = { *list, 0 }; char *save = NULL; char *str = rb->strtok_r(list, delimbuf, &save); for(int i = 0; i < sel; ++i) str = rb->strtok_r(NULL, delimbuf, &save); rb->snprintf(buf, len, "%s", str); sfree(list); return buf; } static int list_choose(const char *list_str, const char *title, int sel) { char delim = *list_str; const char *ptr = list_str + 1; int n = 0; while(ptr) { n++; ptr = strchr(ptr + 1, delim); } struct gui_synclist list; rb->gui_synclist_init(&list, &config_choices_formatter, (void*)list_str, false, 1, NULL); rb->gui_synclist_set_icon_callback(&list, NULL); rb->gui_synclist_set_nb_items(&list, n); rb->gui_synclist_limit_scroll(&list, false); rb->gui_synclist_select_item(&list, sel); rb->gui_synclist_set_title(&list, (char*)title, NOICON); while (1) { rb->gui_synclist_draw(&list); int button = rb->get_action(CONTEXT_LIST, TIMEOUT_BLOCK); if(rb->gui_synclist_do_button(&list, &button, LIST_WRAP_ON)) continue; switch(button) { case ACTION_STD_OK: return rb->gui_synclist_get_sel_pos(&list); case ACTION_STD_PREV: case ACTION_STD_CANCEL: return -1; default: break; } } } static bool is_integer(const char *str) { while(*str) { char c = *str++; if(!isdigit(c) && c != '-') return false; } return true; } static void int_chooser(config_item *cfgs, int idx, int val) { config_item *cfg = cfgs + idx; int old_val = val; rb->snprintf(cfg->u.string.sval, MAX_STRLEN, "%d", val); rb->lcd_clear_display(); while(1) { rb->lcd_set_foreground(LCD_WHITE); rb->lcd_puts(0, 0, cfg->name); rb->lcd_putsf(0, 1, "< %d >", val); rb->lcd_update(); rb->lcd_set_foreground(ERROR_COLOR); int d = 0; int button = rb->button_get(true); switch(button) { case BTN_RIGHT: case BTN_RIGHT | BUTTON_REPEAT: d = 1; break; case BTN_LEFT: case BTN_LEFT | BUTTON_REPEAT: d = -1; break; case BTN_FIRE: /* config is already set */ rb->lcd_scroll_stop(); return; case BTN_PAUSE: if(val != old_val) rb->splash(HZ, "Canceled."); val = old_val; rb->snprintf(cfg->u.string.sval, MAX_STRLEN, "%d", val); rb->lcd_scroll_stop(); return; } if(d) { const char *ret; for(int i = 0; i < CHOOSER_MAX_INCR; ++i) { val += d; rb->snprintf(cfg->u.string.sval, MAX_STRLEN, "%d", val); ret = midend_set_config(me, CFG_SETTINGS, cfgs); if(!ret) { /* clear any error message */ rb->lcd_clear_display(); rb->lcd_scroll_stop(); break; } } /* failure */ if(ret) { /* bright, annoying red */ rb->lcd_set_foreground(ERROR_COLOR); rb->lcd_puts_scroll(0, 2, ret); /* reset value */ val -= d * CHOOSER_MAX_INCR; rb->snprintf(cfg->u.string.sval, MAX_STRLEN, "%d", val); assert(!midend_set_config(me, CFG_SETTINGS, cfgs)); } } } } /* return value is only meaningful when type == C_STRING, where it * indicates whether cfg->sval has been freed or otherwise altered */ static bool do_configure_item(config_item *cfgs, int idx) { config_item *cfg = cfgs + idx; switch(cfg->type) { case C_STRING: { char *newstr = smalloc(MAX_STRLEN); rb->lcd_set_foreground(LCD_WHITE); rb->lcd_set_background(LCD_BLACK); if(is_integer(cfg->u.string.sval)) { int val = atoi(cfg->u.string.sval); /* we now free the original string and give int_chooser() * a clean buffer to work with */ sfree(cfg->u.string.sval); cfg->u.string.sval = newstr; int_chooser(cfgs, idx, val); rb->lcd_set_foreground(LCD_WHITE); rb->lcd_set_background(LCD_BLACK); return true; } rb->strlcpy(newstr, cfg->u.string.sval, MAX_STRLEN); if(rb->kbd_input(newstr, MAX_STRLEN) < 0) { sfree(newstr); return false; } sfree(cfg->u.string.sval); cfg->u.string.sval = newstr; return true; } case C_BOOLEAN: { bool res = cfg->u.boolean.bval != 0; rb->set_bool(cfg->name, &res); /* seems to reset backdrop */ rb->lcd_set_backdrop(NULL); cfg->u.boolean.bval = res; break; } case C_CHOICES: { int sel = list_choose(cfg->u.choices.choicenames, cfg->name, cfg->u.choices.selected); if(sel >= 0) cfg->u.choices.selected = sel; break; } default: fatal(""); break; } return false; } const char *config_formatter(int sel, void *data, char *buf, size_t len) { config_item *cfg = data; cfg += sel; rb->snprintf(buf, len, "%s", cfg->name); return buf; } static bool config_menu(void) { char *title; config_item *config = midend_get_config(me, CFG_SETTINGS, &title); cur_font = FONT_UI; rb->lcd_setfont(cur_font); bool success = false; if(!config) { rb->splash(HZ, "Nothing to configure."); goto done; } /* count */ int n = 0; config_item *ptr = config; while(ptr->type != C_END) { n++; ptr++; } /* display a list */ struct gui_synclist list; rb->gui_synclist_init(&list, &config_formatter, config, false, 1, NULL); rb->gui_synclist_set_icon_callback(&list, NULL); rb->gui_synclist_set_nb_items(&list, n); rb->gui_synclist_limit_scroll(&list, false); rb->gui_synclist_select_item(&list, 0); bool done = false; rb->gui_synclist_set_title(&list, title, NOICON); while (!done) { rb->gui_synclist_draw(&list); int button = rb->get_action(CONTEXT_LIST, TIMEOUT_BLOCK); if(rb->gui_synclist_do_button(&list, &button, LIST_WRAP_ON)) continue; switch(button) { case ACTION_STD_OK: { int pos = rb->gui_synclist_get_sel_pos(&list); /* store the initial state */ config_item old; memcpy(&old, config + pos, sizeof(old)); char *old_str = NULL; if(old.type == C_STRING) old_str = dupstr(old.u.string.sval); bool freed_str = do_configure_item(config, pos); const char *err = midend_set_config(me, CFG_SETTINGS, config); if(err) { rb->splash(HZ, err); /* restore old state */ memcpy(config + pos, &old, sizeof(old)); if(old.type == C_STRING && freed_str) config[pos].u.string.sval = old_str; } else { if(old.type == C_STRING) { /* success, and we duplicated the old string when * we didn't need to, so free it now */ sfree(old_str); } success = true; } break; } case ACTION_STD_PREV: case ACTION_STD_CANCEL: done = true; break; default: break; } } done: sfree(title); free_cfg(config); return success; } static const char *preset_formatter(int sel, void *data, char *buf, size_t len) { struct preset_menu *menu = data; rb->snprintf(buf, len, "%s", menu->entries[sel].title); return buf; } /* main worker function */ /* returns the index of the selected item on success, -1 on failure */ static int do_preset_menu(struct preset_menu *menu, char *title, int selected) { if(!menu->n_entries) return false; /* display a list */ struct gui_synclist list; rb->gui_synclist_init(&list, &preset_formatter, menu, false, 1, NULL); rb->gui_synclist_set_icon_callback(&list, NULL); rb->gui_synclist_set_nb_items(&list, menu->n_entries); rb->gui_synclist_limit_scroll(&list, false); rb->gui_synclist_select_item(&list, selected); static char def[] = "Game Type"; rb->gui_synclist_set_title(&list, title ? title : def, NOICON); while(1) { rb->gui_synclist_draw(&list); int button = rb->get_action(CONTEXT_LIST, TIMEOUT_BLOCK); if(rb->gui_synclist_do_button(&list, &button, LIST_WRAP_ON)) continue; switch(button) { case ACTION_STD_OK: { int sel = rb->gui_synclist_get_sel_pos(&list); struct preset_menu_entry *entry = menu->entries + sel; if(entry->params) { midend_set_params(me, entry->params); return sel; } else { /* recurse */ if(do_preset_menu(entry->submenu, entry->title, 0)) /* select first one */ return sel; } break; } case ACTION_STD_PREV: case ACTION_STD_CANCEL: return -1; default: break; } } } static bool presets_menu(void) { /* figure out the index of the current preset * if it's in a submenu, give up and default to the first item */ struct preset_menu *top = midend_get_presets(me, NULL); int sel = 0; for(int i = 0; i < top->n_entries; ++i) { if(top->entries[i].id == midend_which_preset(me)) { sel = i; break; } } return do_preset_menu(midend_get_presets(me, NULL), NULL, sel) >= 0; } static void quick_help(void) { #if defined(FOR_REAL) && defined(DEBUG_MENU) if(++help_times >= 5) { rb->splash(HZ, "You are now a developer!"); debug_mode = true; } #endif rb->splash(0, quick_help_text); rb->button_get(true); return; } static void full_help(const char *name) { unsigned old_bg = rb->lcd_get_background(); bool orig_clipped = clipped; if(orig_clipped) rb_unclip(NULL); rb->lcd_set_foreground(LCD_WHITE); rb->lcd_set_background(LCD_BLACK); unload_fonts(); cur_font = FONT_UI; rb->lcd_setfont(cur_font); char *buf = smalloc(help_text_len); LZ4_decompress_tiny(help_text, buf, help_text_len); view_text(name, buf); sfree(buf); rb->lcd_set_background(old_bg); if(orig_clipped) rb_clip(NULL, clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height); } static void init_default_settings(void) { settings.slowmo_factor = 1; settings.timerflash = false; settings.clipoff = false; settings.shortcuts = false; settings.no_aa = false; settings.polyanim = false; } #ifdef DEBUG_MENU static void bench_aa(void) { rb->sleep(0); #ifdef HAVE_ADJUSTABLE_CPU_FREQ rb->cpu_boost(true); #endif int next = *rb->current_tick + HZ; int i = 0; while(*rb->current_tick < next) { draw_antialiased_line(rb->lcd_framebuffer, LCD_WIDTH, LCD_HEIGHT, 0, 0, 20, 31); ++i; } rb->splashf(HZ, "%d AA lines/sec", i); next = *rb->current_tick + HZ; int j = 0; while(*rb->current_tick < next) { rb->lcd_drawline(0, 0, 20, 31); ++j; } rb->splashf(HZ, "%d normal lines/sec", j); rb->splashf(HZ, "Efficiency: %d%%", 100 * i / j); #ifdef HAVE_ADJUSTABLE_CPU_FREQ rb->cpu_boost(false); #endif } static void debug_menu(void) { MENUITEM_STRINGLIST(menu, "Debug Menu", NULL, "Slowmo factor", "Randomize colors", "Toggle flash pixel on timer", "Toggle clip", "Toggle shortcuts", "Toggle antialias", "Benchmark antialias", "Toggle show poly steps", "Back"); bool quit = false; int sel = 0; while(!quit) { switch(rb->do_menu(&menu, &sel, NULL, false)) { case 0: rb->set_int("Slowmo factor", "", UNIT_INT, &settings.slowmo_factor, NULL, 1, 1, 15, NULL); break; case 1: { unsigned *ptr = colors; for(int i = 0; i < ncolors; ++i) { /* not seeded, who cares? */ *ptr++ = LCD_RGBPACK(rb->rand()%255, rb->rand()%255, rb->rand()%255); } break; } case 2: settings.timerflash = !settings.timerflash; break; case 3: settings.clipoff = !settings.clipoff; break; case 4: settings.shortcuts = !settings.shortcuts; break; case 5: settings.no_aa = !settings.no_aa; break; case 6: bench_aa(); break; case 7: settings.polyanim = !settings.polyanim; break; case 8: default: quit = true; break; } } } #endif static int pausemenu_cb(int action, const struct menu_item_ex *this_item) { int i = (intptr_t) this_item; if(action == ACTION_REQUEST_MENUITEM) { switch(i) { case 3: if(!midend_can_undo(me)) return ACTION_EXIT_MENUITEM; break; case 4: if(!midend_can_redo(me)) return ACTION_EXIT_MENUITEM; break; case 5: if(!midend_which_game(me)->can_solve) return ACTION_EXIT_MENUITEM; break; case 7: break; case 9: if(audiobuf_available) break; else return ACTION_EXIT_MENUITEM; case 10: if(!midend_get_presets(me, NULL)->n_entries) return ACTION_EXIT_MENUITEM; break; case 11: #if defined(FOR_REAL) && defined(DEBUG_MENU) if(debug_mode) break; return ACTION_EXIT_MENUITEM; #else break; #endif case 12: if(!midend_which_game(me)->can_configure) return ACTION_EXIT_MENUITEM; break; default: break; } } return action; } static void clear_and_draw(void) { rb->lcd_clear_display(); rb->lcd_update(); midend_force_redraw(me); draw_title(true); } static void reset_drawing(void) { rb->lcd_set_viewport(NULL); rb->lcd_set_backdrop(NULL); rb->lcd_set_foreground(LCD_BLACK); rb->lcd_set_background(BG_COLOR); } static int pause_menu(void) { #define static auto #define const MENUITEM_STRINGLIST(menu, NULL, pausemenu_cb, "Resume Game", // 0 "New Game", // 1 "Restart Game", // 2 "Undo", // 3 "Redo", // 4 "Solve", // 5 "Zoom In", // 6 "Quick Help", // 7 "Extensive Help", // 8 "Playback Control", // 9 "Game Type", // 10 "Debug Menu", // 11 "Configure Game", // 12 "Quit without Saving", // 13 "Quit"); // 14 #undef static #undef const /* HACK ALERT */ char title[32] = { 0 }; rb->snprintf(title, sizeof(title), "%s Menu", midend_which_game(me)->name); menu__.desc = title; #if defined(FOR_REAL) && defined(DEBUG_MENU) help_times = 0; #endif bool quit = false; int sel = 0; while(!quit) { switch(rb->do_menu(&menu, &sel, NULL, false)) { case 0: quit = true; break; case 1: midend_new_game(me); fix_size(); quit = true; break; case 2: midend_restart_game(me); fix_size(); quit = true; break; case 3: if(!midend_can_undo(me)) rb->splash(HZ, "Cannot undo."); else midend_process_key(me, 0, 0, 'u'); quit = true; break; case 4: if(!midend_can_redo(me)) rb->splash(HZ, "Cannot redo."); else midend_process_key(me, 0, 0, 'r'); quit = true; break; case 5: { const char *msg = midend_solve(me); if(msg) rb->splash(HZ, msg); quit = true; break; } case 6: zoom(); break; case 7: quick_help(); break; case 8: full_help(midend_which_game(me)->name); break; case 9: playback_control(NULL); break; case 10: if(presets_menu()) { midend_new_game(me); fix_size(); reset_drawing(); clear_and_draw(); quit = true; } break; case 11: #ifdef DEBUG_MENU debug_menu(); #endif break; case 12: if(config_menu()) { midend_new_game(me); fix_size(); reset_drawing(); clear_and_draw(); quit = true; } break; case 13: return -2; case 14: return -3; default: break; } } rb->lcd_set_background(BG_COLOR); rb->lcd_clear_display(); rb->lcd_update(); midend_force_redraw(me); return 0; } /* points to pluginbuf, used by rbmalloc.c */ /* useless side note: originally giant_buffer was a statically * allocated giant array (4096KB IIRC), hence its name. */ char *giant_buffer = NULL; static size_t giant_buffer_len = 0; /* set on start */ static void fix_size(void) { int w = LCD_WIDTH, h = LCD_HEIGHT, h_x; cur_font = FONT_UI; rb->lcd_setfont(cur_font); rb->lcd_getstringsize("X", NULL, &h_x); h -= h_x; midend_size(me, &w, &h, TRUE); } static void init_tlsf(void) { /* reset tlsf by nuking the signature */ /* will make any already-allocated memory point to garbage */ memset(giant_buffer, 0, 4); init_memory_pool(giant_buffer_len, giant_buffer); } static int read_wrapper(void *ptr, void *buf, int len) { int fd = (int) ptr; return rb->read(fd, buf, len); } static void write_wrapper(void *ptr, const void *buf, int len) { int fd = (int) ptr; rb->write(fd, buf, len); } static void init_colors(void) { float *floatcolors = midend_colors(me, &ncolors); /* convert them to packed RGB */ colors = smalloc(ncolors * sizeof(unsigned)); unsigned *ptr = colors; float *floatptr = floatcolors; for(int i = 0; i < ncolors; ++i) { int r = 255 * *(floatptr++); int g = 255 * *(floatptr++); int b = 255 * *(floatptr++); LOGF("color %d is %d %d %d", i, r, g, b); *ptr++ = LCD_RGBPACK(r, g, b); } sfree(floatcolors); } static const char *init_for_game(const game *gm, int load_fd, bool draw) { me = midend_new(NULL, gm, &rb_drawing, NULL); if(load_fd < 0) midend_new_game(me); else { const char *ret = midend_deserialize(me, read_wrapper, (void*) load_fd); if(ret) return ret; } fix_size(); init_colors(); reset_drawing(); if(draw) { clear_and_draw(); } return NULL; } static void shutdown_tlsf(void) { memset(giant_buffer, 0, 4); } static void exit_handler(void) { unload_fonts(); shutdown_tlsf(); #ifdef HAVE_ADJUSTABLE_CPU_FREQ rb->cpu_boost(false); #endif } #ifdef FONT_CACHING /* try loading the fonts indicated in the on-disk font table */ static void load_fonts(void) { int fd = rb->open(FONT_TABLE, O_RDONLY); if(fd < 0) return; uint64_t fontmask = 0; while(1) { char linebuf[MAX_LINE], *ptr = linebuf; int len = rb->read_line(fd, linebuf, sizeof(linebuf)); if(len <= 0) break; char *tok, *save; tok = rb->strtok_r(ptr, ":", &save); ptr = NULL; if(!strcmp(tok, midend_which_game(me)->name)) { uint32_t left, right; tok = rb->strtok_r(ptr, ":", &save); left = atoi(tok); tok = rb->strtok_r(ptr, ":", &save); right = atoi(tok); fontmask = ((uint64_t)left << 31) | right; break; } } /* nothing to do */ if(!fontmask) { rb->close(fd); return; } /* loop through each bit of the mask and try loading the corresponding font */ for(int i = 0; i < 2 * BUNDLE_COUNT; ++i) { if(fontmask & ((uint64_t)1 << i)) { int size = (i > BUNDLE_COUNT ? i - BUNDLE_COUNT : i) + BUNDLE_MIN; int type = i > BUNDLE_COUNT ? FONT_VARIABLE : FONT_FIXED; rb_setfont(type, size); } } rb->close(fd); } /* remember which fonts were loaded */ static void save_fonts(void) { #if 2*BUNDLE_COUNT > 62 #error too many fonts for 62-bit mask #endif /* first assemble the bitmask */ uint64_t fontmask = 0; for(int i = 0; i < 2 * BUNDLE_COUNT; ++i) { /* try loading if we attempted to load */ if(loaded_fonts[i].status >= -2) { fontmask |= (uint64_t)1 << i; } } if(fontmask) { /* font table format is as follows: * [GAME NAME]:[32-halves of bit mask in decimal][newline] */ int fd = rb->open(FONT_TABLE, O_RDONLY); int outfd = rb->open(FONT_TABLE ".tmp", O_WRONLY | O_CREAT | O_TRUNC, 0666); if(outfd < 0) return; uint64_t oldmask = 0; if(fd >= 0) { while(1) { char linebuf[MAX_LINE], *ptr = linebuf; char origbuf[MAX_LINE]; int len = rb->read_line(fd, linebuf, sizeof(linebuf)); if(len <= 0) break; rb->memcpy(origbuf, linebuf, len); char *tok, *save; tok = rb->strtok_r(ptr, ":", &save); ptr = NULL; /* copy line if not matching */ if(strcmp(tok, midend_which_game(me)->name) != 0) { rb->write(outfd, origbuf, len); rb->fdprintf(outfd, "\n"); } else { /* matching, remember the old mask */ assert(oldmask == 0); uint32_t left, right; tok = rb->strtok_r(ptr, ":", &save); left = atoi(tok); tok = rb->strtok_r(ptr, ":", &save); right = atoi(tok); oldmask = ((uint64_t)left << 31) | right; } } rb->close(fd); } uint64_t final = fontmask; if(n_fonts < MAX_FONTS) final |= oldmask; uint32_t left = final >> 31; uint32_t right = final & 0x7fffffff; rb->fdprintf(outfd, "%s:%lu:%lu\n", midend_which_game(me)->name, left, right); rb->close(outfd); rb->rename(FONT_TABLE ".tmp", FONT_TABLE); } } #endif static void save_fname(char *buf) { rb->snprintf(buf, MAX_PATH, "%s/sgt-%s.sav", PLUGIN_GAMES_DATA_DIR, thegame.htmlhelp_topic); } /* expects a totally free me* pointer */ static bool load_game(void) { char fname[MAX_PATH]; save_fname(fname); int fd = rb->open(fname, O_RDONLY); if(fd < 0) return false; rb->splash(0, "Loading..."); char *game; const char *ret = identify_game(&game, read_wrapper, (void*)fd); if(!*game && ret) { sfree(game); rb->splash(HZ, ret); rb->close(fd); return false; } else { /* seek to beginning */ rb->lseek(fd, 0, SEEK_SET); if(!strcmp(game, thegame.name)) { ret = init_for_game(&thegame, fd, false); if(ret) { rb->splash(HZ, ret); rb->close(fd); rb->remove(fname); return false; } rb->close(fd); rb->remove(fname); #ifdef FONT_CACHING load_fonts(); #endif /* success */ return true; } rb->splashf(HZ, "Failed loading save for %s!", game); /* clean up, even on failure */ rb->close(fd); rb->remove(fname); return false; } } static void save_game(void) { rb->splash(0, "Saving..."); char fname[MAX_PATH]; save_fname(fname); /* save game */ int fd = rb->open(fname, O_WRONLY | O_CREAT | O_TRUNC, 0666); midend_serialize(me, write_wrapper, (void*) fd); rb->close(fd); #ifdef FONT_CACHING save_fonts(); #endif rb->lcd_update(); } static int mainmenu_cb(int action, const struct menu_item_ex *this_item) { int i = (intptr_t) this_item; if(action == ACTION_REQUEST_MENUITEM) { switch(i) { case 0: case 7: if(!load_success) return ACTION_EXIT_MENUITEM; break; case 3: break; case 4: if(audiobuf_available) break; else return ACTION_EXIT_MENUITEM; case 5: if(!midend_get_presets(me, NULL)->n_entries) return ACTION_EXIT_MENUITEM; break; case 6: if(!midend_which_game(me)->can_configure) return ACTION_EXIT_MENUITEM; break; default: break; } } return action; } enum plugin_status plugin_start(const void *param) { (void) param; #ifdef HAVE_ADJUSTABLE_CPU_FREQ /* boost for init */ rb->cpu_boost(true); #endif giant_buffer = rb->plugin_get_buffer(&giant_buffer_len); rb_atexit(exit_handler); init_tlsf(); /* sanity check */ if(fabs(sqrt(3)/2 - sin(PI/3)) > .01) { return PLUGIN_ERROR; } init_default_settings(); init_fonttab(); load_success = load_game(); if(!load_success) { /* our main menu expects a ready-to-use midend */ init_for_game(&thegame, -1, false); } #ifdef HAVE_ADJUSTABLE_CPU_FREQ /* about to go to menu or button block */ rb->cpu_boost(false); #endif #if defined(FOR_REAL) && defined(DEBUG_MENU) help_times = 0; #endif #define static auto #define const MENUITEM_STRINGLIST(menu, NULL, mainmenu_cb, "Resume Game", // 0 "New Game", // 1 "Quick Help", // 2 "Extensive Help", // 3 "Playback Control", // 4 "Game Type", // 5 "Configure Game", // 6 "Quit without Saving", // 7 "Quit"); // 8 #undef static #undef const /* HACK ALERT */ char title[32] = { 0 }; rb->snprintf(title, sizeof(title), "%s Menu", midend_which_game(me)->name); menu__.desc = title; bool quit = false; int sel = 0; while(!quit) { switch(rb->do_menu(&menu, &sel, NULL, false)) { case 0: clear_and_draw(); goto game_loop; case 1: if(!load_success) { clear_and_draw(); goto game_loop; } quit = true; break; case 2: quick_help(); break; case 3: full_help(midend_which_game(me)->name); break; case 4: playback_control(NULL); break; case 5: if(presets_menu()) { midend_new_game(me); fix_size(); init_colors(); reset_drawing(); clear_and_draw(); goto game_loop; } break; case 6: if(config_menu()) { midend_new_game(me); fix_size(); init_colors(); reset_drawing(); clear_and_draw(); goto game_loop; } break; case 8: if(load_success) save_game(); /* fall through */ case 7: /* we don't care about freeing anything because tlsf will * be wiped out the next time around */ return PLUGIN_OK; default: break; } } while(1) { init_for_game(&thegame, -1, true); last_keystate = 0; accept_input = true; game_loop: while(1) { want_redraw = true; int theight = get_titleheight(); draw_title(true); rb->lcd_update_rect(0, LCD_HEIGHT - theight, LCD_WIDTH, theight); int button = process_input(timer_on ? TIMER_INTERVAL : -1, true); if(button < 0) { rb_unclip(NULL); deactivate_timer(NULL); if(titlebar) { sfree(titlebar); titlebar = NULL; } switch(button) { case -1: /* new game */ midend_free(me); break; case -2: /* quit without saving */ midend_free(me); sfree(colors); exit(PLUGIN_OK); 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(true); /* 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(); rb->yield(); } sfree(colors); } }