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!)
56
* Extern references to Javascript functions provided in emcclib.js.
58
extern void js_debug(const char *);
59
extern void js_error_box(const char *message);
60
extern void js_remove_type_dropdown(void);
61
extern void js_remove_solve_button(void);
62
extern void js_add_preset(const char *name);
63
extern int js_get_selected_preset(void);
64
extern void js_select_preset(int n);
65
extern void js_get_date_64(unsigned *p);
66
extern void js_update_permalinks(const char *desc, const char *seed);
67
extern void js_enable_undo_redo(int undo, int redo);
68
extern void js_activate_timer();
69
extern void js_deactivate_timer();
70
extern void js_canvas_start_draw(void);
71
extern void js_canvas_draw_update(int x, int y, int w, int h);
72
extern void js_canvas_end_draw(void);
73
extern void js_canvas_draw_rect(int x, int y, int w, int h,
75
extern void js_canvas_clip_rect(int x, int y, int w, int h);
76
extern void js_canvas_unclip(void);
77
extern void js_canvas_draw_line(float x1, float y1, float x2, float y2,
78
int width, const char *colour);
79
extern void js_canvas_draw_poly(int *points, int npoints,
80
const char *fillcolour,
81
const char *outlinecolour);
82
extern void js_canvas_draw_circle(int x, int y, int r,
83
const char *fillcolour,
84
const char *outlinecolour);
85
extern int js_canvas_find_font_midpoint(int height, const char *fontptr);
86
extern void js_canvas_draw_text(int x, int y, int halign,
87
const char *colptr, const char *fontptr,
89
extern int js_canvas_new_blitter(int w, int h);
90
extern void js_canvas_free_blitter(int id);
91
extern void js_canvas_copy_to_blitter(int id, int x, int y, int w, int h);
92
extern void js_canvas_copy_from_blitter(int id, int x, int y, int w, int h);
93
extern void js_canvas_make_statusbar(void);
94
extern void js_canvas_set_statusbar(const char *text);
95
extern void js_canvas_set_size(int w, int h);
97
extern void js_dialog_init(const char *title);
98
extern void js_dialog_string(int i, const char *title, const char *initvalue);
99
extern void js_dialog_choices(int i, const char *title, const char *choicelist,
101
extern void js_dialog_boolean(int i, const char *title, int initvalue);
102
extern void js_dialog_launch(void);
103
extern void js_dialog_cleanup(void);
104
extern void js_focus_canvas(void);
107
* Call JS to get the date, and use that to initialise our random
108
* number generator to invent the first game seed.
110
void get_random_seed(void **randseed, int *randseedsize)
112
unsigned *ret = snewn(2, unsigned);
115
*randseedsize = 2*sizeof(unsigned);
119
* Fatal error, called in cases of complete despair such as when
120
* malloc() has returned NULL.
122
void fatal(char *fmt, ...)
127
strcpy(buf, "puzzle fatal error: ");
130
vsnprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), fmt, ap);
136
void debug_printf(char *fmt, ...)
141
vsnprintf(buf, sizeof(buf), fmt, ap);
147
* Helper function that makes it easy to test strings that might be
150
int strnullcmp(const char *a, const char *b)
152
if (a == NULL || b == NULL)
153
return a != NULL ? +1 : b != NULL ? -1 : 0;
158
* HTMLish names for the colours allocated by the puzzle.
160
char **colour_strings;
164
* The global midend object.
168
/* ----------------------------------------------------------------------
171
int timer_active = FALSE;
172
void deactivate_timer(frontend *fe)
174
js_deactivate_timer();
175
timer_active = FALSE;
177
void activate_timer(frontend *fe)
184
void timer_callback(double tplus)
187
midend_timer(me, tplus);
190
/* ----------------------------------------------------------------------
191
* Helper functions to resize the canvas, and variables to remember
192
* its size for other functions (e.g. trimming blitter rectangles).
194
static int canvas_w, canvas_h;
196
/* Called when we resize as a result of changing puzzle settings */
197
static void resize(void)
201
midend_size(me, &w, &h, FALSE);
202
js_canvas_set_size(w, h);
207
/* Called from JS when the user uses the resize handle */
208
void resize_puzzle(int w, int h)
210
midend_size(me, &w, &h, TRUE);
211
if (canvas_w != w || canvas_h != h) {
212
js_canvas_set_size(w, h);
215
midend_force_redraw(me);
219
/* Called from JS when the user uses the restore button */
220
void restore_puzzle_size(int w, int h)
222
midend_reset_tilesize(me);
224
midend_force_redraw(me);
228
* HTML doesn't give us a default frontend colour of its own, so we
229
* just make up a lightish grey ourselves.
231
void frontend_default_colour(frontend *fe, float *output)
233
output[0] = output[1] = output[2] = 0.9F;
237
* Helper function called from all over the place to ensure the undo
238
* and redo buttons get properly enabled and disabled after every move
239
* or undo or new-game event.
241
static void update_undo_redo(void)
243
js_enable_undo_redo(midend_can_undo(me), midend_can_redo(me));
247
* Mouse event handlers called from JS.
249
void mousedown(int x, int y, int button)
251
button = (button == 0 ? LEFT_BUTTON :
252
button == 1 ? MIDDLE_BUTTON : RIGHT_BUTTON);
253
midend_process_key(me, x, y, button);
257
void mouseup(int x, int y, int button)
259
button = (button == 0 ? LEFT_RELEASE :
260
button == 1 ? MIDDLE_RELEASE : RIGHT_RELEASE);
261
midend_process_key(me, x, y, button);
265
void mousemove(int x, int y, int buttons)
267
int button = (buttons & 2 ? MIDDLE_DRAG :
268
buttons & 4 ? RIGHT_DRAG : LEFT_DRAG);
269
midend_process_key(me, x, y, button);
274
* Keyboard handler called from JS.
276
void key(int keycode, int charcode, const char *key, const char *chr,
281
if (!strnullcmp(key, "Backspace") || !strnullcmp(key, "Del") ||
282
keycode == 8 || keycode == 46) {
283
keyevent = 127; /* Backspace / Delete */
284
} else if (!strnullcmp(key, "Enter") || keycode == 13) {
285
keyevent = 13; /* return */
286
} else if (!strnullcmp(key, "Left") || keycode == 37) {
287
keyevent = CURSOR_LEFT;
288
} else if (!strnullcmp(key, "Up") || keycode == 38) {
289
keyevent = CURSOR_UP;
290
} else if (!strnullcmp(key, "Right") || keycode == 39) {
291
keyevent = CURSOR_RIGHT;
292
} else if (!strnullcmp(key, "Down") || keycode == 40) {
293
keyevent = CURSOR_DOWN;
294
} else if (!strnullcmp(key, "End") || keycode == 35) {
296
* We interpret Home, End, PgUp and PgDn as numeric keypad
297
* controls regardless of whether they're the ones on the
298
* numeric keypad (since we can't tell). The effect of
299
* this should only be that the non-numeric-pad versions
300
* of those keys generate directions in 8-way movement
301
* puzzles like Cube and Inertia.
303
keyevent = MOD_NUM_KEYPAD | '1';
304
} else if (!strnullcmp(key, "PageDown") || keycode==34) {
305
keyevent = MOD_NUM_KEYPAD | '3';
306
} else if (!strnullcmp(key, "Home") || keycode==36) {
307
keyevent = MOD_NUM_KEYPAD | '7';
308
} else if (!strnullcmp(key, "PageUp") || keycode==33) {
309
keyevent = MOD_NUM_KEYPAD | '9';
310
} else if (chr && chr[0] && !chr[1]) {
311
keyevent = chr[0] & 0xFF;
312
} else if (keycode >= 96 && keycode < 106) {
313
keyevent = MOD_NUM_KEYPAD | ('0' + keycode - 96);
314
} else if (keycode >= 65 && keycode <= 90) {
315
keyevent = keycode + (shift ? 0 : 32);
316
} else if (keycode >= 48 && keycode <= 57) {
321
if (shift && keyevent >= 0x100)
322
keyevent |= MOD_SHFT;
325
if (keyevent >= 0x100)
326
keyevent |= MOD_CTRL;
331
midend_process_key(me, 0, 0, keyevent);
337
* Helper function called from several places to update the permalinks
338
* whenever a new game is created.
340
static void update_permalinks(void)
343
desc = midend_get_game_id(me);
344
seed = midend_get_random_seed(me);
345
js_update_permalinks(desc, seed);
351
* Callback from the midend when the game ids change, so we can update
354
static void ids_changed(void *ignored)
359
/* ----------------------------------------------------------------------
360
* Implementation of the drawing API by calling Javascript canvas
361
* drawing functions. (Well, half of it; the other half is on the JS
364
static void js_start_draw(void *handle)
366
js_canvas_start_draw();
369
static void js_clip(void *handle, int x, int y, int w, int h)
371
js_canvas_clip_rect(x, y, w, h);
374
static void js_unclip(void *handle)
379
static void js_draw_text(void *handle, int x, int y, int fonttype,
380
int fontsize, int align, int colour, char *text)
385
sprintf(fontstyle, "%dpx %s", fontsize,
386
fonttype == FONT_FIXED ? "monospace" : "sans-serif");
388
if (align & ALIGN_VCENTRE)
389
y += js_canvas_find_font_midpoint(fontsize, fontstyle);
391
if (align & ALIGN_HCENTRE)
393
else if (align & ALIGN_HRIGHT)
398
js_canvas_draw_text(x, y, halign, colour_strings[colour], fontstyle, text);
401
static void js_draw_rect(void *handle, int x, int y, int w, int h, int colour)
403
js_canvas_draw_rect(x, y, w, h, colour_strings[colour]);
406
static void js_draw_line(void *handle, int x1, int y1, int x2, int y2,
409
js_canvas_draw_line(x1, y1, x2, y2, 1, colour_strings[colour]);
412
static void js_draw_thick_line(void *handle, float thickness,
413
float x1, float y1, float x2, float y2,
416
js_canvas_draw_line(x1, y1, x2, y2, thickness, colour_strings[colour]);
419
static void js_draw_poly(void *handle, int *coords, int npoints,
420
int fillcolour, int outlinecolour)
422
js_canvas_draw_poly(coords, npoints,
423
fillcolour >= 0 ? colour_strings[fillcolour] : NULL,
424
colour_strings[outlinecolour]);
427
static void js_draw_circle(void *handle, int cx, int cy, int radius,
428
int fillcolour, int outlinecolour)
430
js_canvas_draw_circle(cx, cy, radius,
431
fillcolour >= 0 ? colour_strings[fillcolour] : NULL,
432
colour_strings[outlinecolour]);
436
int id; /* allocated on the js side */
437
int w, h; /* easier to retain here */
440
static blitter *js_blitter_new(void *handle, int w, int h)
442
blitter *bl = snew(blitter);
445
bl->id = js_canvas_new_blitter(w, h);
449
static void js_blitter_free(void *handle, blitter *bl)
451
js_canvas_free_blitter(bl->id);
455
static void trim_rect(int *x, int *y, int *w, int *h)
460
* Reduce the size of the copied rectangle to stop it going
461
* outside the bounds of the canvas.
464
/* Transform from x,y,w,h form into coordinates of all edges */
470
/* Clip each coordinate at both extremes of the canvas */
471
x0 = (x0 < 0 ? 0 : x0 > canvas_w ? canvas_w : x0);
472
x1 = (x1 < 0 ? 0 : x1 > canvas_w ? canvas_w : x1);
473
y0 = (y0 < 0 ? 0 : y0 > canvas_h ? canvas_h : y0);
474
y1 = (y1 < 0 ? 0 : y1 > canvas_h ? canvas_h : y1);
476
/* Transform back into x,y,w,h to return */
483
static void js_blitter_save(void *handle, blitter *bl, int x, int y)
485
int w = bl->w, h = bl->h;
486
trim_rect(&x, &y, &w, &h);
488
js_canvas_copy_to_blitter(bl->id, x, y, w, h);
491
static void js_blitter_load(void *handle, blitter *bl, int x, int y)
493
int w = bl->w, h = bl->h;
494
trim_rect(&x, &y, &w, &h);
496
js_canvas_copy_from_blitter(bl->id, x, y, w, h);
499
static void js_draw_update(void *handle, int x, int y, int w, int h)
501
trim_rect(&x, &y, &w, &h);
503
js_canvas_draw_update(x, y, w, h);
506
static void js_end_draw(void *handle)
508
js_canvas_end_draw();
511
static void js_status_bar(void *handle, char *text)
513
js_canvas_set_statusbar(text);
516
static char *js_text_fallback(void *handle, const char *const *strings,
519
return dupstr(strings[0]); /* Emscripten has no trouble with UTF-8 */
522
const struct drawing_api js_drawing = {
538
NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */
539
NULL, NULL, /* line_width, line_dotted */
544
/* ----------------------------------------------------------------------
545
* Presets and game-configuration dialog support.
547
static game_params **presets;
549
int have_presets_dropdown;
551
void select_appropriate_preset(void)
553
if (have_presets_dropdown) {
554
int preset = midend_which_preset(me);
555
js_select_preset(preset < 0 ? -1 : preset);
559
static config_item *cfg = NULL;
560
static int cfg_which;
563
* Set up a dialog box. This is pretty easy on the C side; most of the
564
* work is done in JS.
566
static void cfg_start(int which)
571
cfg = midend_get_config(me, which, &title);
574
js_dialog_init(title);
577
for (i = 0; cfg[i].type != C_END; i++) {
578
switch (cfg[i].type) {
580
js_dialog_string(i, cfg[i].name, cfg[i].sval);
583
js_dialog_boolean(i, cfg[i].name, cfg[i].ival);
586
js_dialog_choices(i, cfg[i].name, cfg[i].sval, cfg[i].ival);
595
* Callbacks from JS when the OK button is clicked, to return the
596
* final state of each control.
598
void dlg_return_sval(int index, const char *val)
600
sfree(cfg[index].sval);
601
cfg[index].sval = dupstr(val);
603
void dlg_return_ival(int index, int val)
605
cfg[index].ival = val;
609
* Called when the user clicks OK or Cancel. use_results will be TRUE
610
* or FALSE respectively, in those cases. We terminate the dialog box,
611
* unless the user selected an invalid combination of parameters.
613
static void cfg_end(int use_results)
619
char *err = midend_set_config(me, cfg_which, cfg);
623
* The settings were unacceptable, so leave the config box
624
* open for the user to adjust them and try again.
629
* New settings are fine; start a new game and close the
632
select_appropriate_preset();
641
* User hit Cancel. Close the dialog, but also we must still
642
* reselect the right element of the dropdown list.
644
* (Because: imagine you have a preset selected, and then you
645
* select Custom from the list, but change your mind and hit
646
* Esc. The Custom option will now still be selected in the
647
* list, whereas obviously it should show the preset you still
648
* _actually_ have selected. Worse still, it'll be the visible
649
* rather than invisible Custom option - see the comment in
650
* js_add_preset in emcclib.js - so you won't even be able to
651
* select Custom without a faffy workaround.)
653
select_appropriate_preset();
660
/* ----------------------------------------------------------------------
661
* Called from JS when a command is given to the puzzle by clicking a
662
* button or control of some sort.
667
case 0: /* specific game ID */
670
case 1: /* random game seed */
673
case 2: /* game parameter dropdown changed */
675
int i = js_get_selected_preset();
678
* The user selected 'Custom', so launch the config
681
if (thegame.can_configure) /* (double-check just in case) */
682
cfg_start(CFG_SETTINGS);
685
* The user selected a preset, so just switch straight
688
assert(i < npresets);
689
midend_set_params(me, presets[i]);
695
select_appropriate_preset(); /* sort out Custom/Customise */
699
case 3: /* OK clicked in a config box */
703
case 4: /* Cancel clicked in a config box */
707
case 5: /* New Game */
708
midend_process_key(me, 0, 0, 'n');
712
case 6: /* Restart */
713
midend_restart_game(me);
718
midend_process_key(me, 0, 0, 'u');
723
midend_process_key(me, 0, 0, 'r');
728
if (thegame.can_solve) {
729
char *msg = midend_solve(me);
739
/* ----------------------------------------------------------------------
740
* Setup function called at page load time. It's called main() because
741
* that's the most convenient thing in Emscripten, but it's not main()
742
* in the usual sense of bounding the program's entire execution.
743
* Instead, this function returns once the initial puzzle is set up
744
* and working, and everything thereafter happens by means of JS event
745
* handlers sending us callbacks.
747
int main(int argc, char **argv)
754
* Instantiate a midend.
756
me = midend_new(NULL, &thegame, &js_drawing, NULL);
759
* Chuck in the HTML fragment ID if we have one (trimming the
760
* leading # off the front first). If that's invalid, we retain
761
* the error message and will display it at the end, after setting
762
* up a random puzzle as usual.
764
if (argc > 1 && argv[1][0] == '#' && argv[1][1] != '\0')
765
param_err = midend_game_id(me, argv[1] + 1);
770
* Create either a random game or the specified one, and set the
771
* canvas size appropriately.
777
* Create a status bar, if needed.
779
if (midend_wants_statusbar(me))
780
js_canvas_make_statusbar();
783
* Set up the game-type dropdown with presets and/or the Custom
786
npresets = midend_num_presets(me);
789
* This puzzle doesn't have selectable game types at all.
790
* Completely remove the drop-down list from the page.
792
js_remove_type_dropdown();
793
have_presets_dropdown = FALSE;
797
presets = snewn(npresets, game_params *);
798
for (i = 0; i < npresets; i++) {
800
midend_fetch_preset(me, i, &name, &presets[i]);
803
if (thegame.can_configure)
804
js_add_preset(NULL); /* the 'Custom' entry in the dropdown */
806
have_presets_dropdown = TRUE;
809
* Now ensure the appropriate element of the presets menu
810
* starts off selected, in case it isn't the first one in the
813
select_appropriate_preset();
817
* Remove the Solve button if the game doesn't support it.
819
if (!thegame.can_solve)
820
js_remove_solve_button();
823
* Retrieve the game's colours, and convert them into #abcdef type
826
colours = midend_colours(me, &ncolours);
827
colour_strings = snewn(ncolours, char *);
828
for (i = 0; i < ncolours; i++) {
830
sprintf(col, "#%02x%02x%02x",
831
(unsigned)(0.5 + 255 * colours[i*3+0]),
832
(unsigned)(0.5 + 255 * colours[i*3+1]),
833
(unsigned)(0.5 + 255 * colours[i*3+2]));
834
colour_strings[i] = dupstr(col);
838
* Request notification when the game ids change (e.g. if the user
839
* presses 'n', and also when Mines supersedes its game
840
* description), so that we can proactively update the permalink.
842
midend_request_id_changes(me, ids_changed, NULL);
845
* Draw the puzzle's initial state, and set up the permalinks and
846
* undo/redo greying out.
853
* If we were given an erroneous game ID in argv[1], now's the
854
* time to put up the error box about it, after we've fully set up
855
* a random puzzle. Then when the user clicks 'ok', we have a
859
js_error_box(param_err);
862
* Done. Return to JS, and await callbacks!