rockbox/apps/plugins/puzzles/rockbox.c
Franklin Wei b3356e3aff puzzles: resync with upstream
This brings the code to upstream commit 3ece3d6 (I've made my own Rockbox-
specific changes on top of that).

Changes include using C99 `bool' throughout, and minor logic fixes for some
puzzles.

Change-Id: Ie823e73ae49a8ee1de411d6d406df2ba835af541
2018-12-21 22:13:33 -05:00

3450 lines
90 KiB
C

/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2018 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:
*
* <https://www.chiark.greenend.org.uk/~sgtatham/puzzles/devel/>.
*/
#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"
#include "pluginbitmaps/puzzles_cursor.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, mouse_mode = false;
static int mouse_x, mouse_y;
extern bool audiobuf_available; /* defined in rbmalloc.c */
static fb_data *zoom_fb; /* dynamically allocated */
static int zoom_x, zoom_y, 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;
/* some games can run in a separate thread for a larger stack (only
* Solo for now) */
static int thread = -1;
/* how to process the input (custom, per-game) */
static struct {
bool want_spacebar; /* send spacebar event on long-press of select */
bool falling_edge; /* send events upon button release, not initial press */
bool ignore_repeats; /* ignore repeated button events (currently in all games but Untangle) */
bool rclick_on_hold; /* if in mouse mode, send right-click on long-press of select */
bool numerical_chooser; /* repurpose select to activate a numerical chooser */
} input_settings;
static bool accept_input = true;
/* last timer call */
static long last_tstamp;
static bool timer_on = false;
static bool load_success;
/* debug settings */
/* ...did I mention there's a secret debug menu? */
static struct {
int slowmo_factor;
bool timerflash, clipoff, shortcuts, no_aa, polyanim;
} debug_settings;
/* These are re-implementations of many rockbox drawing functions, adapted to
* draw into a custom framebuffer (used for the zoom feature). */
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
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);
zoom_fb[y * zoom_w + x].x = 255;
#elif 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()),
255
};
#elif 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);
zoom_fb[(y + i * 8 + dy) * zoom_w + x + j].x = 255;
#elif 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 routines */
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;
rb->lcd_setfont(cur_font = FONT_UI);
}
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 (normal, no zoom) ***/
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(!debug_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;
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;
if(!zoom_enabled)
{
LOGF("rb_draw_text(%d %d %s)", x, y, text);
offset_coords(&x, &y);
rb->lcd_set_drawmode(DRMODE_FG);
rb->lcd_putsxy(x, y, text);
rb->lcd_set_drawmode(DRMODE_SOLID);
}
else
{
/* 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)
{
rb_color(color);
if(!zoom_enabled)
{
LOGF("rb_draw_rect(%d, %d, %d, %d, %d)", x, y, w, h, color);
offset_coords(&x, &y);
rb->lcd_fillrect(x, y, w, h);
}
else
{
/* TODO: clipping */
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);
#elif LCD_DEPTH > 24
*ptr = (fb_data) {b, g, r, 255};
#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)
{
rb_color(color);
if(!zoom_enabled)
{
LOGF("rb_draw_line(%d, %d, %d, %d, %d)", x1, y1, x2, y2, color);
#if defined(FOR_REAL) && defined(DEBUG_MENU)
if(debug_settings.no_aa)
{
offset_coords(&x1, &y1);
offset_coords(&x2, &y2);
rb->lcd_drawline(x1, y1, x2, y2);
}
else
#endif
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 */
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<count*2; i+=2) {
int x1=pxy[i];
int y1=pxy[i+1];
int x2=pxy[i+2];
int y2=pxy[i+3];
// skip if line is outside of scanline
if (y1 < y2) {
if (scanline < y1 || scanline > 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; i<num_of_intersects-1; ++i) {
for (j=i+1; j<num_of_intersects; ++j) {
if (intersection[j]<intersection[i]) {
int temp=intersection[i];
intersection[i]=intersection[j];
intersection[j]=temp;
}
}
}
// draw
for (i=0; i<num_of_intersects; i+=2) {
rb->lcd_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<count*2; i+=2) {
if (pxy[i] < y1) y1 = pxy[i];
else if (pxy[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(debug_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(debug_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(debug_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(debug_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);
}
}
/* blitters allow the game code to save/restore a piece of the
* framebuffer */
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 ? screenw: x0);
x1 = (x1 < 0 ? 0 : x1 > screenw ? screenw: x1);
y0 = (y0 < 0 ? 0 : y0 > screenh ? screenh: y0);
y1 = (y1 < 0 ? 0 : y1 > screenh ? screenh: 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 && 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;
/* we ignore the backend's redraw requests and just unconditionally update everything */
#if 0
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 */
}
#endif
}
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 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 = false;
if(!zoom_enabled)
{
orig_clipped = clipped;
if(orig_clipped)
rb_unclip(NULL);
}
int w, h;
rb->lcd_setfont(cur_font = FONT_UI);
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);
}
}
#define MOUSE_W BMPWIDTH_puzzles_cursor
#define MOUSE_H BMPHEIGHT_puzzles_cursor
static blitter *mouse_bl = NULL;
static void clear_mouse(void)
{
bool orig_clipped = clipped;
if(!zoom_enabled)
{
if(orig_clipped)
rb_unclip(NULL);
}
if(mouse_bl)
rb_blitter_load(NULL, mouse_bl, BLITTER_FROMSAVED, BLITTER_FROMSAVED);
if(!zoom_enabled)
{
if(orig_clipped)
rb_clip(NULL, clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height);
}
}
static void draw_mouse(void)
{
bool orig_clipped = clipped;
if(!zoom_enabled)
{
if(orig_clipped)
rb_unclip(NULL);
}
if(!mouse_bl)
mouse_bl = rb_blitter_new(NULL, MOUSE_W, MOUSE_H);
/* save area being covered (will be restored elsewhere) */
rb_blitter_save(NULL, mouse_bl, mouse_x, mouse_y);
rb->lcd_bitmap_transparent(puzzles_cursor, mouse_x, mouse_y, BMPWIDTH_puzzles_cursor, BMPHEIGHT_puzzles_cursor);
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,
};
/** utility 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);
if(rb->thread_self() == thread)
rb->thread_exit();
else
exit(PLUGIN_ERROR);
}
void get_random_seed(void **randseed, int *randseedsize)
{
*randseed = snew(long);
long seed = *rb->current_tick;
rb->memcpy(*randseed, &seed, sizeof(seed));
*randseedsize = sizeof(seed);
}
static void timer_cb(void)
{
#if LCD_DEPTH < 24
if(debug_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) / debug_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 **/
static void send_click(int button, bool release)
{
int x = (zoom_enabled ? zoom_x : 0) + mouse_x,
y = (zoom_enabled ? zoom_y : 0) + mouse_y;
assert(LEFT_BUTTON + 6 == LEFT_RELEASE);
midend_process_key(me, x, y, button);
if(release)
midend_process_key(me, x, y, button + 6);
}
static int choose_key(void)
{
int options = 0;
key_label *game_keys = midend_request_keys(me, &options);
if(!game_keys || !options)
return 0;
int sel = 0;
while(1)
{
if(timer_on)
timer_cb();
midend_process_key(me, 0, 0, game_keys[sel].button);
midend_redraw(me);
rb->lcd_update();
rb->yield();
int button = rb->button_get_w_tmo(timer_on ? TIMER_INTERVAL : -1);
switch(button)
{
case BTN_LEFT:
if(--sel < 0)
sel = options - 1;
break;
case BTN_RIGHT:
if(++sel >= options)
sel = 0;
break;
case BTN_PAUSE:
return -1;
case BTN_FIRE:
free_keys(game_keys, options);
/* the key has already been sent to the game, just return */
return 0;
}
}
}
/* This function handles most user input. It has specific workarounds
* and fixes for certain games to allow them to work well on
* Rockbox. It will either return a positive value that can be passed
* to the midend, or a negative flag value. `do_pausemenu' sets how
* this function will handle BUTTON_PAUSE: if true, it will handle it
* all, otherwise it will simply return -1 and let the caller do the
* work (this is used for zoom mode). */
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);
/* See if the button is a long-press. */
if(accept_input && (button == (BTN_FIRE | BUTTON_REPEAT)))
{
LOGF("button is long-press, ignoring subsequent input until release");
/* Ignore repeated long presses. */
accept_input = false;
if(mouse_mode && input_settings.rclick_on_hold)
{
/* simulate right-click */
LOGF("sending right click");
send_click(RIGHT_BUTTON, true);
return 0;
}
/* These games want a spacebar in the event of a long press. */
if(!mouse_mode && input_settings.want_spacebar)
return ' ';
}
button = rb->button_status();
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
rb->cpu_boost(true);
#endif
if(button == BTN_PAUSE)
{
if(do_pausemenu)
{
/* 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;
}
/* Mouse movement (if enabled). This goes here since none of the
* following code is needed for mouse mode. */
if(mouse_mode)
{
static int last_mousedir = 0, held_count = 0, v = 1;
if(button & BTN_UP)
state = CURSOR_UP;
else if(button & BTN_DOWN)
state = CURSOR_DOWN;
else if(button & BTN_LEFT)
state = CURSOR_LEFT;
else if(button & BTN_RIGHT)
state = CURSOR_RIGHT;
unsigned released = ~button & last_keystate,
pressed = button & ~last_keystate;
last_keystate = button;
/* move */
/* get the direction vector the cursor is moving in. */
int new_x = mouse_x, new_y = mouse_y;
/* in src/misc.c */
move_cursor(state, &new_x, &new_y, LCD_WIDTH, LCD_HEIGHT, false);
int dx = new_x - mouse_x, dy = new_y - mouse_y;
mouse_x += dx * v;
mouse_y += dy * v;
/* clamp */
/* The % operator with negative operands is messy; this is much
* simpler. */
if(mouse_x < 0)
mouse_x = 0;
if(mouse_y < 0)
mouse_y = 0;
if(mouse_x >= LCD_WIDTH)
mouse_x = LCD_WIDTH - 1;
if(mouse_y >= LCD_HEIGHT)
mouse_y = LCD_HEIGHT - 1;
/* clicking/dragging */
/* rclick on hold requires that we fire left-click on a
* release, otherwise it's impossible to distinguish the
* two. */
if(input_settings.rclick_on_hold)
{
if(accept_input && released == BTN_FIRE)
{
LOGF("sending left click");
send_click(LEFT_BUTTON, true); /* right-click is handled earlier */
}
}
else
{
if(pressed & BTN_FIRE)
send_click(LEFT_BUTTON, false);
else if(released & BTN_FIRE)
send_click(LEFT_RELEASE, false);
else if(button & BTN_FIRE)
send_click(LEFT_DRAG, false);
}
/* acceleration */
if(state && state == last_mousedir)
{
if(++held_count % 5 == 0 && v < 15)
v++;
}
else
{
if(!button)
{
LOGF("all keys released, accepting further input");
accept_input = true;
}
last_mousedir = state;
v = 1;
held_count = 0;
}
/* no buttons are sent to the midend in mouse mode */
return 0;
}
/* These games require, for one reason or another, that events
* fire upon buttons being released rather than when they are
* pressed. For Inertia, it is because it needs to be able to
* sense multiple simultaneous keypresses (to move diagonally),
* and the others require a long press to map to a secondary
* "action" key. */
if(input_settings.falling_edge)
{
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);
}
/* Ignore repeats in all games which are not Untangle. */
else if(input_settings.ignore_repeats)
{
/* start accepting input again after a release */
if(!button)
{
accept_input = true;
return 0;
}
/* ignore repeats (in mouse mode, only ignore repeats of BTN_FIRE) */
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(input_settings.numerical_chooser)
{
if(choose_key() < 0)
{
if(do_pausemenu)
{
/* 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;
}
}
else
{
if(!strcmp("Fifteen", midend_which_game(me)->name))
state = 'h'; /* hint */
else
state = CURSOR_SELECT;
}
break;
default:
break;
}
if(debug_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;
}
/* This function handles zoom mode, where the user can either pan
* around a zoomed-in image or play a zoomed-in version of the game. */
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->splash(HZ, "OOM");
return;
}
zoom_enabled = true;
/* draws go to the zoom framebuffer */
midend_force_redraw(me);
zoom_x = zoom_y = 0;
rb->lcd_bitmap_part(zoom_fb, zoom_x, zoom_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:
zoom_y -= PAN_Y; /* clamped later */
break;
case BTN_DOWN:
zoom_y += PAN_Y; /* clamped later */
break;
case BTN_LEFT:
zoom_x -= PAN_X; /* clamped later */
break;
case BTN_RIGHT:
zoom_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(zoom_y < 0)
zoom_y = 0;
if(zoom_x < 0)
zoom_x = 0;
if(zoom_y + LCD_HEIGHT >= zoom_h)
zoom_y = zoom_h - LCD_HEIGHT;
if(zoom_x + LCD_WIDTH >= zoom_w)
zoom_x = zoom_w - LCD_WIDTH;
if(timer_on)
timer_cb();
/* goes to zoom_fb */
midend_redraw(me);
rb->lcd_bitmap_part(zoom_fb, zoom_x, zoom_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();
midend_redraw(me);
/* blit */
rb->lcd_bitmap_part(zoom_fb, zoom_x, zoom_y, STRIDE(SCREEN_MAIN, zoom_w, zoom_h),
0, 0, LCD_WIDTH, LCD_HEIGHT);
draw_title(false);
/* The cursor is always in screenspace coordinates; when
* zoomed, this means the mouse is always restricted to
* the bounds of the physical display, not the virtual
* zoom framebuffer. */
if(mouse_mode)
draw_mouse();
rb->lcd_update();
if(mouse_mode)
clear_mouse();
rb->yield();
}
}
}
/** settings/preset code */
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);
rb->lcd_setfont(cur_font = FONT_UI);
bool success = false;
if(!config)
{
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();
rb->lcd_setfont(cur_font = FONT_UI);
/* The help text is stored in compressed format in the help_text[]
* array. display_text wants an array of pointers to
* null-terminated words, so we create that here. */
char *buf = smalloc(help_text_len);
LZ4_decompress_tiny(help_text, buf, help_text_len);
/* create the word_ptrs array to pass to display_text */
char **word_ptrs = smalloc(sizeof(char*) * help_text_words);
char **ptr = word_ptrs;
bool last_was_null = false;
*ptr++ = buf;
for(int i = 1; i < help_text_len; ++i)
{
switch(buf[i])
{
case '\0':
if(last_was_null)
{
/* newline */
*ptr++ = buf + i;
}
else
last_was_null = true;
break;
default:
if(last_was_null)
*ptr++ = buf + i;
last_was_null = false;
break;
}
}
display_text(help_text_words, word_ptrs, help_text_style, NULL, true);
sfree(buf);
sfree(word_ptrs);
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)
{
debug_settings.slowmo_factor = 1;
debug_settings.timerflash = false;
debug_settings.clipoff = false;
debug_settings.shortcuts = false;
debug_settings.no_aa = false;
debug_settings.polyanim = false;
}
#ifdef DEBUG_MENU
/* Useless debug code. Mostly a waste of space. */
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",
"Toggle mouse mode",
"Toggle spacebar on long click",
"Toggle send keys on release",
"Toggle ignore repeats",
"Toggle right-click on hold vs. dragging",
"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, &debug_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:
debug_settings.timerflash = !debug_settings.timerflash;
break;
case 3:
debug_settings.clipoff = !debug_settings.clipoff;
break;
case 4:
debug_settings.shortcuts = !debug_settings.shortcuts;
break;
case 5:
debug_settings.no_aa = !debug_settings.no_aa;
break;
case 6:
bench_aa();
break;
case 7:
debug_settings.polyanim = !debug_settings.polyanim;
break;
case 8:
mouse_mode = !mouse_mode;
break;
case 9:
input_settings.want_spacebar = !input_settings.want_spacebar;
break;
case 10:
input_settings.falling_edge = !input_settings.falling_edge;
break;
case 11:
input_settings.ignore_repeats = !input_settings.ignore_repeats;
break;
case 12:
input_settings.rclick_on_hold = !input_settings.rclick_on_hold;
break;
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 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();
midend_force_redraw(me);
draw_title(true);
if(mouse_mode)
draw_mouse();
rb->lcd_update();
if(mouse_mode)
clear_mouse();
}
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();
midend_force_redraw(me);
rb->lcd_update();
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;
rb->lcd_setfont(cur_font = FONT_UI);
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 bool string_in_list(const char *target, const char **list)
{
/* list is terminated with NULL */
const char *i;
while((i = *list++))
{
if(!strcmp(target, i))
return true;
}
return false;
}
/* this function sets game-specific input settings */
static void tune_input(const char *name)
{
static const char *want_spacebar[] = {
"Magnets",
"Mines",
"Palisade",
NULL
};
/* these get a spacebar on long click */
input_settings.want_spacebar = string_in_list(name, want_spacebar);
static const char *falling_edge[] = {
"Inertia",
"Magnets",
"Map",
"Mines",
"Palisade",
NULL
};
/* wait until a key is released to send an action */
input_settings.falling_edge = string_in_list(name, falling_edge);
/* ignore repeated keypresses in all games but untangle (mouse
* mode overrides this no matter what) */
static const char *ignore_repeats[] = {
"Untangle",
NULL
};
input_settings.ignore_repeats = !string_in_list(name, ignore_repeats);
/* set to false if you want dragging to be possible */
static const char *rclick_on_hold[] = {
"Map",
"Signpost",
"Untangle",
NULL
};
input_settings.rclick_on_hold = !string_in_list(name, rclick_on_hold);
static const char *mouse_games[] = {
"Loopy",
NULL
};
mouse_mode = string_in_list(name, mouse_games);
static const char *number_chooser_games[] = {
"Filling",
"Keen",
"Solo",
"Towers",
"Undead",
"Unequal",
NULL
};
input_settings.numerical_chooser = string_in_list(name, number_chooser_games);
}
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;
}
tune_input(gm->name);
mouse_x = LCD_WIDTH / 2;
mouse_y = LCD_HEIGHT / 2;
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->splash(HZ, "Load failed.");
/* 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;
}
static void puzzles_main(void)
{
rb_atexit(exit_handler);
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;
default:
break;
}
}
while(1)
{
init_for_game(&thegame, -1, true);
last_keystate = 0;
accept_input = true;
game_loop:
while(1)
{
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);
return;
case -3:
/* save and quit */
save_game();
midend_free(me);
sfree(colors);
return;
default:
break;
}
}
if(button)
midend_process_key(me, 0, 0, button);
if(timer_on)
timer_cb();
midend_redraw(me);
draw_title(true); /* will draw to fb */
if(mouse_mode)
draw_mouse();
rb->lcd_update();
if(mouse_mode)
clear_mouse();
rb->yield();
}
sfree(colors);
}
}
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);
init_tlsf();
if(!strcmp(thegame.name, "Solo"))
{
/* Solo needs a big stack */
int stack_sz = 16 * DEFAULT_STACK_SIZE;
uintptr_t old = (uintptr_t)smalloc(stack_sz);
/* word alignment */
long *stack = (long*)((char*)(((uintptr_t)old & (uintptr_t)(~0x3)) + 4));
stack_sz -= ((char*)stack - (char*)old);
thread = rb->create_thread(puzzles_main, stack, stack_sz, 0, "puzzles"
IF_PRIO(, PRIORITY_USER_INTERFACE) IF_COP(, CPU));
rb->thread_wait(thread);
}
else
puzzles_main();
return PLUGIN_OK;
}