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
10 * Further thoughts on possible enhancements:
12 * - I think it might be feasible to have these JS puzzles permit
13 * loading and saving games in disk files. Saving would be done by
14 * constructing a data: URI encapsulating the save file, and then
15 * telling the browser to visit that URI with the effect that it
16 * would naturally pop up a 'where would you like to save this'
17 * dialog box. Loading, more or less similarly, might be feasible
18 * by using the DOM File API to ask the user to select a file and
19 * permit us to see its contents.
21 * - it ought to be possible to make the puzzle canvases resizable,
22 * by superimposing some kind of draggable resize handle. Also I
23 * quite like the idea of having a few buttons for standard sizes:
24 * reset to default size, maximise to the browser window dimensions
25 * (if we can find those out), and perhaps even go full-screen.
27 * - I should think about whether these webified puzzles can support
28 * touchscreen-based tablet browsers (assuming there are any that
29 * can cope with the reasonably modern JS and run it fast enough to
32 * - think about making use of localStorage. It might be useful to
33 * let the user save games into there as an alternative to disk
34 * files - disk files are all very well for getting the save right
35 * out of your browser to (e.g.) email to me as a bug report, but
36 * for just resuming a game you were in the middle of, you'd
37 * probably rather have a nice simple 'quick save' and 'quick load'
38 * button pair. Also, that might be a useful place to store
39 * preferences, if I ever get round to writing a preferences UI.
41 * - some CSS to make the button bar and configuration dialogs a
42 * little less ugly would probably not go amiss.
44 * - this is a downright silly idea, but it does occur to me that if
45 * I were to write a PDF output driver for the Puzzles printing
46 * API, then I might be able to implement a sort of 'printing'
47 * feature in this front end, using data: URIs again. (Ask the user
48 * exactly what they want printed, then construct an appropriate
49 * PDF and embed it in a gigantic data: URI. Then they can print
50 * that using whatever they normally use to print PDFs!)
58 * Extern references to Javascript functions provided in emcclib.js.
60 extern void js_debug(const char *);
61 extern void js_error_box(const char *message);
62 extern void js_remove_type_dropdown(void);
63 extern void js_remove_solve_button(void);
64 extern void js_add_preset(const char *name);
65 extern int js_get_selected_preset(void);
66 extern void js_select_preset(int n);
67 extern void js_get_date_64(unsigned *p);
68 extern void js_update_permalinks(const char *desc, const char *seed);
69 extern void js_enable_undo_redo(int undo, int redo);
70 extern void js_activate_timer();
71 extern void js_deactivate_timer();
72 extern void js_canvas_start_draw(void);
73 extern void js_canvas_draw_update(int x, int y, int w, int h);
74 extern void js_canvas_end_draw(void);
75 extern void js_canvas_draw_rect(int x, int y, int w, int h,
77 extern void js_canvas_clip_rect(int x, int y, int w, int h);
78 extern void js_canvas_unclip(void);
79 extern void js_canvas_draw_line(float x1, float y1, float x2, float y2,
80 int width, const char *colour);
81 extern void js_canvas_draw_poly(int *points, int npoints,
82 const char *fillcolour,
83 const char *outlinecolour);
84 extern void js_canvas_draw_circle(int x, int y, int r,
85 const char *fillcolour,
86 const char *outlinecolour);
87 extern int js_canvas_find_font_midpoint(int height, const char *fontptr);
88 extern void js_canvas_draw_text(int x, int y, int halign,
89 const char *colptr, const char *fontptr,
91 extern int js_canvas_new_blitter(int w, int h);
92 extern void js_canvas_free_blitter(int id);
93 extern void js_canvas_copy_to_blitter(int id, int x, int y, int w, int h);
94 extern void js_canvas_copy_from_blitter(int id, int x, int y, int w, int h);
95 extern void js_canvas_make_statusbar(void);
96 extern void js_canvas_set_statusbar(const char *text);
97 extern void js_canvas_set_size(int w, int h);
99 extern void js_dialog_init(const char *title);
100 extern void js_dialog_string(int i, const char *title, const char *initvalue);
101 extern void js_dialog_choices(int i, const char *title, const char *choicelist,
103 extern void js_dialog_boolean(int i, const char *title, int initvalue);
104 extern void js_dialog_launch(void);
105 extern void js_dialog_cleanup(void);
106 extern void js_focus_canvas(void);
109 * Call JS to get the date, and use that to initialise our random
110 * number generator to invent the first game seed.
112 void get_random_seed(void **randseed, int *randseedsize)
114 unsigned *ret = snewn(2, unsigned);
117 *randseedsize = 2*sizeof(unsigned);
121 * Fatal error, called in cases of complete despair such as when
122 * malloc() has returned NULL.
124 void fatal(char *fmt, ...)
129 strcpy(buf, "puzzle fatal error: ");
132 vsnprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), fmt, ap);
139 * HTMLish names for the colours allocated by the puzzle.
141 char **colour_strings;
145 * The global midend object.
149 /* ----------------------------------------------------------------------
152 int timer_active = FALSE;
153 void deactivate_timer(frontend *fe)
155 js_deactivate_timer();
156 timer_active = FALSE;
158 void activate_timer(frontend *fe)
165 void timer_callback(double tplus)
168 midend_timer(me, tplus);
171 /* ----------------------------------------------------------------------
172 * Helper function to resize the canvas, and variables to remember its
173 * size for other functions (e.g. trimming blitter rectangles).
175 static int canvas_w, canvas_h;
176 static void resize(void)
180 midend_size(me, &w, &h, FALSE);
181 js_canvas_set_size(w, h);
187 * HTML doesn't give us a default frontend colour of its own, so we
188 * just make up a lightish grey ourselves.
190 void frontend_default_colour(frontend *fe, float *output)
192 output[0] = output[1] = output[2] = 0.9F;
196 * Helper function called from all over the place to ensure the undo
197 * and redo buttons get properly enabled and disabled after every move
198 * or undo or new-game event.
200 static void update_undo_redo(void)
202 js_enable_undo_redo(midend_can_undo(me), midend_can_redo(me));
206 * Mouse event handlers called from JS.
208 void mousedown(int x, int y, int button)
210 button = (button == 0 ? LEFT_BUTTON :
211 button == 1 ? MIDDLE_BUTTON : RIGHT_BUTTON);
212 midend_process_key(me, x, y, button);
216 void mouseup(int x, int y, int button)
218 button = (button == 0 ? LEFT_RELEASE :
219 button == 1 ? MIDDLE_RELEASE : RIGHT_RELEASE);
220 midend_process_key(me, x, y, button);
224 void mousemove(int x, int y, int buttons)
226 int button = (buttons & 2 ? MIDDLE_DRAG :
227 buttons & 4 ? RIGHT_DRAG : LEFT_DRAG);
228 midend_process_key(me, x, y, button);
233 * Keyboard handler called from JS.
235 void key(int keycode, int charcode, int shift, int ctrl)
239 keyevent = charcode & (ctrl ? 0x1F : 0xFF);
243 keyevent = '\177'; /* backspace */
246 keyevent = 13; /* return */
249 keyevent = CURSOR_LEFT;
252 keyevent = CURSOR_UP;
255 keyevent = CURSOR_RIGHT;
258 keyevent = CURSOR_DOWN;
261 * We interpret Home, End, PgUp and PgDn as numeric keypad
262 * controls regardless of whether they're the ones on the
263 * numeric keypad (since we can't tell). The effect of
264 * this should only be that the non-numeric-pad versions
265 * of those keys generate directions in 8-way movement
266 * puzzles like Cube and Inertia.
269 keyevent = MOD_NUM_KEYPAD | '1';
272 keyevent = MOD_NUM_KEYPAD | '3';
275 keyevent = MOD_NUM_KEYPAD | '7';
278 keyevent = MOD_NUM_KEYPAD | '9';
280 case 96: case 97: case 98: case 99: case 100:
281 case 101: case 102: case 103: case 104: case 105:
282 keyevent = MOD_NUM_KEYPAD | ('0' + keycode - 96);
285 /* not a key we care about */
289 if (shift && keyevent >= 0x100)
290 keyevent |= MOD_SHFT;
291 if (ctrl && keyevent >= 0x100)
292 keyevent |= MOD_CTRL;
294 midend_process_key(me, 0, 0, keyevent);
299 * Helper function called from several places to update the permalinks
300 * whenever a new game is created.
302 static void update_permalinks(void)
305 desc = midend_get_game_id(me);
306 seed = midend_get_random_seed(me);
307 js_update_permalinks(desc, seed);
312 /* ----------------------------------------------------------------------
313 * Implementation of the drawing API by calling Javascript canvas
314 * drawing functions. (Well, half of it; the other half is on the JS
317 static void js_start_draw(void *handle)
319 js_canvas_start_draw();
322 static void js_clip(void *handle, int x, int y, int w, int h)
324 js_canvas_clip_rect(x, y, w, h);
327 static void js_unclip(void *handle)
332 static void js_draw_text(void *handle, int x, int y, int fonttype,
333 int fontsize, int align, int colour, char *text)
338 sprintf(fontstyle, "%dpx %s", fontsize,
339 fonttype == FONT_FIXED ? "monospace" : "sans-serif");
341 if (align & ALIGN_VCENTRE)
342 y += js_canvas_find_font_midpoint(fontsize, fontstyle);
344 if (align & ALIGN_HCENTRE)
346 else if (align & ALIGN_HRIGHT)
351 js_canvas_draw_text(x, y, halign, colour_strings[colour], fontstyle, text);
354 static void js_draw_rect(void *handle, int x, int y, int w, int h, int colour)
356 js_canvas_draw_rect(x, y, w, h, colour_strings[colour]);
359 static void js_draw_line(void *handle, int x1, int y1, int x2, int y2,
362 js_canvas_draw_line(x1, y1, x2, y2, 1, colour_strings[colour]);
365 static void js_draw_thick_line(void *handle, float thickness,
366 float x1, float y1, float x2, float y2,
369 js_canvas_draw_line(x1, y1, x2, y2, thickness, colour_strings[colour]);
372 static void js_draw_poly(void *handle, int *coords, int npoints,
373 int fillcolour, int outlinecolour)
375 js_canvas_draw_poly(coords, npoints,
376 fillcolour >= 0 ? colour_strings[fillcolour] : NULL,
377 colour_strings[outlinecolour]);
380 static void js_draw_circle(void *handle, int cx, int cy, int radius,
381 int fillcolour, int outlinecolour)
383 js_canvas_draw_circle(cx, cy, radius,
384 fillcolour >= 0 ? colour_strings[fillcolour] : NULL,
385 colour_strings[outlinecolour]);
389 int id; /* allocated on the js side */
390 int w, h; /* easier to retain here */
393 static blitter *js_blitter_new(void *handle, int w, int h)
395 blitter *bl = snew(blitter);
398 bl->id = js_canvas_new_blitter(w, h);
402 static void js_blitter_free(void *handle, blitter *bl)
404 js_canvas_free_blitter(bl->id);
408 static void trim_rect(int *x, int *y, int *w, int *h)
411 * Reduce the size of the copied rectangle to stop it going
412 * outside the bounds of the canvas.
422 if (*w > canvas_w - *x)
424 if (*h > canvas_h - *y)
433 static void js_blitter_save(void *handle, blitter *bl, int x, int y)
435 int w = bl->w, h = bl->h;
436 trim_rect(&x, &y, &w, &h);
438 js_canvas_copy_to_blitter(bl->id, x, y, w, h);
441 static void js_blitter_load(void *handle, blitter *bl, int x, int y)
443 int w = bl->w, h = bl->h;
444 trim_rect(&x, &y, &w, &h);
446 js_canvas_copy_from_blitter(bl->id, x, y, w, h);
449 static void js_draw_update(void *handle, int x, int y, int w, int h)
451 trim_rect(&x, &y, &w, &h);
452 js_canvas_draw_update(x, y, w, h);
455 static void js_end_draw(void *handle)
457 js_canvas_end_draw();
460 static void js_status_bar(void *handle, char *text)
462 js_canvas_set_statusbar(text);
465 static char *js_text_fallback(void *handle, const char *const *strings,
468 return dupstr(strings[0]); /* Emscripten has no trouble with UTF-8 */
471 const struct drawing_api js_drawing = {
487 NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */
488 NULL, NULL, /* line_width, line_dotted */
493 /* ----------------------------------------------------------------------
494 * Presets and game-configuration dialog support.
496 static game_params **presets;
497 static int custom_preset;
499 static config_item *cfg = NULL;
500 static int cfg_which;
503 * Set up a dialog box. This is pretty easy on the C side; most of the
504 * work is done in JS.
506 static void cfg_start(int which)
511 cfg = midend_get_config(me, which, &title);
514 js_dialog_init(title);
517 for (i = 0; cfg[i].type != C_END; i++) {
518 switch (cfg[i].type) {
520 js_dialog_string(i, cfg[i].name, cfg[i].sval);
523 js_dialog_boolean(i, cfg[i].name, cfg[i].ival);
526 js_dialog_choices(i, cfg[i].name, cfg[i].sval, cfg[i].ival);
535 * Callbacks from JS when the OK button is clicked, to return the
536 * final state of each control.
538 void dlg_return_sval(int index, const char *val)
540 sfree(cfg[index].sval);
541 cfg[index].sval = dupstr(val);
543 void dlg_return_ival(int index, int val)
545 cfg[index].ival = val;
549 * Called when the user clicks OK or Cancel. use_results will be TRUE
550 * or FALSE respectively, in those cases. We terminate the dialog box,
551 * unless the user selected an invalid combination of parameters.
553 static void cfg_end(int use_results)
559 char *err = midend_set_config(me, cfg_which, cfg);
563 * The settings were unacceptable, so leave the config box
564 * open for the user to adjust them and try again.
569 * New settings are fine; start a new game and close the
572 int preset = midend_which_preset(me);
573 js_select_preset(preset < 0 ? custom_preset : preset);
584 * User hit Cancel. Just close the dialog.
591 /* ----------------------------------------------------------------------
592 * Called from JS when a command is given to the puzzle by clicking a
593 * button or control of some sort.
598 case 0: /* specific game ID */
601 case 1: /* random game seed */
604 case 2: /* game parameter dropdown changed */
606 int i = js_get_selected_preset();
607 if (i == custom_preset) {
609 * The user selected 'Custom', so launch the config
612 if (thegame.can_configure) /* (double-check just in case) */
613 cfg_start(CFG_SETTINGS);
616 * The user selected a preset, so just switch straight
619 midend_set_params(me, presets[i]);
629 case 3: /* OK clicked in a config box */
633 case 4: /* Cancel clicked in a config box */
637 case 5: /* New Game */
638 midend_process_key(me, 0, 0, 'n');
642 case 6: /* Restart */
643 midend_restart_game(me);
648 midend_process_key(me, 0, 0, 'u');
653 midend_process_key(me, 0, 0, 'r');
658 if (thegame.can_solve) {
659 char *msg = midend_solve(me);
669 /* ----------------------------------------------------------------------
670 * Setup function called at page load time. It's called main() because
671 * that's the most convenient thing in Emscripten, but it's not main()
672 * in the usual sense of bounding the program's entire execution.
673 * Instead, this function returns once the initial puzzle is set up
674 * and working, and everything thereafter happens by means of JS event
675 * handlers sending us callbacks.
677 int main(int argc, char **argv)
684 * Instantiate a midend.
686 me = midend_new(NULL, &thegame, &js_drawing, NULL);
689 * Chuck in the HTML fragment ID if we have one (trimming the
690 * leading # off the front first). If that's invalid, we retain
691 * the error message and will display it at the end, after setting
692 * up a random puzzle as usual.
694 if (argc > 1 && argv[1][0] == '#' && argv[1][1] != '\0')
695 param_err = midend_game_id(me, argv[1] + 1);
700 * Create either a random game or the specified one, and set the
701 * canvas size appropriately.
707 * Create a status bar, if needed.
709 if (midend_wants_statusbar(me))
710 js_canvas_make_statusbar();
713 * Set up the game-type dropdown with presets and/or the Custom
714 * option. We remember the index of the Custom option (as
715 * custom_preset) so that we can easily treat it specially when
718 custom_preset = midend_num_presets(me);
719 presets = snewn(custom_preset, game_params *);
720 for (i = 0; i < custom_preset; i++) {
722 midend_fetch_preset(me, i, &name, &presets[i]);
725 if (thegame.can_configure)
726 js_add_preset("Custom");
727 else if (custom_preset == 0)
728 js_remove_type_dropdown();
731 * Remove the Solve button if the game doesn't support it.
733 if (!thegame.can_solve)
734 js_remove_solve_button();
737 * Retrieve the game's colours, and convert them into #abcdef type
740 colours = midend_colours(me, &ncolours);
741 colour_strings = snewn(ncolours, char *);
742 for (i = 0; i < ncolours; i++) {
744 sprintf(col, "#%02x%02x%02x",
745 (unsigned)(0.5 + 255 * colours[i*3+0]),
746 (unsigned)(0.5 + 255 * colours[i*3+1]),
747 (unsigned)(0.5 + 255 * colours[i*3+2]));
748 colour_strings[i] = dupstr(col);
752 * Draw the puzzle's initial state, and set up the permalinks and
753 * undo/redo greying out.
760 * If we were given an erroneous game ID in argv[1], now's the
761 * time to put up the error box about it, after we've fully set up
762 * a random puzzle. Then when the user clicks 'ok', we have a
766 js_error_box(param_err);
769 * Done. Return to JS, and await callbacks!