ref: 92ac45fe240b2063455b0b01dedc6ef6996f18af
dir: /emcc.c/
/* * emcc.c: the C component of an Emscripten-based web/Javascript front * end for Puzzles. * * The Javascript parts of this system live in emcclib.js and * 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. */ /* * Further thoughts on possible enhancements: * * - I should think about whether these webified puzzles can support * touchscreen-based tablet browsers. * * - think about making use of localStorage. It might be useful to * let the user save games into there as an alternative to disk * files - disk files are all very well for getting the save right * out of your browser to (e.g.) email to me as a bug report, but * for just resuming a game you were in the middle of, you'd * probably rather have a nice simple 'quick save' and 'quick load' * button pair. Also, that might be a useful place to store * preferences, if I ever get round to writing a preferences UI. * * - this is a downright silly idea, but it does occur to me that if * I were to write a PDF output driver for the Puzzles printing * API, then I might be able to implement a sort of 'printing' * feature in this front end, using data: URIs again. (Ask the user * exactly what they want printed, then construct an appropriate * PDF and embed it in a gigantic data: URI. Then they can print * that using whatever they normally use to print PDFs!) */ #include <assert.h> #include <stdio.h> #include <string.h> #include <stdarg.h> #include "puzzles.h" /* * Extern references to Javascript functions provided in emcclib.js. */ extern void js_init_puzzle(void); extern void js_post_init(void); 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(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_default_colour(float *output); extern void js_set_colour(int colour_number, const char *colour_string); extern void js_get_date_64(unsigned *p); extern void js_update_permalinks(const char *desc, const char *seed); extern void js_enable_undo_redo(bool undo, bool redo); extern void js_update_key_labels(const char *lsk, const char *csk); extern void js_activate_timer(void); extern void js_deactivate_timer(void); extern void js_canvas_start_draw(void); extern void js_canvas_draw_update(int x, int y, int w, int h); extern void js_canvas_end_draw(void); extern void js_canvas_draw_rect(int x, int y, int w, int h, int colour); extern void js_canvas_clip_rect(int x, int y, int w, int h); extern void js_canvas_unclip(void); extern void js_canvas_draw_line(float x1, float y1, float x2, float y2, int width, int colour); extern void js_canvas_draw_poly(const int *points, int npoints, int fillcolour, int outlinecolour); extern void js_canvas_draw_circle(int x, int y, int r, int fillcolour, int outlinecolour); extern int js_canvas_find_font_midpoint(int height, bool monospaced); extern void js_canvas_draw_text(int x, int y, int halign, int colour, int height, bool monospaced, const char *text); extern int js_canvas_new_blitter(int w, int h); extern void js_canvas_free_blitter(int id); extern void js_canvas_copy_to_blitter(int id, int x, int y, int w, int h); extern void js_canvas_copy_from_blitter(int id, int x, int y, int w, int h); extern void js_canvas_remove_statusbar(void); extern void js_canvas_set_statusbar(const char *text); extern bool js_canvas_get_preferred_size(int *wp, int *hp); extern void js_canvas_set_size(int w, int h); extern double js_get_device_pixel_ratio(void); extern void js_dialog_init(const char *title); extern void js_dialog_string(int i, const char *title, const char *initvalue); extern void js_dialog_choices(int i, const char *title, const char *choicelist, int initvalue); extern void js_dialog_boolean(int i, const char *title, bool initvalue); extern void js_dialog_launch(void); extern void js_dialog_cleanup(void); extern void js_focus_canvas(void); extern bool js_savefile_read(void *buf, int len); extern void js_save_prefs(const char *); extern void js_load_prefs(midend *); /* * These functions are called from JavaScript, so their prototypes * need to be kept in sync with emccpre.js. */ bool mouseup(int x, int y, int button); bool mousedown(int x, int y, int button); bool mousemove(int x, int y, int buttons); bool key(int keycode, const char *key, const char *chr, int location, bool shift, bool ctrl); void timer_callback(double tplus); void command(int n); char *get_text_format(void); void free_save_file(char *buffer); char *get_save_file(void); void free_save_file(char *buffer); void load_game(void); void dlg_return_sval(int index, const char *val); void dlg_return_ival(int index, int val); void resize_puzzle(int w, int h); void restore_puzzle_size(int w, int h); void rescale_puzzle(void); /* * Internal forward references. */ static void save_prefs(midend *me); /* * Call JS to get the date, and use that to initialise our random * number generator to invent the first game seed. */ void get_random_seed(void **randseed, int *randseedsize) { unsigned *ret = snewn(2, unsigned); js_get_date_64(ret); *randseed = ret; *randseedsize = 2*sizeof(unsigned); } /* * Fatal error, called in cases of complete despair such as when * malloc() has returned NULL. */ void fatal(const char *fmt, ...) { char buf[512]; va_list ap; strcpy(buf, "puzzle fatal error: "); va_start(ap, fmt); vsnprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), fmt, ap); va_end(ap); js_error_box(buf); } #ifdef DEBUGGING void debug_printf(const char *fmt, ...) { char buf[512]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); js_debug(buf); } #endif /* * Helper function that makes it easy to test strings that might be * NULL. */ static 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); } /* * The global midend object. */ static midend *me; /* ---------------------------------------------------------------------- * Timing functions. */ static bool timer_active = false; void deactivate_timer(frontend *fe) { js_deactivate_timer(); timer_active = false; } void activate_timer(frontend *fe) { if (!timer_active) { js_activate_timer(); timer_active = true; } } void timer_callback(double tplus) { if (timer_active) midend_timer(me, tplus); } /* ---------------------------------------------------------------------- * 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 * or device pixel ratio. */ static void resize(void) { int w, h; bool user; w = h = INT_MAX; user = js_canvas_get_preferred_size(&w, &h); midend_size(me, &w, &h, user, js_get_device_pixel_ratio()); js_canvas_set_size(w, h); canvas_w = w; canvas_h = h; } /* Called from JS when the device pixel ratio changes */ void rescale_puzzle(void) { resize(); midend_force_redraw(me); } /* Called from JS when the user uses the resize handle */ void resize_puzzle(int w, int h) { midend_size(me, &w, &h, true, js_get_device_pixel_ratio()); 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); } /* * Try to extract a background colour from the canvas's CSS. In case * it doesn't have a usable one, make up a lightish grey ourselves. */ void frontend_default_colour(frontend *fe, float *output) { output[0] = output[1] = output[2] = 0.9F; js_default_colour(output); } /* * Helper function called from all over the place to ensure the undo * and redo buttons get properly enabled and disabled after every move * or undo or new-game event. */ static void post_move(void) { js_enable_undo_redo(midend_can_undo(me), midend_can_redo(me)); js_update_key_labels(midend_current_key_label(me, CURSOR_SELECT2), midend_current_key_label(me, CURSOR_SELECT)); } /* * Mouse event handlers called from JS. */ bool mousedown(int x, int y, int button) { bool handled; button = (button == 0 ? LEFT_BUTTON : button == 1 ? MIDDLE_BUTTON : RIGHT_BUTTON); handled = midend_process_key(me, x, y, button) != PKR_UNUSED; post_move(); return handled; } bool mouseup(int x, int y, int button) { bool handled; button = (button == 0 ? LEFT_RELEASE : button == 1 ? MIDDLE_RELEASE : RIGHT_RELEASE); handled = midend_process_key(me, x, y, button) != PKR_UNUSED; post_move(); return handled; } bool mousemove(int x, int y, int buttons) { int button = (buttons & 2 ? MIDDLE_DRAG : buttons & 4 ? RIGHT_DRAG : LEFT_DRAG); bool handled; handled = midend_process_key(me, x, y, button) != PKR_UNUSED; post_move(); return handled; } /* * Keyboard handler called from JS. Returns true if the key was * handled and hence the keydown event should be cancelled. */ bool key(int keycode, const char *key, const char *chr, int location, bool shift, bool ctrl) { /* Key location constants from JavaScript. */ #define DOM_KEY_LOCATION_STANDARD 0 #define DOM_KEY_LOCATION_LEFT 1 #define DOM_KEY_LOCATION_RIGHT 2 #define DOM_KEY_LOCATION_NUMPAD 3 int keyevent = -1; int process_key_result; if (!strnullcmp(key, "Backspace") || !strnullcmp(key, "Delete") || !strnullcmp(key, "Del")) keyevent = 127; /* Backspace / Delete */ else if (!strnullcmp(key, "Enter")) keyevent = 13; /* return */ else if (!strnullcmp(key, "Spacebar")) keyevent = ' '; else if (!strnullcmp(key, "Escape")) keyevent = 27; else if (!strnullcmp(key, "ArrowLeft") || !strnullcmp(key, "Left")) keyevent = CURSOR_LEFT; else if (!strnullcmp(key, "ArrowUp") || !strnullcmp(key, "Up")) keyevent = CURSOR_UP; else if (!strnullcmp(key, "ArrowRight") || !strnullcmp(key, "Right")) keyevent = CURSOR_RIGHT; else if (!strnullcmp(key, "ArrowDown") || !strnullcmp(key, "Down")) keyevent = CURSOR_DOWN; else if (!strnullcmp(key, "SoftLeft")) /* Left soft key on KaiOS. */ keyevent = CURSOR_SELECT2; else if (!strnullcmp(key, "End")) /* * 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")) keyevent = MOD_NUM_KEYPAD | '3'; else if (!strnullcmp(key, "Home")) keyevent = MOD_NUM_KEYPAD | '7'; else if (!strnullcmp(key, "PageUp")) keyevent = MOD_NUM_KEYPAD | '9'; else if (shift && ctrl && (!strnullcmp(key, "Z") || !strnullcmp(key, "z"))) keyevent = UI_REDO; else if (key && (unsigned char)key[0] < 0x80 && key[1] == '\0') /* Key generating a single ASCII character. */ keyevent = key[0]; /* * In modern browsers (since about 2017), all keys that Puzzles * cares about should be matched by one of the clauses above. The * code below that checks keycode and chr should be relavent only * in older browsers. */ else if (keycode == 8 || keycode == 46) keyevent = 127; /* Backspace / Delete */ else if (keycode == 13) keyevent = 13; /* return */ else if (keycode == 37) keyevent = CURSOR_LEFT; else if (keycode == 38) keyevent = CURSOR_UP; else if (keycode == 39) keyevent = CURSOR_RIGHT; else if (keycode == 40) keyevent = CURSOR_DOWN; else if (keycode == 35) keyevent = MOD_NUM_KEYPAD | '1'; else if (keycode == 34) keyevent = MOD_NUM_KEYPAD | '3'; else if (keycode == 36) keyevent = MOD_NUM_KEYPAD | '7'; else if (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 (keyevent >= 0) { if (shift) keyevent |= MOD_SHFT; if (ctrl) keyevent |= MOD_CTRL; if (location == DOM_KEY_LOCATION_NUMPAD) keyevent |= MOD_NUM_KEYPAD; process_key_result = midend_process_key(me, 0, 0, keyevent); post_move(); /* * Treat Backspace specially because that's expected on KaiOS. * https://developer.kaiostech.com/docs/design-guide/key */ if (process_key_result == PKR_NO_EFFECT && !strnullcmp(key, "Backspace")) return false; return process_key_result != PKR_UNUSED; } return false; /* Event not handled, because we don't even recognise it. */ } /* * Helper function called from several places to update the permalinks * whenever a new game is created. */ static void update_permalinks(void) { char *desc, *seed; desc = midend_get_game_id(me); seed = midend_get_random_seed(me); js_update_permalinks(desc, seed); sfree(desc); sfree(seed); } /* * Callback from the midend when the game ids change, so we can update * the permalinks. */ static void ids_changed(void *ignored) { update_permalinks(); } /* ---------------------------------------------------------------------- * Implementation of the drawing API by calling Javascript canvas * drawing functions. (Well, half of it; the other half is on the JS * side.) */ static void js_start_draw(void *handle) { js_canvas_start_draw(); } static void js_clip(void *handle, int x, int y, int w, int h) { js_canvas_clip_rect(x, y, w, h); } static void js_unclip(void *handle) { js_canvas_unclip(); } static void js_draw_text(void *handle, int x, int y, int fonttype, int fontsize, int align, int colour, const char *text) { int halign; if (align & ALIGN_VCENTRE) y += js_canvas_find_font_midpoint(fontsize, fonttype == FONT_FIXED); if (align & ALIGN_HCENTRE) halign = 1; else if (align & ALIGN_HRIGHT) halign = 2; else halign = 0; js_canvas_draw_text(x, y, halign, colour, fontsize, fonttype == FONT_FIXED, text); } static void js_draw_rect(void *handle, int x, int y, int w, int h, int colour) { js_canvas_draw_rect(x, y, w, h, colour); } static void js_draw_line(void *handle, int x1, int y1, int x2, int y2, int colour) { js_canvas_draw_line(x1, y1, x2, y2, 1, colour); } static void js_draw_thick_line(void *handle, float thickness, float x1, float y1, float x2, float y2, int colour) { js_canvas_draw_line(x1, y1, x2, y2, thickness, colour); } static void js_draw_poly(void *handle, const int *coords, int npoints, int fillcolour, int outlinecolour) { js_canvas_draw_poly(coords, npoints, fillcolour, outlinecolour); } static void js_draw_circle(void *handle, int cx, int cy, int radius, int fillcolour, int outlinecolour) { js_canvas_draw_circle(cx, cy, radius, fillcolour, outlinecolour); } struct blitter { int id; /* allocated on the js side */ int w, h; /* easier to retain here */ }; static blitter *js_blitter_new(void *handle, int w, int h) { blitter *bl = snew(blitter); bl->w = w; bl->h = h; bl->id = js_canvas_new_blitter(w, h); return bl; } static void js_blitter_free(void *handle, blitter *bl) { js_canvas_free_blitter(bl->id); sfree(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. */ /* 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) { int w = bl->w, h = bl->h; trim_rect(&x, &y, &w, &h); if (w > 0 && h > 0) js_canvas_copy_to_blitter(bl->id, x, y, w, h); } static void js_blitter_load(void *handle, blitter *bl, int x, int y) { int w = bl->w, h = bl->h; trim_rect(&x, &y, &w, &h); if (w > 0 && h > 0) js_canvas_copy_from_blitter(bl->id, x, y, w, h); } static void js_draw_update(void *handle, int x, int y, int w, int h) { trim_rect(&x, &y, &w, &h); if (w > 0 && h > 0) js_canvas_draw_update(x, y, w, h); } static void js_end_draw(void *handle) { js_canvas_end_draw(); } static void js_status_bar(void *handle, const char *text) { js_canvas_set_statusbar(text); } static char *js_text_fallback(void *handle, const char *const *strings, int nstrings) { return dupstr(strings[0]); /* Emscripten has no trouble with UTF-8 */ } static const struct drawing_api js_drawing = { js_draw_text, js_draw_rect, js_draw_line, js_draw_poly, js_draw_circle, js_draw_update, js_clip, js_unclip, js_start_draw, js_end_draw, js_status_bar, js_blitter_new, js_blitter_free, js_blitter_save, js_blitter_load, NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */ NULL, NULL, /* line_width, line_dotted */ js_text_fallback, js_draw_thick_line, }; /* ---------------------------------------------------------------------- * Presets and game-configuration dialog support. */ static game_params **presets; static int npresets; static bool have_presets_dropdown; static 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); } } } static 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; /* * Set up a dialog box. This is pretty easy on the C side; most of the * work is done in JS. */ static void cfg_start(int which) { char *title; int i; cfg = midend_get_config(me, which, &title); cfg_which = which; js_dialog_init(title); sfree(title); for (i = 0; cfg[i].type != C_END; i++) { switch (cfg[i].type) { case C_STRING: js_dialog_string(i, cfg[i].name, cfg[i].u.string.sval); break; case C_BOOLEAN: js_dialog_boolean(i, cfg[i].name, cfg[i].u.boolean.bval); break; case C_CHOICES: js_dialog_choices(i, cfg[i].name, cfg[i].u.choices.choicenames, cfg[i].u.choices.selected); break; } } js_dialog_launch(); } /* * Callbacks from JS when the OK button is clicked, to return the * final state of each control. */ void dlg_return_sval(int index, const char *val) { config_item *i = cfg + index; switch (i->type) { case C_STRING: sfree(i->u.string.sval); i->u.string.sval = dupstr(val); break; default: assert(0 && "Bad type for return_sval"); } } void dlg_return_ival(int index, int val) { config_item *i = cfg + index; switch (i->type) { case C_BOOLEAN: i->u.boolean.bval = val; break; case C_CHOICES: i->u.choices.selected = val; break; default: assert(0 && "Bad type for return_ival"); } } /* * Called when the user clicks OK or Cancel. use_results will be true * or false respectively, in those cases. We terminate the dialog box, * unless the user selected an invalid combination of parameters. */ static void cfg_end(bool use_results) { if (use_results) { /* * User hit OK. */ const char *err = midend_set_config(me, cfg_which, cfg); if (err) { /* * The settings were unacceptable, so leave the config box * open for the user to adjust them and try again. */ js_error_box(err); } else if (cfg_which == CFG_PREFS) { /* * Acceptable settings for user preferences: enact them * without blowing away the current game. */ resize(); midend_redraw(me); free_cfg(cfg); js_dialog_cleanup(); save_prefs(me); } else { /* * Acceptable settings for the remaining configuration * types: start a new game and close the dialog. */ select_appropriate_preset(); midend_new_game(me); resize(); midend_redraw(me); free_cfg(cfg); js_dialog_cleanup(); } } else { /* * User hit Cancel. Close the dialog, but also we must still * reselect the right element of the dropdown list. * * (Because: imagine you have a preset selected, and then you * select Custom from the list, but change your mind and hit * Esc. The Custom option will now still be selected in the * list, whereas obviously it should show the preset you still * _actually_ have selected.) */ select_appropriate_preset(); free_cfg(cfg); js_dialog_cleanup(); } } /* ---------------------------------------------------------------------- * Called from JS when a command is given to the puzzle by clicking a * button or control of some sort. */ void command(int n) { switch (n) { case 0: /* specific game ID */ cfg_start(CFG_DESC); break; case 1: /* random game seed */ cfg_start(CFG_SEED); break; case 2: /* game parameter dropdown changed */ { int i = js_get_selected_preset(); if (i < 0) { /* * The user selected 'Custom', so launch the config * box. */ if (thegame.can_configure) /* (double-check just in case) */ cfg_start(CFG_SETTINGS); } else { /* * The user selected a preset, so just switch straight * to that. */ assert(i < npresets); midend_set_params(me, presets[i]); midend_new_game(me); resize(); midend_redraw(me); post_move(); js_focus_canvas(); select_appropriate_preset(); } } break; case 3: /* OK clicked in a config box */ cfg_end(true); post_move(); break; case 4: /* Cancel clicked in a config box */ cfg_end(false); post_move(); break; case 5: /* New Game */ midend_process_key(me, 0, 0, UI_NEWGAME); post_move(); js_focus_canvas(); break; case 6: /* Restart */ midend_restart_game(me); post_move(); js_focus_canvas(); break; case 7: /* Undo */ midend_process_key(me, 0, 0, UI_UNDO); post_move(); js_focus_canvas(); break; case 8: /* Redo */ midend_process_key(me, 0, 0, UI_REDO); post_move(); js_focus_canvas(); break; case 9: /* Solve */ if (thegame.can_solve) { const char *msg = midend_solve(me); if (msg) js_error_box(msg); } post_move(); js_focus_canvas(); break; case 10: /* user preferences */ cfg_start(CFG_PREFS); break; } } char *get_text_format(void) { return midend_text_format(me); } void free_text_format(char *buffer) { sfree(buffer); } /* ---------------------------------------------------------------------- * 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, const 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. We have to put a * terminating \0 on the end (which we expect never to show up in * the actual serialisation format - it's text, not binary) so * that the Javascript side can easily find out the length. */ ctx.buffer = snewn(size+1, char); ctx.pos = 0; midend_serialise(me, savefile_write, &ctx); assert(ctx.pos == size); ctx.buffer[ctx.pos] = '\0'; return ctx.buffer; } void free_save_file(char *buffer) { sfree(buffer); } static bool savefile_read(void *vctx, void *buf, int len) { return js_savefile_read(buf, len); } void load_game(void) { const char *err; /* * savefile_read_callback in JavaScript was set up by our caller * as a closure that knows what file we're loading. */ err = midend_deserialise(me, savefile_read, NULL); if (err) { js_error_box(err); } else { select_appropriate_preset(); resize(); midend_redraw(me); update_permalinks(); post_move(); } } /* ---------------------------------------------------------------------- * Functions to load and save preferences, calling out to JS to access * the appropriate localStorage slot. */ static void save_prefs(midend *me) { struct savefile_write_ctx ctx; size_t size; /* First pass, to count up the size */ ctx.buffer = NULL; ctx.pos = 0; midend_save_prefs(me, savefile_write, &ctx); size = ctx.pos; /* Second pass, to actually write out the data. As with * get_save_file, we append a terminating \0. */ ctx.buffer = snewn(size+1, char); ctx.pos = 0; midend_save_prefs(me, savefile_write, &ctx); assert(ctx.pos == size); ctx.buffer[ctx.pos] = '\0'; js_save_prefs(ctx.buffer); sfree(ctx.buffer); } struct prefs_read_ctx { const char *buffer; size_t pos, len; }; static bool prefs_read(void *vctx, void *buf, int len) { struct prefs_read_ctx *ctx = (struct prefs_read_ctx *)vctx; if (len < 0) return false; if (ctx->len - ctx->pos < len) return false; memcpy(buf, ctx->buffer + ctx->pos, len); ctx->pos += len; return true; } void prefs_load_callback(midend *me, const char *prefs) { struct prefs_read_ctx ctx; ctx.buffer = prefs; ctx.len = strlen(prefs); ctx.pos = 0; midend_load_prefs(me, prefs_read, &ctx); } /* ---------------------------------------------------------------------- * 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() * in the usual sense of bounding the program's entire execution. * Instead, this function returns once the initial puzzle is set up * and working, and everything thereafter happens by means of JS event * handlers sending us callbacks. */ int main(int argc, char **argv) { const char *param_err; float *colours; int i, ncolours; /* * Initialise JavaScript event handlers. */ js_init_puzzle(); /* * Instantiate a midend. */ me = midend_new(NULL, &thegame, &js_drawing, NULL); js_load_prefs(me); /* * Chuck in the HTML fragment ID if we have one (trimming the * leading # off the front first). If that's invalid, we retain * the error message and will display it at the end, after setting * up a random puzzle as usual. */ if (argc > 1 && argv[1][0] == '#' && argv[1][1] != '\0') param_err = midend_game_id(me, argv[1] + 1); else param_err = NULL; /* * Create either a random game or the specified one, and set the * canvas size appropriately. */ midend_new_game(me); resize(); /* * Remove the status bar, if not needed. */ if (!midend_wants_statusbar(me)) js_canvas_remove_statusbar(); /* * Set up the game-type dropdown with presets and/or the Custom * option. */ { struct preset_menu *menu = midend_get_presets(me, &npresets); bool may_configure = false; presets = snewn(npresets, game_params *); for (i = 0; i < npresets; i++) presets[i] = NULL; populate_js_preset_menu(0, menu); /* * Crude hack to allow the "Custom..." item to be hidden on * KaiOS, where dialogs don't yet work. */ if (thegame.can_configure && getenv_bool("PUZZLES_ALLOW_CUSTOM", true)) may_configure = true; if (may_configure) js_add_preset(0, "Custom...", -1); have_presets_dropdown = npresets > 1 || may_configure; if (have_presets_dropdown) /* * 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(); else js_remove_type_dropdown(); } /* * Remove the Solve button if the game doesn't support it. */ if (!thegame.can_solve) js_remove_solve_button(); /* * Retrieve the game's colours, and convert them into #abcdef type * hex ID strings. */ colours = midend_colours(me, &ncolours); for (i = 0; i < ncolours; i++) { char col[40]; sprintf(col, "#%02x%02x%02x", (unsigned)(0.5F + 255 * colours[i*3+0]), (unsigned)(0.5F + 255 * colours[i*3+1]), (unsigned)(0.5F + 255 * colours[i*3+2])); js_set_colour(i, col); } /* * 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_id_changes(me, ids_changed, NULL); /* * Draw the puzzle's initial state, and set up the permalinks and * undo/redo greying out. */ midend_redraw(me); update_permalinks(); post_move(); /* * If we were given an erroneous game ID in argv[1], now's the * time to put up the error box about it, after we've fully set up * a random puzzle. Then when the user clicks 'ok', we have a * puzzle for them. */ if (param_err) js_error_box(param_err); /* * Reveal the puzzle! */ js_post_init(); /* * Done. Return to JS, and await callbacks! */ return 0; }