X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?a=blobdiff_plain;f=emcc.c;h=23ab333f5db0cb58b7579b10b9f974d4e45f9c95;hb=db313b3948d27244dd7c34c2609c66d6204d8931;hp=2a5f788e403486d883ae2a96a1ce4b4b733db931;hpb=bb14689b4a555b4b176192d2c3cd02a4c354a37f;p=sgt-puzzles.git diff --git a/emcc.c b/emcc.c index 2a5f788..23ab333 100644 --- a/emcc.c +++ b/emcc.c @@ -3,7 +3,10 @@ * end for Puzzles. * * The Javascript parts of this system live in emcclib.js and - * emccpre.js. + * emccpre.js. It also depends on being run in the context of a web + * page containing an appropriate collection of bits and pieces (a + * canvas, some buttons and links etc), which is generated for each + * puzzle by the script html/jspage.pl. */ /* @@ -18,12 +21,6 @@ * by using the DOM File API to ask the user to select a file and * permit us to see its contents. * - * - it ought to be possible to make the puzzle canvases resizable, - * by superimposing some kind of draggable resize handle. Also I - * quite like the idea of having a few buttons for standard sizes: - * reset to default size, maximise to the browser window dimensions - * (if we can find those out), and perhaps even go full-screen. - * * - I should think about whether these webified puzzles can support * touchscreen-based tablet browsers (assuming there are any that * can cope with the reasonably modern JS and run it fast enough to @@ -50,7 +47,10 @@ * that using whatever they normally use to print PDFs!) */ +#include +#include #include +#include #include "puzzles.h" @@ -61,7 +61,8 @@ extern void js_debug(const char *); extern void js_error_box(const char *message); extern void js_remove_type_dropdown(void); extern void js_remove_solve_button(void); -extern void js_add_preset(const char *name); +extern void js_add_preset(int menuid, const char *name, int value); +extern int js_add_preset_submenu(int menuid, const char *name); extern int js_get_selected_preset(void); extern void js_select_preset(int n); extern void js_get_date_64(unsigned *p); @@ -135,6 +136,27 @@ void fatal(char *fmt, ...) js_error_box(buf); } +void debug_printf(char *fmt, ...) +{ + char buf[512]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + js_debug(buf); +} + +/* + * Helper function that makes it easy to test strings that might be + * NULL. + */ +int strnullcmp(const char *a, const char *b) +{ + if (a == NULL || b == NULL) + return a != NULL ? +1 : b != NULL ? -1 : 0; + return strcmp(a, b); +} + /* * HTMLish names for the colours allocated by the puzzle. */ @@ -169,10 +191,12 @@ void timer_callback(double tplus) } /* ---------------------------------------------------------------------- - * Helper function to resize the canvas, and variables to remember its - * size for other functions (e.g. trimming blitter rectangles). + * Helper functions to resize the canvas, and variables to remember + * its size for other functions (e.g. trimming blitter rectangles). */ static int canvas_w, canvas_h; + +/* Called when we resize as a result of changing puzzle settings */ static void resize(void) { int w, h; @@ -183,6 +207,26 @@ static void resize(void) canvas_h = h; } +/* Called from JS when the user uses the resize handle */ +void resize_puzzle(int w, int h) +{ + midend_size(me, &w, &h, TRUE); + if (canvas_w != w || canvas_h != h) { + js_canvas_set_size(w, h); + canvas_w = w; + canvas_h = h; + midend_force_redraw(me); + } +} + +/* Called from JS when the user uses the restore button */ +void restore_puzzle_size(int w, int h) +{ + midend_reset_tilesize(me); + resize(); + midend_force_redraw(me); +} + /* * HTML doesn't give us a default frontend colour of its own, so we * just make up a lightish grey ourselves. @@ -232,67 +276,68 @@ void mousemove(int x, int y, int buttons) /* * Keyboard handler called from JS. */ -void key(int keycode, int charcode, int shift, int ctrl) +void key(int keycode, int charcode, const char *key, const char *chr, + int shift, int ctrl) { int keyevent = -1; - if (charcode != 0) { - keyevent = charcode & (ctrl ? 0x1F : 0xFF); - } else { - switch (keycode) { - case 8: - keyevent = '\177'; /* backspace */ - break; - case 13: - keyevent = 13; /* return */ - break; - case 37: - keyevent = CURSOR_LEFT; - break; - case 38: - keyevent = CURSOR_UP; - break; - case 39: - keyevent = CURSOR_RIGHT; - break; - case 40: - keyevent = CURSOR_DOWN; - break; - /* - * We interpret Home, End, PgUp and PgDn as numeric keypad - * controls regardless of whether they're the ones on the - * numeric keypad (since we can't tell). The effect of - * this should only be that the non-numeric-pad versions - * of those keys generate directions in 8-way movement - * puzzles like Cube and Inertia. - */ - case 35: /* End */ - keyevent = MOD_NUM_KEYPAD | '1'; - break; - case 34: /* PgDn */ - keyevent = MOD_NUM_KEYPAD | '3'; - break; - case 36: /* Home */ - keyevent = MOD_NUM_KEYPAD | '7'; - break; - case 33: /* PgUp */ - keyevent = MOD_NUM_KEYPAD | '9'; - break; - case 96: case 97: case 98: case 99: case 100: - case 101: case 102: case 103: case 104: case 105: - keyevent = MOD_NUM_KEYPAD | ('0' + keycode - 96); - break; - default: - /* not a key we care about */ - return; - } + + if (!strnullcmp(key, "Backspace") || !strnullcmp(key, "Del") || + keycode == 8 || keycode == 46) { + keyevent = 127; /* Backspace / Delete */ + } else if (!strnullcmp(key, "Enter") || keycode == 13) { + keyevent = 13; /* return */ + } else if (!strnullcmp(key, "Left") || keycode == 37) { + keyevent = CURSOR_LEFT; + } else if (!strnullcmp(key, "Up") || keycode == 38) { + keyevent = CURSOR_UP; + } else if (!strnullcmp(key, "Right") || keycode == 39) { + keyevent = CURSOR_RIGHT; + } else if (!strnullcmp(key, "Down") || keycode == 40) { + keyevent = CURSOR_DOWN; + } else if (!strnullcmp(key, "End") || keycode == 35) { + /* + * We interpret Home, End, PgUp and PgDn as numeric keypad + * controls regardless of whether they're the ones on the + * numeric keypad (since we can't tell). The effect of + * this should only be that the non-numeric-pad versions + * of those keys generate directions in 8-way movement + * puzzles like Cube and Inertia. + */ + keyevent = MOD_NUM_KEYPAD | '1'; + } else if (!strnullcmp(key, "PageDown") || keycode==34) { + keyevent = MOD_NUM_KEYPAD | '3'; + } else if (!strnullcmp(key, "Home") || keycode==36) { + keyevent = MOD_NUM_KEYPAD | '7'; + } else if (!strnullcmp(key, "PageUp") || keycode==33) { + keyevent = MOD_NUM_KEYPAD | '9'; + } else if (shift && ctrl && (keycode & 0x1F) == 26) { + keyevent = UI_REDO; + } else if (chr && chr[0] && !chr[1]) { + keyevent = chr[0] & 0xFF; + } else if (keycode >= 96 && keycode < 106) { + keyevent = MOD_NUM_KEYPAD | ('0' + keycode - 96); + } else if (keycode >= 65 && keycode <= 90) { + keyevent = keycode + (shift ? 0 : 32); + } else if (keycode >= 48 && keycode <= 57) { + keyevent = keycode; + } else if (keycode == 32) { /* space / CURSOR_SELECT2 */ + keyevent = keycode; } - if (shift && keyevent >= 0x100) - keyevent |= MOD_SHFT; - if (ctrl && keyevent >= 0x100) - keyevent |= MOD_CTRL; - midend_process_key(me, 0, 0, keyevent); - update_undo_redo(); + if (keyevent >= 0) { + if (shift && (keyevent >= 0x100 && !IS_UI_FAKE_KEY(keyevent))) + keyevent |= MOD_SHFT; + + if (ctrl && !IS_UI_FAKE_KEY(keyevent)) { + if (keyevent >= 0x100) + keyevent |= MOD_CTRL; + else + keyevent &= 0x1F; + } + + midend_process_key(me, 0, 0, keyevent); + update_undo_redo(); + } } /* @@ -310,10 +355,10 @@ static void update_permalinks(void) } /* - * Callback from the midend if Mines supersedes its game description, - * so we can update the permalinks. + * Callback from the midend when the game ids change, so we can update + * the permalinks. */ -static void desc_changed(void *ignored) +static void ids_changed(void *ignored) { update_permalinks(); } @@ -416,27 +461,30 @@ static void js_blitter_free(void *handle, blitter *bl) 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. */ - if (*x < 0) { - *w += *x; - *x = 0; - } - if (*y < 0) { - *h += *y; - *y = 0; - } - if (*w > canvas_w - *x) - *w = canvas_w - *x; - if (*h > canvas_h - *y) - *h = canvas_h - *y; - if (*w < 0) - *w = 0; - if (*h < 0) - *h = 0; + /* Transform from x,y,w,h form into coordinates of all edges */ + x0 = *x; + y0 = *y; + x1 = *x + *w; + y1 = *y + *h; + + /* Clip each coordinate at both extremes of the canvas */ + x0 = (x0 < 0 ? 0 : x0 > canvas_w ? canvas_w : x0); + x1 = (x1 < 0 ? 0 : x1 > canvas_w ? canvas_w : x1); + y0 = (y0 < 0 ? 0 : y0 > canvas_h ? canvas_h : y0); + y1 = (y1 < 0 ? 0 : y1 > canvas_h ? canvas_h : y1); + + /* Transform back into x,y,w,h to return */ + *x = x0; + *y = y0; + *w = x1 - x0; + *h = y1 - y0; } static void js_blitter_save(void *handle, blitter *bl, int x, int y) @@ -458,7 +506,8 @@ static void js_blitter_load(void *handle, blitter *bl, int x, int y) static void js_draw_update(void *handle, int x, int y, int w, int h) { trim_rect(&x, &y, &w, &h); - js_canvas_draw_update(x, y, w, h); + if (w > 0 && h > 0) + js_canvas_draw_update(x, y, w, h); } static void js_end_draw(void *handle) @@ -503,7 +552,31 @@ const struct drawing_api js_drawing = { * Presets and game-configuration dialog support. */ static game_params **presets; -static int custom_preset; +static int npresets; +int have_presets_dropdown; + +void populate_js_preset_menu(int menuid, struct preset_menu *menu) +{ + int i; + for (i = 0; i < menu->n_entries; i++) { + struct preset_menu_entry *entry = &menu->entries[i]; + if (entry->params) { + presets[entry->id] = entry->params; + js_add_preset(menuid, entry->title, entry->id); + } else { + int js_submenu = js_add_preset_submenu(menuid, entry->title); + populate_js_preset_menu(js_submenu, entry->submenu); + } + } +} + +void select_appropriate_preset(void) +{ + if (have_presets_dropdown) { + int preset = midend_which_preset(me); + js_select_preset(preset < 0 ? -1 : preset); + } +} static config_item *cfg = NULL; static int cfg_which; @@ -578,13 +651,10 @@ static void cfg_end(int use_results) * New settings are fine; start a new game and close the * dialog. */ - int preset = midend_which_preset(me); - js_select_preset(preset < 0 ? custom_preset : preset); - + select_appropriate_preset(); midend_new_game(me); resize(); midend_redraw(me); - update_permalinks(); free_cfg(cfg); js_dialog_cleanup(); } @@ -602,8 +672,7 @@ static void cfg_end(int use_results) * js_add_preset in emcclib.js - so you won't even be able to * select Custom without a faffy workaround.) */ - int preset = midend_which_preset(me); - js_select_preset(preset < 0 ? custom_preset : preset); + select_appropriate_preset(); free_cfg(cfg); js_dialog_cleanup(); @@ -626,7 +695,7 @@ void command(int n) case 2: /* game parameter dropdown changed */ { int i = js_get_selected_preset(); - if (i == custom_preset) { + if (i < 0) { /* * The user selected 'Custom', so launch the config * box. @@ -638,13 +707,14 @@ void command(int n) * The user selected a preset, so just switch straight * to that. */ + assert(i < npresets); midend_set_params(me, presets[i]); midend_new_game(me); - update_permalinks(); resize(); midend_redraw(me); update_undo_redo(); js_focus_canvas(); + select_appropriate_preset(); } } break; @@ -657,7 +727,7 @@ void command(int n) update_undo_redo(); break; case 5: /* New Game */ - midend_process_key(me, 0, 0, 'n'); + midend_process_key(me, 0, 0, UI_NEWGAME); update_undo_redo(); js_focus_canvas(); break; @@ -667,12 +737,12 @@ void command(int n) js_focus_canvas(); break; case 7: /* Undo */ - midend_process_key(me, 0, 0, 'u'); + midend_process_key(me, 0, 0, UI_UNDO); update_undo_redo(); js_focus_canvas(); break; case 8: /* Redo */ - midend_process_key(me, 0, 0, 'r'); + midend_process_key(me, 0, 0, UI_REDO); update_undo_redo(); js_focus_canvas(); break; @@ -688,6 +758,83 @@ void command(int n) } } +/* ---------------------------------------------------------------------- + * Called from JS to prepare a save-game file, and free one after it's + * been used. + */ + +struct savefile_write_ctx { + char *buffer; + size_t pos; +}; + +static void savefile_write(void *vctx, void *buf, int len) +{ + struct savefile_write_ctx *ctx = (struct savefile_write_ctx *)vctx; + if (ctx->buffer) + memcpy(ctx->buffer + ctx->pos, buf, len); + ctx->pos += len; +} + +char *get_save_file(void) +{ + struct savefile_write_ctx ctx; + size_t size; + + /* First pass, to count up the size */ + ctx.buffer = NULL; + ctx.pos = 0; + midend_serialise(me, savefile_write, &ctx); + size = ctx.pos; + + /* Second pass, to actually write out the data */ + ctx.buffer = snewn(size, char); + ctx.pos = 0; + midend_serialise(me, savefile_write, &ctx); + assert(ctx.pos == size); + + return ctx.buffer; +} + +void free_save_file(char *buffer) +{ + sfree(buffer); +} + +struct savefile_read_ctx { + const char *buffer; + int len_remaining; +}; + +static int savefile_read(void *vctx, void *buf, int len) +{ + struct savefile_read_ctx *ctx = (struct savefile_read_ctx *)vctx; + if (ctx->len_remaining < len) + return FALSE; + memcpy(buf, ctx->buffer, len); + ctx->len_remaining -= len; + ctx->buffer += len; + return TRUE; +} + +void load_game(const char *buffer, int len) +{ + struct savefile_read_ctx ctx; + const char *err; + + ctx.buffer = buffer; + ctx.len_remaining = len; + err = midend_deserialise(me, savefile_read, &ctx); + + if (err) { + js_error_box(err); + } else { + select_appropriate_preset(); + resize(); + midend_redraw(me); + } +} + /* ---------------------------------------------------------------------- * Setup function called at page load time. It's called main() because * that's the most convenient thing in Emscripten, but it's not main() @@ -733,21 +880,28 @@ int main(int argc, char **argv) /* * Set up the game-type dropdown with presets and/or the Custom - * option. We remember the index of the Custom option (as - * custom_preset) so that we can easily treat it specially when - * it's selected. + * option. */ - custom_preset = midend_num_presets(me); - presets = snewn(custom_preset, game_params *); - for (i = 0; i < custom_preset; i++) { - char *name; - midend_fetch_preset(me, i, &name, &presets[i]); - js_add_preset(name); + { + struct preset_menu *menu = midend_get_presets(me, &npresets); + presets = snewn(npresets, game_params *); + for (i = 0; i < npresets; i++) + presets[i] = NULL; + + populate_js_preset_menu(0, menu); + + if (thegame.can_configure) + js_add_preset(0, "Custom", -1); + + have_presets_dropdown = TRUE; + + /* + * Now ensure the appropriate element of the presets menu + * starts off selected, in case it isn't the first one in the + * list (e.g. Slant). + */ + select_appropriate_preset(); } - if (thegame.can_configure) - js_add_preset(NULL); /* the 'Custom' entry in the dropdown */ - else if (custom_preset == 0) - js_remove_type_dropdown(); /* * Remove the Solve button if the game doesn't support it. @@ -771,11 +925,11 @@ int main(int argc, char **argv) } /* - * Request notification if a puzzle (hopefully only ever Mines) - * supersedes its game description, so that we can proactively - * update the permalink. + * Request notification when the game ids change (e.g. if the user + * presses 'n', and also when Mines supersedes its game + * description), so that we can proactively update the permalink. */ - midend_request_desc_changes(me, desc_changed, NULL); + midend_request_id_changes(me, ids_changed, NULL); /* * Draw the puzzle's initial state, and set up the permalinks and