2 * emcc.c: the C component of an Emscripten-based web/Javascript front
5 * The Javascript parts of this system live in emcclib.js and
6 * emccpre.js. It also depends on being run in the context of a web
7 * page containing an appropriate collection of bits and pieces (a
8 * canvas, some buttons and links etc), which is generated for each
9 * puzzle by the script html/jspage.pl.
13 * Further thoughts on possible enhancements:
15 * - I think it might be feasible to have these JS puzzles permit
16 * loading and saving games in disk files. Saving would be done by
17 * constructing a data: URI encapsulating the save file, and then
18 * telling the browser to visit that URI with the effect that it
19 * would naturally pop up a 'where would you like to save this'
20 * dialog box. Loading, more or less similarly, might be feasible
21 * by using the DOM File API to ask the user to select a file and
22 * permit us to see its contents.
24 * - I should think about whether these webified puzzles can support
25 * touchscreen-based tablet browsers (assuming there are any that
26 * can cope with the reasonably modern JS and run it fast enough to
29 * - think about making use of localStorage. It might be useful to
30 * let the user save games into there as an alternative to disk
31 * files - disk files are all very well for getting the save right
32 * out of your browser to (e.g.) email to me as a bug report, but
33 * for just resuming a game you were in the middle of, you'd
34 * probably rather have a nice simple 'quick save' and 'quick load'
35 * button pair. Also, that might be a useful place to store
36 * preferences, if I ever get round to writing a preferences UI.
38 * - some CSS to make the button bar and configuration dialogs a
39 * little less ugly would probably not go amiss.
41 * - this is a downright silly idea, but it does occur to me that if
42 * I were to write a PDF output driver for the Puzzles printing
43 * API, then I might be able to implement a sort of 'printing'
44 * feature in this front end, using data: URIs again. (Ask the user
45 * exactly what they want printed, then construct an appropriate
46 * PDF and embed it in a gigantic data: URI. Then they can print
47 * that using whatever they normally use to print PDFs!)
57 * Extern references to Javascript functions provided in emcclib.js.
59 extern void js_debug(const char *);
60 extern void js_error_box(const char *message);
61 extern void js_remove_type_dropdown(void);
62 extern void js_remove_solve_button(void);
63 extern void js_add_preset(const char *name);
64 extern int js_get_selected_preset(void);
65 extern void js_select_preset(int n);
66 extern void js_get_date_64(unsigned *p);
67 extern void js_update_permalinks(const char *desc, const char *seed);
68 extern void js_enable_undo_redo(int undo, int redo);
69 extern void js_activate_timer();
70 extern void js_deactivate_timer();
71 extern void js_canvas_start_draw(void);
72 extern void js_canvas_draw_update(int x, int y, int w, int h);
73 extern void js_canvas_end_draw(void);
74 extern void js_canvas_draw_rect(int x, int y, int w, int h,
76 extern void js_canvas_clip_rect(int x, int y, int w, int h);
77 extern void js_canvas_unclip(void);
78 extern void js_canvas_draw_line(float x1, float y1, float x2, float y2,
79 int width, const char *colour);
80 extern void js_canvas_draw_poly(int *points, int npoints,
81 const char *fillcolour,
82 const char *outlinecolour);
83 extern void js_canvas_draw_circle(int x, int y, int r,
84 const char *fillcolour,
85 const char *outlinecolour);
86 extern int js_canvas_find_font_midpoint(int height, const char *fontptr);
87 extern void js_canvas_draw_text(int x, int y, int halign,
88 const char *colptr, const char *fontptr,
90 extern int js_canvas_new_blitter(int w, int h);
91 extern void js_canvas_free_blitter(int id);
92 extern void js_canvas_copy_to_blitter(int id, int x, int y, int w, int h);
93 extern void js_canvas_copy_from_blitter(int id, int x, int y, int w, int h);
94 extern void js_canvas_make_statusbar(void);
95 extern void js_canvas_set_statusbar(const char *text);
96 extern void js_canvas_set_size(int w, int h);
98 extern void js_dialog_init(const char *title);
99 extern void js_dialog_string(int i, const char *title, const char *initvalue);
100 extern void js_dialog_choices(int i, const char *title, const char *choicelist,
102 extern void js_dialog_boolean(int i, const char *title, int initvalue);
103 extern void js_dialog_launch(void);
104 extern void js_dialog_cleanup(void);
105 extern void js_focus_canvas(void);
108 * Call JS to get the date, and use that to initialise our random
109 * number generator to invent the first game seed.
111 void get_random_seed(void **randseed, int *randseedsize)
113 unsigned *ret = snewn(2, unsigned);
116 *randseedsize = 2*sizeof(unsigned);
120 * Fatal error, called in cases of complete despair such as when
121 * malloc() has returned NULL.
123 void fatal(char *fmt, ...)
128 strcpy(buf, "puzzle fatal error: ");
131 vsnprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), fmt, ap);
137 void debug_printf(char *fmt, ...)
142 vsnprintf(buf, sizeof(buf), fmt, ap);
148 * Helper function that makes it easy to test strings that might be
151 int strnullcmp(const char *a, const char *b)
153 if (a == NULL || b == NULL)
154 return a != NULL ? +1 : b != NULL ? -1 : 0;
159 * HTMLish names for the colours allocated by the puzzle.
161 char **colour_strings;
165 * The global midend object.
169 /* ----------------------------------------------------------------------
172 int timer_active = FALSE;
173 void deactivate_timer(frontend *fe)
175 js_deactivate_timer();
176 timer_active = FALSE;
178 void activate_timer(frontend *fe)
185 void timer_callback(double tplus)
188 midend_timer(me, tplus);
191 /* ----------------------------------------------------------------------
192 * Helper functions to resize the canvas, and variables to remember
193 * its size for other functions (e.g. trimming blitter rectangles).
195 static int canvas_w, canvas_h;
197 /* Called when we resize as a result of changing puzzle settings */
198 static void resize(void)
202 midend_size(me, &w, &h, FALSE);
203 js_canvas_set_size(w, h);
208 /* Called from JS when the user uses the resize handle */
209 void resize_puzzle(int w, int h)
211 midend_size(me, &w, &h, TRUE);
212 if (canvas_w != w || canvas_h != h) {
213 js_canvas_set_size(w, h);
216 midend_force_redraw(me);
220 /* Called from JS when the user uses the restore button */
221 void restore_puzzle_size(int w, int h)
223 midend_reset_tilesize(me);
225 midend_force_redraw(me);
229 * HTML doesn't give us a default frontend colour of its own, so we
230 * just make up a lightish grey ourselves.
232 void frontend_default_colour(frontend *fe, float *output)
234 output[0] = output[1] = output[2] = 0.9F;
238 * Helper function called from all over the place to ensure the undo
239 * and redo buttons get properly enabled and disabled after every move
240 * or undo or new-game event.
242 static void update_undo_redo(void)
244 js_enable_undo_redo(midend_can_undo(me), midend_can_redo(me));
248 * Mouse event handlers called from JS.
250 void mousedown(int x, int y, int button)
252 button = (button == 0 ? LEFT_BUTTON :
253 button == 1 ? MIDDLE_BUTTON : RIGHT_BUTTON);
254 midend_process_key(me, x, y, button);
258 void mouseup(int x, int y, int button)
260 button = (button == 0 ? LEFT_RELEASE :
261 button == 1 ? MIDDLE_RELEASE : RIGHT_RELEASE);
262 midend_process_key(me, x, y, button);
266 void mousemove(int x, int y, int buttons)
268 int button = (buttons & 2 ? MIDDLE_DRAG :
269 buttons & 4 ? RIGHT_DRAG : LEFT_DRAG);
270 midend_process_key(me, x, y, button);
275 * Keyboard handler called from JS.
277 void key(int keycode, int charcode, const char *key, const char *chr,
282 if (!strnullcmp(key, "Backspace") || !strnullcmp(key, "Del") ||
283 keycode == 8 || keycode == 46) {
284 keyevent = 127; /* Backspace / Delete */
285 } else if (!strnullcmp(key, "Enter") || keycode == 13) {
286 keyevent = 13; /* return */
287 } else if (!strnullcmp(key, "Left") || keycode == 37) {
288 keyevent = CURSOR_LEFT;
289 } else if (!strnullcmp(key, "Up") || keycode == 38) {
290 keyevent = CURSOR_UP;
291 } else if (!strnullcmp(key, "Right") || keycode == 39) {
292 keyevent = CURSOR_RIGHT;
293 } else if (!strnullcmp(key, "Down") || keycode == 40) {
294 keyevent = CURSOR_DOWN;
295 } else if (!strnullcmp(key, "End") || keycode == 35) {
297 * We interpret Home, End, PgUp and PgDn as numeric keypad
298 * controls regardless of whether they're the ones on the
299 * numeric keypad (since we can't tell). The effect of
300 * this should only be that the non-numeric-pad versions
301 * of those keys generate directions in 8-way movement
302 * puzzles like Cube and Inertia.
304 keyevent = MOD_NUM_KEYPAD | '1';
305 } else if (!strnullcmp(key, "PageDown") || keycode==34) {
306 keyevent = MOD_NUM_KEYPAD | '3';
307 } else if (!strnullcmp(key, "Home") || keycode==36) {
308 keyevent = MOD_NUM_KEYPAD | '7';
309 } else if (!strnullcmp(key, "PageUp") || keycode==33) {
310 keyevent = MOD_NUM_KEYPAD | '9';
311 } else if (chr && chr[0] && !chr[1]) {
312 keyevent = chr[0] & 0xFF;
313 } else if (keycode >= 96 && keycode < 106) {
314 keyevent = MOD_NUM_KEYPAD | ('0' + keycode - 96);
315 } else if (keycode >= 65 && keycode <= 90) {
316 keyevent = keycode + (shift ? 0 : 32);
317 } else if (keycode >= 48 && keycode <= 57) {
322 if (shift && keyevent >= 0x100)
323 keyevent |= MOD_SHFT;
326 if (keyevent >= 0x100)
327 keyevent |= MOD_CTRL;
332 midend_process_key(me, 0, 0, keyevent);
338 * Helper function called from several places to update the permalinks
339 * whenever a new game is created.
341 static void update_permalinks(void)
344 desc = midend_get_game_id(me);
345 seed = midend_get_random_seed(me);
346 js_update_permalinks(desc, seed);
352 * Callback from the midend when the game ids change, so we can update
355 static void ids_changed(void *ignored)
360 /* ----------------------------------------------------------------------
361 * Implementation of the drawing API by calling Javascript canvas
362 * drawing functions. (Well, half of it; the other half is on the JS
365 static void js_start_draw(void *handle)
367 js_canvas_start_draw();
370 static void js_clip(void *handle, int x, int y, int w, int h)
372 js_canvas_clip_rect(x, y, w, h);
375 static void js_unclip(void *handle)
380 static void js_draw_text(void *handle, int x, int y, int fonttype,
381 int fontsize, int align, int colour, char *text)
386 sprintf(fontstyle, "%dpx %s", fontsize,
387 fonttype == FONT_FIXED ? "monospace" : "sans-serif");
389 if (align & ALIGN_VCENTRE)
390 y += js_canvas_find_font_midpoint(fontsize, fontstyle);
392 if (align & ALIGN_HCENTRE)
394 else if (align & ALIGN_HRIGHT)
399 js_canvas_draw_text(x, y, halign, colour_strings[colour], fontstyle, text);
402 static void js_draw_rect(void *handle, int x, int y, int w, int h, int colour)
404 js_canvas_draw_rect(x, y, w, h, colour_strings[colour]);
407 static void js_draw_line(void *handle, int x1, int y1, int x2, int y2,
410 js_canvas_draw_line(x1, y1, x2, y2, 1, colour_strings[colour]);
413 static void js_draw_thick_line(void *handle, float thickness,
414 float x1, float y1, float x2, float y2,
417 js_canvas_draw_line(x1, y1, x2, y2, thickness, colour_strings[colour]);
420 static void js_draw_poly(void *handle, int *coords, int npoints,
421 int fillcolour, int outlinecolour)
423 js_canvas_draw_poly(coords, npoints,
424 fillcolour >= 0 ? colour_strings[fillcolour] : NULL,
425 colour_strings[outlinecolour]);
428 static void js_draw_circle(void *handle, int cx, int cy, int radius,
429 int fillcolour, int outlinecolour)
431 js_canvas_draw_circle(cx, cy, radius,
432 fillcolour >= 0 ? colour_strings[fillcolour] : NULL,
433 colour_strings[outlinecolour]);
437 int id; /* allocated on the js side */
438 int w, h; /* easier to retain here */
441 static blitter *js_blitter_new(void *handle, int w, int h)
443 blitter *bl = snew(blitter);
446 bl->id = js_canvas_new_blitter(w, h);
450 static void js_blitter_free(void *handle, blitter *bl)
452 js_canvas_free_blitter(bl->id);
456 static void trim_rect(int *x, int *y, int *w, int *h)
461 * Reduce the size of the copied rectangle to stop it going
462 * outside the bounds of the canvas.
465 /* Transform from x,y,w,h form into coordinates of all edges */
471 /* Clip each coordinate at both extremes of the canvas */
472 x0 = (x0 < 0 ? 0 : x0 > canvas_w ? canvas_w : x0);
473 x1 = (x1 < 0 ? 0 : x1 > canvas_w ? canvas_w : x1);
474 y0 = (y0 < 0 ? 0 : y0 > canvas_h ? canvas_h : y0);
475 y1 = (y1 < 0 ? 0 : y1 > canvas_h ? canvas_h : y1);
477 /* Transform back into x,y,w,h to return */
484 static void js_blitter_save(void *handle, blitter *bl, int x, int y)
486 int w = bl->w, h = bl->h;
487 trim_rect(&x, &y, &w, &h);
489 js_canvas_copy_to_blitter(bl->id, x, y, w, h);
492 static void js_blitter_load(void *handle, blitter *bl, int x, int y)
494 int w = bl->w, h = bl->h;
495 trim_rect(&x, &y, &w, &h);
497 js_canvas_copy_from_blitter(bl->id, x, y, w, h);
500 static void js_draw_update(void *handle, int x, int y, int w, int h)
502 trim_rect(&x, &y, &w, &h);
504 js_canvas_draw_update(x, y, w, h);
507 static void js_end_draw(void *handle)
509 js_canvas_end_draw();
512 static void js_status_bar(void *handle, char *text)
514 js_canvas_set_statusbar(text);
517 static char *js_text_fallback(void *handle, const char *const *strings,
520 return dupstr(strings[0]); /* Emscripten has no trouble with UTF-8 */
523 const struct drawing_api js_drawing = {
539 NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */
540 NULL, NULL, /* line_width, line_dotted */
545 /* ----------------------------------------------------------------------
546 * Presets and game-configuration dialog support.
548 static game_params **presets;
550 int have_presets_dropdown;
552 void select_appropriate_preset(void)
554 if (have_presets_dropdown) {
555 int preset = midend_which_preset(me);
556 js_select_preset(preset < 0 ? -1 : preset);
560 static config_item *cfg = NULL;
561 static int cfg_which;
564 * Set up a dialog box. This is pretty easy on the C side; most of the
565 * work is done in JS.
567 static void cfg_start(int which)
572 cfg = midend_get_config(me, which, &title);
575 js_dialog_init(title);
578 for (i = 0; cfg[i].type != C_END; i++) {
579 switch (cfg[i].type) {
581 js_dialog_string(i, cfg[i].name, cfg[i].sval);
584 js_dialog_boolean(i, cfg[i].name, cfg[i].ival);
587 js_dialog_choices(i, cfg[i].name, cfg[i].sval, cfg[i].ival);
596 * Callbacks from JS when the OK button is clicked, to return the
597 * final state of each control.
599 void dlg_return_sval(int index, const char *val)
601 sfree(cfg[index].sval);
602 cfg[index].sval = dupstr(val);
604 void dlg_return_ival(int index, int val)
606 cfg[index].ival = val;
610 * Called when the user clicks OK or Cancel. use_results will be TRUE
611 * or FALSE respectively, in those cases. We terminate the dialog box,
612 * unless the user selected an invalid combination of parameters.
614 static void cfg_end(int use_results)
620 char *err = midend_set_config(me, cfg_which, cfg);
624 * The settings were unacceptable, so leave the config box
625 * open for the user to adjust them and try again.
630 * New settings are fine; start a new game and close the
633 select_appropriate_preset();
642 * User hit Cancel. Close the dialog, but also we must still
643 * reselect the right element of the dropdown list.
645 * (Because: imagine you have a preset selected, and then you
646 * select Custom from the list, but change your mind and hit
647 * Esc. The Custom option will now still be selected in the
648 * list, whereas obviously it should show the preset you still
649 * _actually_ have selected. Worse still, it'll be the visible
650 * rather than invisible Custom option - see the comment in
651 * js_add_preset in emcclib.js - so you won't even be able to
652 * select Custom without a faffy workaround.)
654 select_appropriate_preset();
661 /* ----------------------------------------------------------------------
662 * Called from JS when a command is given to the puzzle by clicking a
663 * button or control of some sort.
668 case 0: /* specific game ID */
671 case 1: /* random game seed */
674 case 2: /* game parameter dropdown changed */
676 int i = js_get_selected_preset();
679 * The user selected 'Custom', so launch the config
682 if (thegame.can_configure) /* (double-check just in case) */
683 cfg_start(CFG_SETTINGS);
686 * The user selected a preset, so just switch straight
689 assert(i < npresets);
690 midend_set_params(me, presets[i]);
696 select_appropriate_preset(); /* sort out Custom/Customise */
700 case 3: /* OK clicked in a config box */
704 case 4: /* Cancel clicked in a config box */
708 case 5: /* New Game */
709 midend_process_key(me, 0, 0, 'n');
713 case 6: /* Restart */
714 midend_restart_game(me);
719 midend_process_key(me, 0, 0, 'u');
724 midend_process_key(me, 0, 0, 'r');
729 if (thegame.can_solve) {
730 char *msg = midend_solve(me);
740 /* ----------------------------------------------------------------------
741 * Setup function called at page load time. It's called main() because
742 * that's the most convenient thing in Emscripten, but it's not main()
743 * in the usual sense of bounding the program's entire execution.
744 * Instead, this function returns once the initial puzzle is set up
745 * and working, and everything thereafter happens by means of JS event
746 * handlers sending us callbacks.
748 int main(int argc, char **argv)
755 * Instantiate a midend.
757 me = midend_new(NULL, &thegame, &js_drawing, NULL);
760 * Chuck in the HTML fragment ID if we have one (trimming the
761 * leading # off the front first). If that's invalid, we retain
762 * the error message and will display it at the end, after setting
763 * up a random puzzle as usual.
765 if (argc > 1 && argv[1][0] == '#' && argv[1][1] != '\0')
766 param_err = midend_game_id(me, argv[1] + 1);
771 * Create either a random game or the specified one, and set the
772 * canvas size appropriately.
778 * Create a status bar, if needed.
780 if (midend_wants_statusbar(me))
781 js_canvas_make_statusbar();
784 * Set up the game-type dropdown with presets and/or the Custom
787 npresets = midend_num_presets(me);
790 * This puzzle doesn't have selectable game types at all.
791 * Completely remove the drop-down list from the page.
793 js_remove_type_dropdown();
794 have_presets_dropdown = FALSE;
798 presets = snewn(npresets, game_params *);
799 for (i = 0; i < npresets; i++) {
801 midend_fetch_preset(me, i, &name, &presets[i]);
804 if (thegame.can_configure)
805 js_add_preset(NULL); /* the 'Custom' entry in the dropdown */
807 have_presets_dropdown = TRUE;
810 * Now ensure the appropriate element of the presets menu
811 * starts off selected, in case it isn't the first one in the
814 select_appropriate_preset();
818 * Remove the Solve button if the game doesn't support it.
820 if (!thegame.can_solve)
821 js_remove_solve_button();
824 * Retrieve the game's colours, and convert them into #abcdef type
827 colours = midend_colours(me, &ncolours);
828 colour_strings = snewn(ncolours, char *);
829 for (i = 0; i < ncolours; i++) {
831 sprintf(col, "#%02x%02x%02x",
832 (unsigned)(0.5 + 255 * colours[i*3+0]),
833 (unsigned)(0.5 + 255 * colours[i*3+1]),
834 (unsigned)(0.5 + 255 * colours[i*3+2]));
835 colour_strings[i] = dupstr(col);
839 * Request notification when the game ids change (e.g. if the user
840 * presses 'n', and also when Mines supersedes its game
841 * description), so that we can proactively update the permalink.
843 midend_request_id_changes(me, ids_changed, NULL);
846 * Draw the puzzle's initial state, and set up the permalinks and
847 * undo/redo greying out.
854 * If we were given an erroneous game ID in argv[1], now's the
855 * time to put up the error box about it, after we've fully set up
856 * a random puzzle. Then when the user clicks 'ok', we have a
860 js_error_box(param_err);
863 * Done. Return to JS, and await callbacks!