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!)
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(int menuid
, const char *name
, int value
);
65 extern int js_add_preset_submenu(int menuid
, const char *name
);
66 extern int js_get_selected_preset(void);
67 extern void js_select_preset(int n
);
68 extern void js_get_date_64(unsigned *p
);
69 extern void js_update_permalinks(const char *desc
, const char *seed
);
70 extern void js_enable_undo_redo(int undo
, int redo
);
71 extern void js_activate_timer();
72 extern void js_deactivate_timer();
73 extern void js_canvas_start_draw(void);
74 extern void js_canvas_draw_update(int x
, int y
, int w
, int h
);
75 extern void js_canvas_end_draw(void);
76 extern void js_canvas_draw_rect(int x
, int y
, int w
, int h
,
78 extern void js_canvas_clip_rect(int x
, int y
, int w
, int h
);
79 extern void js_canvas_unclip(void);
80 extern void js_canvas_draw_line(float x1
, float y1
, float x2
, float y2
,
81 int width
, const char *colour
);
82 extern void js_canvas_draw_poly(int *points
, int npoints
,
83 const char *fillcolour
,
84 const char *outlinecolour
);
85 extern void js_canvas_draw_circle(int x
, int y
, int r
,
86 const char *fillcolour
,
87 const char *outlinecolour
);
88 extern int js_canvas_find_font_midpoint(int height
, const char *fontptr
);
89 extern void js_canvas_draw_text(int x
, int y
, int halign
,
90 const char *colptr
, const char *fontptr
,
92 extern int js_canvas_new_blitter(int w
, int h
);
93 extern void js_canvas_free_blitter(int id
);
94 extern void js_canvas_copy_to_blitter(int id
, int x
, int y
, int w
, int h
);
95 extern void js_canvas_copy_from_blitter(int id
, int x
, int y
, int w
, int h
);
96 extern void js_canvas_make_statusbar(void);
97 extern void js_canvas_set_statusbar(const char *text
);
98 extern void js_canvas_set_size(int w
, int h
);
100 extern void js_dialog_init(const char *title
);
101 extern void js_dialog_string(int i
, const char *title
, const char *initvalue
);
102 extern void js_dialog_choices(int i
, const char *title
, const char *choicelist
,
104 extern void js_dialog_boolean(int i
, const char *title
, int initvalue
);
105 extern void js_dialog_launch(void);
106 extern void js_dialog_cleanup(void);
107 extern void js_focus_canvas(void);
110 * Call JS to get the date, and use that to initialise our random
111 * number generator to invent the first game seed.
113 void get_random_seed(void **randseed
, int *randseedsize
)
115 unsigned *ret
= snewn(2, unsigned);
118 *randseedsize
= 2*sizeof(unsigned);
122 * Fatal error, called in cases of complete despair such as when
123 * malloc() has returned NULL.
125 void fatal(char *fmt
, ...)
130 strcpy(buf
, "puzzle fatal error: ");
133 vsnprintf(buf
+strlen(buf
), sizeof(buf
)-strlen(buf
), fmt
, ap
);
139 void debug_printf(char *fmt
, ...)
144 vsnprintf(buf
, sizeof(buf
), fmt
, ap
);
150 * Helper function that makes it easy to test strings that might be
153 int strnullcmp(const char *a
, const char *b
)
155 if (a
== NULL
|| b
== NULL
)
156 return a
!= NULL
? +1 : b
!= NULL
? -1 : 0;
161 * HTMLish names for the colours allocated by the puzzle.
163 char **colour_strings
;
167 * The global midend object.
171 /* ----------------------------------------------------------------------
174 int timer_active
= FALSE
;
175 void deactivate_timer(frontend
*fe
)
177 js_deactivate_timer();
178 timer_active
= FALSE
;
180 void activate_timer(frontend
*fe
)
187 void timer_callback(double tplus
)
190 midend_timer(me
, tplus
);
193 /* ----------------------------------------------------------------------
194 * Helper functions to resize the canvas, and variables to remember
195 * its size for other functions (e.g. trimming blitter rectangles).
197 static int canvas_w
, canvas_h
;
199 /* Called when we resize as a result of changing puzzle settings */
200 static void resize(void)
204 midend_size(me
, &w
, &h
, FALSE
);
205 js_canvas_set_size(w
, h
);
210 /* Called from JS when the user uses the resize handle */
211 void resize_puzzle(int w
, int h
)
213 midend_size(me
, &w
, &h
, TRUE
);
214 if (canvas_w
!= w
|| canvas_h
!= h
) {
215 js_canvas_set_size(w
, h
);
218 midend_force_redraw(me
);
222 /* Called from JS when the user uses the restore button */
223 void restore_puzzle_size(int w
, int h
)
225 midend_reset_tilesize(me
);
227 midend_force_redraw(me
);
231 * HTML doesn't give us a default frontend colour of its own, so we
232 * just make up a lightish grey ourselves.
234 void frontend_default_colour(frontend
*fe
, float *output
)
236 output
[0] = output
[1] = output
[2] = 0.9F
;
240 * Helper function called from all over the place to ensure the undo
241 * and redo buttons get properly enabled and disabled after every move
242 * or undo or new-game event.
244 static void update_undo_redo(void)
246 js_enable_undo_redo(midend_can_undo(me
), midend_can_redo(me
));
250 * Mouse event handlers called from JS.
252 void mousedown(int x
, int y
, int button
)
254 button
= (button
== 0 ? LEFT_BUTTON
:
255 button
== 1 ? MIDDLE_BUTTON
: RIGHT_BUTTON
);
256 midend_process_key(me
, x
, y
, button
);
260 void mouseup(int x
, int y
, int button
)
262 button
= (button
== 0 ? LEFT_RELEASE
:
263 button
== 1 ? MIDDLE_RELEASE
: RIGHT_RELEASE
);
264 midend_process_key(me
, x
, y
, button
);
268 void mousemove(int x
, int y
, int buttons
)
270 int button
= (buttons
& 2 ? MIDDLE_DRAG
:
271 buttons
& 4 ? RIGHT_DRAG
: LEFT_DRAG
);
272 midend_process_key(me
, x
, y
, button
);
277 * Keyboard handler called from JS.
279 void key(int keycode
, int charcode
, const char *key
, const char *chr
,
284 if (!strnullcmp(key
, "Backspace") || !strnullcmp(key
, "Del") ||
285 keycode
== 8 || keycode
== 46) {
286 keyevent
= 127; /* Backspace / Delete */
287 } else if (!strnullcmp(key
, "Enter") || keycode
== 13) {
288 keyevent
= 13; /* return */
289 } else if (!strnullcmp(key
, "Left") || keycode
== 37) {
290 keyevent
= CURSOR_LEFT
;
291 } else if (!strnullcmp(key
, "Up") || keycode
== 38) {
292 keyevent
= CURSOR_UP
;
293 } else if (!strnullcmp(key
, "Right") || keycode
== 39) {
294 keyevent
= CURSOR_RIGHT
;
295 } else if (!strnullcmp(key
, "Down") || keycode
== 40) {
296 keyevent
= CURSOR_DOWN
;
297 } else if (!strnullcmp(key
, "End") || keycode
== 35) {
299 * We interpret Home, End, PgUp and PgDn as numeric keypad
300 * controls regardless of whether they're the ones on the
301 * numeric keypad (since we can't tell). The effect of
302 * this should only be that the non-numeric-pad versions
303 * of those keys generate directions in 8-way movement
304 * puzzles like Cube and Inertia.
306 keyevent
= MOD_NUM_KEYPAD
| '1';
307 } else if (!strnullcmp(key
, "PageDown") || keycode
==34) {
308 keyevent
= MOD_NUM_KEYPAD
| '3';
309 } else if (!strnullcmp(key
, "Home") || keycode
==36) {
310 keyevent
= MOD_NUM_KEYPAD
| '7';
311 } else if (!strnullcmp(key
, "PageUp") || keycode
==33) {
312 keyevent
= MOD_NUM_KEYPAD
| '9';
313 } else if (chr
&& chr
[0] && !chr
[1]) {
314 keyevent
= chr
[0] & 0xFF;
315 } else if (keycode
>= 96 && keycode
< 106) {
316 keyevent
= MOD_NUM_KEYPAD
| ('0' + keycode
- 96);
317 } else if (keycode
>= 65 && keycode
<= 90) {
318 keyevent
= keycode
+ (shift
? 0 : 32);
319 } else if (keycode
>= 48 && keycode
<= 57) {
321 } else if (keycode
== 32) { /* space / CURSOR_SELECT2 */
326 if (shift
&& keyevent
>= 0x100)
327 keyevent
|= MOD_SHFT
;
330 if (keyevent
>= 0x100)
331 keyevent
|= MOD_CTRL
;
336 midend_process_key(me
, 0, 0, keyevent
);
342 * Helper function called from several places to update the permalinks
343 * whenever a new game is created.
345 static void update_permalinks(void)
348 desc
= midend_get_game_id(me
);
349 seed
= midend_get_random_seed(me
);
350 js_update_permalinks(desc
, seed
);
356 * Callback from the midend when the game ids change, so we can update
359 static void ids_changed(void *ignored
)
364 /* ----------------------------------------------------------------------
365 * Implementation of the drawing API by calling Javascript canvas
366 * drawing functions. (Well, half of it; the other half is on the JS
369 static void js_start_draw(void *handle
)
371 js_canvas_start_draw();
374 static void js_clip(void *handle
, int x
, int y
, int w
, int h
)
376 js_canvas_clip_rect(x
, y
, w
, h
);
379 static void js_unclip(void *handle
)
384 static void js_draw_text(void *handle
, int x
, int y
, int fonttype
,
385 int fontsize
, int align
, int colour
, char *text
)
390 sprintf(fontstyle
, "%dpx %s", fontsize
,
391 fonttype
== FONT_FIXED
? "monospace" : "sans-serif");
393 if (align
& ALIGN_VCENTRE
)
394 y
+= js_canvas_find_font_midpoint(fontsize
, fontstyle
);
396 if (align
& ALIGN_HCENTRE
)
398 else if (align
& ALIGN_HRIGHT
)
403 js_canvas_draw_text(x
, y
, halign
, colour_strings
[colour
], fontstyle
, text
);
406 static void js_draw_rect(void *handle
, int x
, int y
, int w
, int h
, int colour
)
408 js_canvas_draw_rect(x
, y
, w
, h
, colour_strings
[colour
]);
411 static void js_draw_line(void *handle
, int x1
, int y1
, int x2
, int y2
,
414 js_canvas_draw_line(x1
, y1
, x2
, y2
, 1, colour_strings
[colour
]);
417 static void js_draw_thick_line(void *handle
, float thickness
,
418 float x1
, float y1
, float x2
, float y2
,
421 js_canvas_draw_line(x1
, y1
, x2
, y2
, thickness
, colour_strings
[colour
]);
424 static void js_draw_poly(void *handle
, int *coords
, int npoints
,
425 int fillcolour
, int outlinecolour
)
427 js_canvas_draw_poly(coords
, npoints
,
428 fillcolour
>= 0 ? colour_strings
[fillcolour
] : NULL
,
429 colour_strings
[outlinecolour
]);
432 static void js_draw_circle(void *handle
, int cx
, int cy
, int radius
,
433 int fillcolour
, int outlinecolour
)
435 js_canvas_draw_circle(cx
, cy
, radius
,
436 fillcolour
>= 0 ? colour_strings
[fillcolour
] : NULL
,
437 colour_strings
[outlinecolour
]);
441 int id
; /* allocated on the js side */
442 int w
, h
; /* easier to retain here */
445 static blitter
*js_blitter_new(void *handle
, int w
, int h
)
447 blitter
*bl
= snew(blitter
);
450 bl
->id
= js_canvas_new_blitter(w
, h
);
454 static void js_blitter_free(void *handle
, blitter
*bl
)
456 js_canvas_free_blitter(bl
->id
);
460 static void trim_rect(int *x
, int *y
, int *w
, int *h
)
465 * Reduce the size of the copied rectangle to stop it going
466 * outside the bounds of the canvas.
469 /* Transform from x,y,w,h form into coordinates of all edges */
475 /* Clip each coordinate at both extremes of the canvas */
476 x0
= (x0
< 0 ? 0 : x0
> canvas_w
? canvas_w
: x0
);
477 x1
= (x1
< 0 ? 0 : x1
> canvas_w
? canvas_w
: x1
);
478 y0
= (y0
< 0 ? 0 : y0
> canvas_h
? canvas_h
: y0
);
479 y1
= (y1
< 0 ? 0 : y1
> canvas_h
? canvas_h
: y1
);
481 /* Transform back into x,y,w,h to return */
488 static void js_blitter_save(void *handle
, blitter
*bl
, int x
, int y
)
490 int w
= bl
->w
, h
= bl
->h
;
491 trim_rect(&x
, &y
, &w
, &h
);
493 js_canvas_copy_to_blitter(bl
->id
, x
, y
, w
, h
);
496 static void js_blitter_load(void *handle
, blitter
*bl
, int x
, int y
)
498 int w
= bl
->w
, h
= bl
->h
;
499 trim_rect(&x
, &y
, &w
, &h
);
501 js_canvas_copy_from_blitter(bl
->id
, x
, y
, w
, h
);
504 static void js_draw_update(void *handle
, int x
, int y
, int w
, int h
)
506 trim_rect(&x
, &y
, &w
, &h
);
508 js_canvas_draw_update(x
, y
, w
, h
);
511 static void js_end_draw(void *handle
)
513 js_canvas_end_draw();
516 static void js_status_bar(void *handle
, char *text
)
518 js_canvas_set_statusbar(text
);
521 static char *js_text_fallback(void *handle
, const char *const *strings
,
524 return dupstr(strings
[0]); /* Emscripten has no trouble with UTF-8 */
527 const struct drawing_api js_drawing
= {
543 NULL
, NULL
, NULL
, NULL
, NULL
, NULL
, /* {begin,end}_{doc,page,puzzle} */
544 NULL
, NULL
, /* line_width, line_dotted */
549 /* ----------------------------------------------------------------------
550 * Presets and game-configuration dialog support.
552 static game_params
**presets
;
554 int have_presets_dropdown
;
556 void populate_js_preset_menu(int menuid
, struct preset_menu
*menu
)
559 for (i
= 0; i
< menu
->n_entries
; i
++) {
560 struct preset_menu_entry
*entry
= &menu
->entries
[i
];
562 presets
[entry
->id
] = entry
->params
;
563 js_add_preset(menuid
, entry
->title
, entry
->id
);
565 int js_submenu
= js_add_preset_submenu(menuid
, entry
->title
);
566 populate_js_preset_menu(js_submenu
, entry
->submenu
);
571 void select_appropriate_preset(void)
573 if (have_presets_dropdown
) {
574 int preset
= midend_which_preset(me
);
575 js_select_preset(preset
< 0 ? -1 : preset
);
579 static config_item
*cfg
= NULL
;
580 static int cfg_which
;
583 * Set up a dialog box. This is pretty easy on the C side; most of the
584 * work is done in JS.
586 static void cfg_start(int which
)
591 cfg
= midend_get_config(me
, which
, &title
);
594 js_dialog_init(title
);
597 for (i
= 0; cfg
[i
].type
!= C_END
; i
++) {
598 switch (cfg
[i
].type
) {
600 js_dialog_string(i
, cfg
[i
].name
, cfg
[i
].sval
);
603 js_dialog_boolean(i
, cfg
[i
].name
, cfg
[i
].ival
);
606 js_dialog_choices(i
, cfg
[i
].name
, cfg
[i
].sval
, cfg
[i
].ival
);
615 * Callbacks from JS when the OK button is clicked, to return the
616 * final state of each control.
618 void dlg_return_sval(int index
, const char *val
)
620 sfree(cfg
[index
].sval
);
621 cfg
[index
].sval
= dupstr(val
);
623 void dlg_return_ival(int index
, int val
)
625 cfg
[index
].ival
= val
;
629 * Called when the user clicks OK or Cancel. use_results will be TRUE
630 * or FALSE respectively, in those cases. We terminate the dialog box,
631 * unless the user selected an invalid combination of parameters.
633 static void cfg_end(int use_results
)
639 char *err
= midend_set_config(me
, cfg_which
, cfg
);
643 * The settings were unacceptable, so leave the config box
644 * open for the user to adjust them and try again.
649 * New settings are fine; start a new game and close the
652 select_appropriate_preset();
661 * User hit Cancel. Close the dialog, but also we must still
662 * reselect the right element of the dropdown list.
664 * (Because: imagine you have a preset selected, and then you
665 * select Custom from the list, but change your mind and hit
666 * Esc. The Custom option will now still be selected in the
667 * list, whereas obviously it should show the preset you still
668 * _actually_ have selected. Worse still, it'll be the visible
669 * rather than invisible Custom option - see the comment in
670 * js_add_preset in emcclib.js - so you won't even be able to
671 * select Custom without a faffy workaround.)
673 select_appropriate_preset();
680 /* ----------------------------------------------------------------------
681 * Called from JS when a command is given to the puzzle by clicking a
682 * button or control of some sort.
687 case 0: /* specific game ID */
690 case 1: /* random game seed */
693 case 2: /* game parameter dropdown changed */
695 int i
= js_get_selected_preset();
698 * The user selected 'Custom', so launch the config
701 if (thegame
.can_configure
) /* (double-check just in case) */
702 cfg_start(CFG_SETTINGS
);
705 * The user selected a preset, so just switch straight
708 assert(i
< npresets
);
709 midend_set_params(me
, presets
[i
]);
715 select_appropriate_preset();
719 case 3: /* OK clicked in a config box */
723 case 4: /* Cancel clicked in a config box */
727 case 5: /* New Game */
728 midend_process_key(me
, 0, 0, 'n');
732 case 6: /* Restart */
733 midend_restart_game(me
);
738 midend_process_key(me
, 0, 0, 'u');
743 midend_process_key(me
, 0, 0, 'r');
748 if (thegame
.can_solve
) {
749 char *msg
= midend_solve(me
);
759 /* ----------------------------------------------------------------------
760 * Setup function called at page load time. It's called main() because
761 * that's the most convenient thing in Emscripten, but it's not main()
762 * in the usual sense of bounding the program's entire execution.
763 * Instead, this function returns once the initial puzzle is set up
764 * and working, and everything thereafter happens by means of JS event
765 * handlers sending us callbacks.
767 int main(int argc
, char **argv
)
774 * Instantiate a midend.
776 me
= midend_new(NULL
, &thegame
, &js_drawing
, NULL
);
779 * Chuck in the HTML fragment ID if we have one (trimming the
780 * leading # off the front first). If that's invalid, we retain
781 * the error message and will display it at the end, after setting
782 * up a random puzzle as usual.
784 if (argc
> 1 && argv
[1][0] == '#' && argv
[1][1] != '\0')
785 param_err
= midend_game_id(me
, argv
[1] + 1);
790 * Create either a random game or the specified one, and set the
791 * canvas size appropriately.
797 * Create a status bar, if needed.
799 if (midend_wants_statusbar(me
))
800 js_canvas_make_statusbar();
803 * Set up the game-type dropdown with presets and/or the Custom
807 struct preset_menu
*menu
= midend_get_presets(me
, &npresets
);
808 presets
= snewn(npresets
, game_params
*);
809 for (i
= 0; i
< npresets
; i
++)
812 populate_js_preset_menu(0, menu
);
814 if (thegame
.can_configure
)
815 js_add_preset(0, "Custom", -1);
817 have_presets_dropdown
= TRUE
;
820 * Now ensure the appropriate element of the presets menu
821 * starts off selected, in case it isn't the first one in the
824 select_appropriate_preset();
828 * Remove the Solve button if the game doesn't support it.
830 if (!thegame
.can_solve
)
831 js_remove_solve_button();
834 * Retrieve the game's colours, and convert them into #abcdef type
837 colours
= midend_colours(me
, &ncolours
);
838 colour_strings
= snewn(ncolours
, char *);
839 for (i
= 0; i
< ncolours
; i
++) {
841 sprintf(col
, "#%02x%02x%02x",
842 (unsigned)(0.5 + 255 * colours
[i
*3+0]),
843 (unsigned)(0.5 + 255 * colours
[i
*3+1]),
844 (unsigned)(0.5 + 255 * colours
[i
*3+2]));
845 colour_strings
[i
] = dupstr(col
);
849 * Request notification when the game ids change (e.g. if the user
850 * presses 'n', and also when Mines supersedes its game
851 * description), so that we can proactively update the permalink.
853 midend_request_id_changes(me
, ids_changed
, NULL
);
856 * Draw the puzzle's initial state, and set up the permalinks and
857 * undo/redo greying out.
864 * If we were given an erroneous game ID in argv[1], now's the
865 * time to put up the error box about it, after we've fully set up
866 * a random puzzle. Then when the user clicks 'ok', we have a
870 js_error_box(param_err
);
873 * Done. Return to JS, and await callbacks!