diff options
Diffstat (limited to 'apps/plugins/puzzles/src/emcc.c')
-rw-r--r-- | apps/plugins/puzzles/src/emcc.c | 876 |
1 files changed, 876 insertions, 0 deletions
diff --git a/apps/plugins/puzzles/src/emcc.c b/apps/plugins/puzzles/src/emcc.c new file mode 100644 index 0000000000..ca033cbd47 --- /dev/null +++ b/apps/plugins/puzzles/src/emcc.c | |||
@@ -0,0 +1,876 @@ | |||
1 | /* | ||
2 | * emcc.c: the C component of an Emscripten-based web/Javascript front | ||
3 | * end for Puzzles. | ||
4 | * | ||
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. | ||
10 | */ | ||
11 | |||
12 | /* | ||
13 | * Further thoughts on possible enhancements: | ||
14 | * | ||
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. | ||
23 | * | ||
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 | ||
27 | * be worthwhile). | ||
28 | * | ||
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. | ||
37 | * | ||
38 | * - some CSS to make the button bar and configuration dialogs a | ||
39 | * little less ugly would probably not go amiss. | ||
40 | * | ||
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!) | ||
48 | */ | ||
49 | |||
50 | #include <assert.h> | ||
51 | #include <stdio.h> | ||
52 | #include <string.h> | ||
53 | #include <stdarg.h> | ||
54 | |||
55 | #include "puzzles.h" | ||
56 | |||
57 | /* | ||
58 | * Extern references to Javascript functions provided in emcclib.js. | ||
59 | */ | ||
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, | ||
77 | const char *colour); | ||
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, | ||
91 | const char *text); | ||
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); | ||
99 | |||
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, | ||
103 | int initvalue); | ||
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); | ||
108 | |||
109 | /* | ||
110 | * Call JS to get the date, and use that to initialise our random | ||
111 | * number generator to invent the first game seed. | ||
112 | */ | ||
113 | void get_random_seed(void **randseed, int *randseedsize) | ||
114 | { | ||
115 | unsigned *ret = snewn(2, unsigned); | ||
116 | js_get_date_64(ret); | ||
117 | *randseed = ret; | ||
118 | *randseedsize = 2*sizeof(unsigned); | ||
119 | } | ||
120 | |||
121 | /* | ||
122 | * Fatal error, called in cases of complete despair such as when | ||
123 | * malloc() has returned NULL. | ||
124 | */ | ||
125 | void fatal(char *fmt, ...) | ||
126 | { | ||
127 | char buf[512]; | ||
128 | va_list ap; | ||
129 | |||
130 | strcpy(buf, "puzzle fatal error: "); | ||
131 | |||
132 | va_start(ap, fmt); | ||
133 | vsnprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), fmt, ap); | ||
134 | va_end(ap); | ||
135 | |||
136 | js_error_box(buf); | ||
137 | } | ||
138 | |||
139 | void debug_printf(char *fmt, ...) | ||
140 | { | ||
141 | char buf[512]; | ||
142 | va_list ap; | ||
143 | va_start(ap, fmt); | ||
144 | vsnprintf(buf, sizeof(buf), fmt, ap); | ||
145 | va_end(ap); | ||
146 | js_debug(buf); | ||
147 | } | ||
148 | |||
149 | /* | ||
150 | * Helper function that makes it easy to test strings that might be | ||
151 | * NULL. | ||
152 | */ | ||
153 | int strnullcmp(const char *a, const char *b) | ||
154 | { | ||
155 | if (a == NULL || b == NULL) | ||
156 | return a != NULL ? +1 : b != NULL ? -1 : 0; | ||
157 | return strcmp(a, b); | ||
158 | } | ||
159 | |||
160 | /* | ||
161 | * HTMLish names for the colours allocated by the puzzle. | ||
162 | */ | ||
163 | char **colour_strings; | ||
164 | int ncolours; | ||
165 | |||
166 | /* | ||
167 | * The global midend object. | ||
168 | */ | ||
169 | midend *me; | ||
170 | |||
171 | /* ---------------------------------------------------------------------- | ||
172 | * Timing functions. | ||
173 | */ | ||
174 | int timer_active = FALSE; | ||
175 | void deactivate_timer(frontend *fe) | ||
176 | { | ||
177 | js_deactivate_timer(); | ||
178 | timer_active = FALSE; | ||
179 | } | ||
180 | void activate_timer(frontend *fe) | ||
181 | { | ||
182 | if (!timer_active) { | ||
183 | js_activate_timer(); | ||
184 | timer_active = TRUE; | ||
185 | } | ||
186 | } | ||
187 | void timer_callback(double tplus) | ||
188 | { | ||
189 | if (timer_active) | ||
190 | midend_timer(me, tplus); | ||
191 | } | ||
192 | |||
193 | /* ---------------------------------------------------------------------- | ||
194 | * Helper functions to resize the canvas, and variables to remember | ||
195 | * its size for other functions (e.g. trimming blitter rectangles). | ||
196 | */ | ||
197 | static int canvas_w, canvas_h; | ||
198 | |||
199 | /* Called when we resize as a result of changing puzzle settings */ | ||
200 | static void resize(void) | ||
201 | { | ||
202 | int w, h; | ||
203 | w = h = INT_MAX; | ||
204 | midend_size(me, &w, &h, FALSE); | ||
205 | js_canvas_set_size(w, h); | ||
206 | canvas_w = w; | ||
207 | canvas_h = h; | ||
208 | } | ||
209 | |||
210 | /* Called from JS when the user uses the resize handle */ | ||
211 | void resize_puzzle(int w, int h) | ||
212 | { | ||
213 | midend_size(me, &w, &h, TRUE); | ||
214 | if (canvas_w != w || canvas_h != h) { | ||
215 | js_canvas_set_size(w, h); | ||
216 | canvas_w = w; | ||
217 | canvas_h = h; | ||
218 | midend_force_redraw(me); | ||
219 | } | ||
220 | } | ||
221 | |||
222 | /* Called from JS when the user uses the restore button */ | ||
223 | void restore_puzzle_size(int w, int h) | ||
224 | { | ||
225 | midend_reset_tilesize(me); | ||
226 | resize(); | ||
227 | midend_force_redraw(me); | ||
228 | } | ||
229 | |||
230 | /* | ||
231 | * HTML doesn't give us a default frontend colour of its own, so we | ||
232 | * just make up a lightish grey ourselves. | ||
233 | */ | ||
234 | void frontend_default_colour(frontend *fe, float *output) | ||
235 | { | ||
236 | output[0] = output[1] = output[2] = 0.9F; | ||
237 | } | ||
238 | |||
239 | /* | ||
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. | ||
243 | */ | ||
244 | static void update_undo_redo(void) | ||
245 | { | ||
246 | js_enable_undo_redo(midend_can_undo(me), midend_can_redo(me)); | ||
247 | } | ||
248 | |||
249 | /* | ||
250 | * Mouse event handlers called from JS. | ||
251 | */ | ||
252 | void mousedown(int x, int y, int button) | ||
253 | { | ||
254 | button = (button == 0 ? LEFT_BUTTON : | ||
255 | button == 1 ? MIDDLE_BUTTON : RIGHT_BUTTON); | ||
256 | midend_process_key(me, x, y, button); | ||
257 | update_undo_redo(); | ||
258 | } | ||
259 | |||
260 | void mouseup(int x, int y, int button) | ||
261 | { | ||
262 | button = (button == 0 ? LEFT_RELEASE : | ||
263 | button == 1 ? MIDDLE_RELEASE : RIGHT_RELEASE); | ||
264 | midend_process_key(me, x, y, button); | ||
265 | update_undo_redo(); | ||
266 | } | ||
267 | |||
268 | void mousemove(int x, int y, int buttons) | ||
269 | { | ||
270 | int button = (buttons & 2 ? MIDDLE_DRAG : | ||
271 | buttons & 4 ? RIGHT_DRAG : LEFT_DRAG); | ||
272 | midend_process_key(me, x, y, button); | ||
273 | update_undo_redo(); | ||
274 | } | ||
275 | |||
276 | /* | ||
277 | * Keyboard handler called from JS. | ||
278 | */ | ||
279 | void key(int keycode, int charcode, const char *key, const char *chr, | ||
280 | int shift, int ctrl) | ||
281 | { | ||
282 | int keyevent = -1; | ||
283 | |||
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) { | ||
298 | /* | ||
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. | ||
305 | */ | ||
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) { | ||
320 | keyevent = keycode; | ||
321 | } else if (keycode == 32) { /* space / CURSOR_SELECT2 */ | ||
322 | keyevent = keycode; | ||
323 | } | ||
324 | |||
325 | if (keyevent >= 0) { | ||
326 | if (shift && keyevent >= 0x100) | ||
327 | keyevent |= MOD_SHFT; | ||
328 | |||
329 | if (ctrl) { | ||
330 | if (keyevent >= 0x100) | ||
331 | keyevent |= MOD_CTRL; | ||
332 | else | ||
333 | keyevent &= 0x1F; | ||
334 | } | ||
335 | |||
336 | midend_process_key(me, 0, 0, keyevent); | ||
337 | update_undo_redo(); | ||
338 | } | ||
339 | } | ||
340 | |||
341 | /* | ||
342 | * Helper function called from several places to update the permalinks | ||
343 | * whenever a new game is created. | ||
344 | */ | ||
345 | static void update_permalinks(void) | ||
346 | { | ||
347 | char *desc, *seed; | ||
348 | desc = midend_get_game_id(me); | ||
349 | seed = midend_get_random_seed(me); | ||
350 | js_update_permalinks(desc, seed); | ||
351 | sfree(desc); | ||
352 | sfree(seed); | ||
353 | } | ||
354 | |||
355 | /* | ||
356 | * Callback from the midend when the game ids change, so we can update | ||
357 | * the permalinks. | ||
358 | */ | ||
359 | static void ids_changed(void *ignored) | ||
360 | { | ||
361 | update_permalinks(); | ||
362 | } | ||
363 | |||
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 | ||
367 | * side.) | ||
368 | */ | ||
369 | static void js_start_draw(void *handle) | ||
370 | { | ||
371 | js_canvas_start_draw(); | ||
372 | } | ||
373 | |||
374 | static void js_clip(void *handle, int x, int y, int w, int h) | ||
375 | { | ||
376 | js_canvas_clip_rect(x, y, w, h); | ||
377 | } | ||
378 | |||
379 | static void js_unclip(void *handle) | ||
380 | { | ||
381 | js_canvas_unclip(); | ||
382 | } | ||
383 | |||
384 | static void js_draw_text(void *handle, int x, int y, int fonttype, | ||
385 | int fontsize, int align, int colour, char *text) | ||
386 | { | ||
387 | char fontstyle[80]; | ||
388 | int halign; | ||
389 | |||
390 | sprintf(fontstyle, "%dpx %s", fontsize, | ||
391 | fonttype == FONT_FIXED ? "monospace" : "sans-serif"); | ||
392 | |||
393 | if (align & ALIGN_VCENTRE) | ||
394 | y += js_canvas_find_font_midpoint(fontsize, fontstyle); | ||
395 | |||
396 | if (align & ALIGN_HCENTRE) | ||
397 | halign = 1; | ||
398 | else if (align & ALIGN_HRIGHT) | ||
399 | halign = 2; | ||
400 | else | ||
401 | halign = 0; | ||
402 | |||
403 | js_canvas_draw_text(x, y, halign, colour_strings[colour], fontstyle, text); | ||
404 | } | ||
405 | |||
406 | static void js_draw_rect(void *handle, int x, int y, int w, int h, int colour) | ||
407 | { | ||
408 | js_canvas_draw_rect(x, y, w, h, colour_strings[colour]); | ||
409 | } | ||
410 | |||
411 | static void js_draw_line(void *handle, int x1, int y1, int x2, int y2, | ||
412 | int colour) | ||
413 | { | ||
414 | js_canvas_draw_line(x1, y1, x2, y2, 1, colour_strings[colour]); | ||
415 | } | ||
416 | |||
417 | static void js_draw_thick_line(void *handle, float thickness, | ||
418 | float x1, float y1, float x2, float y2, | ||
419 | int colour) | ||
420 | { | ||
421 | js_canvas_draw_line(x1, y1, x2, y2, thickness, colour_strings[colour]); | ||
422 | } | ||
423 | |||
424 | static void js_draw_poly(void *handle, int *coords, int npoints, | ||
425 | int fillcolour, int outlinecolour) | ||
426 | { | ||
427 | js_canvas_draw_poly(coords, npoints, | ||
428 | fillcolour >= 0 ? colour_strings[fillcolour] : NULL, | ||
429 | colour_strings[outlinecolour]); | ||
430 | } | ||
431 | |||
432 | static void js_draw_circle(void *handle, int cx, int cy, int radius, | ||
433 | int fillcolour, int outlinecolour) | ||
434 | { | ||
435 | js_canvas_draw_circle(cx, cy, radius, | ||
436 | fillcolour >= 0 ? colour_strings[fillcolour] : NULL, | ||
437 | colour_strings[outlinecolour]); | ||
438 | } | ||
439 | |||
440 | struct blitter { | ||
441 | int id; /* allocated on the js side */ | ||
442 | int w, h; /* easier to retain here */ | ||
443 | }; | ||
444 | |||
445 | static blitter *js_blitter_new(void *handle, int w, int h) | ||
446 | { | ||
447 | blitter *bl = snew(blitter); | ||
448 | bl->w = w; | ||
449 | bl->h = h; | ||
450 | bl->id = js_canvas_new_blitter(w, h); | ||
451 | return bl; | ||
452 | } | ||
453 | |||
454 | static void js_blitter_free(void *handle, blitter *bl) | ||
455 | { | ||
456 | js_canvas_free_blitter(bl->id); | ||
457 | sfree(bl); | ||
458 | } | ||
459 | |||
460 | static void trim_rect(int *x, int *y, int *w, int *h) | ||
461 | { | ||
462 | int x0, x1, y0, y1; | ||
463 | |||
464 | /* | ||
465 | * Reduce the size of the copied rectangle to stop it going | ||
466 | * outside the bounds of the canvas. | ||
467 | */ | ||
468 | |||
469 | /* Transform from x,y,w,h form into coordinates of all edges */ | ||
470 | x0 = *x; | ||
471 | y0 = *y; | ||
472 | x1 = *x + *w; | ||
473 | y1 = *y + *h; | ||
474 | |||
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); | ||
480 | |||
481 | /* Transform back into x,y,w,h to return */ | ||
482 | *x = x0; | ||
483 | *y = y0; | ||
484 | *w = x1 - x0; | ||
485 | *h = y1 - y0; | ||
486 | } | ||
487 | |||
488 | static void js_blitter_save(void *handle, blitter *bl, int x, int y) | ||
489 | { | ||
490 | int w = bl->w, h = bl->h; | ||
491 | trim_rect(&x, &y, &w, &h); | ||
492 | if (w > 0 && h > 0) | ||
493 | js_canvas_copy_to_blitter(bl->id, x, y, w, h); | ||
494 | } | ||
495 | |||
496 | static void js_blitter_load(void *handle, blitter *bl, int x, int y) | ||
497 | { | ||
498 | int w = bl->w, h = bl->h; | ||
499 | trim_rect(&x, &y, &w, &h); | ||
500 | if (w > 0 && h > 0) | ||
501 | js_canvas_copy_from_blitter(bl->id, x, y, w, h); | ||
502 | } | ||
503 | |||
504 | static void js_draw_update(void *handle, int x, int y, int w, int h) | ||
505 | { | ||
506 | trim_rect(&x, &y, &w, &h); | ||
507 | if (w > 0 && h > 0) | ||
508 | js_canvas_draw_update(x, y, w, h); | ||
509 | } | ||
510 | |||
511 | static void js_end_draw(void *handle) | ||
512 | { | ||
513 | js_canvas_end_draw(); | ||
514 | } | ||
515 | |||
516 | static void js_status_bar(void *handle, char *text) | ||
517 | { | ||
518 | js_canvas_set_statusbar(text); | ||
519 | } | ||
520 | |||
521 | static char *js_text_fallback(void *handle, const char *const *strings, | ||
522 | int nstrings) | ||
523 | { | ||
524 | return dupstr(strings[0]); /* Emscripten has no trouble with UTF-8 */ | ||
525 | } | ||
526 | |||
527 | const struct drawing_api js_drawing = { | ||
528 | js_draw_text, | ||
529 | js_draw_rect, | ||
530 | js_draw_line, | ||
531 | js_draw_poly, | ||
532 | js_draw_circle, | ||
533 | js_draw_update, | ||
534 | js_clip, | ||
535 | js_unclip, | ||
536 | js_start_draw, | ||
537 | js_end_draw, | ||
538 | js_status_bar, | ||
539 | js_blitter_new, | ||
540 | js_blitter_free, | ||
541 | js_blitter_save, | ||
542 | js_blitter_load, | ||
543 | NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */ | ||
544 | NULL, NULL, /* line_width, line_dotted */ | ||
545 | js_text_fallback, | ||
546 | js_draw_thick_line, | ||
547 | }; | ||
548 | |||
549 | /* ---------------------------------------------------------------------- | ||
550 | * Presets and game-configuration dialog support. | ||
551 | */ | ||
552 | static game_params **presets; | ||
553 | static int npresets; | ||
554 | int have_presets_dropdown; | ||
555 | |||
556 | void populate_js_preset_menu(int menuid, struct preset_menu *menu) | ||
557 | { | ||
558 | int i; | ||
559 | for (i = 0; i < menu->n_entries; i++) { | ||
560 | struct preset_menu_entry *entry = &menu->entries[i]; | ||
561 | if (entry->params) { | ||
562 | presets[entry->id] = entry->params; | ||
563 | js_add_preset(menuid, entry->title, entry->id); | ||
564 | } else { | ||
565 | int js_submenu = js_add_preset_submenu(menuid, entry->title); | ||
566 | populate_js_preset_menu(js_submenu, entry->submenu); | ||
567 | } | ||
568 | } | ||
569 | } | ||
570 | |||
571 | void select_appropriate_preset(void) | ||
572 | { | ||
573 | if (have_presets_dropdown) { | ||
574 | int preset = midend_which_preset(me); | ||
575 | js_select_preset(preset < 0 ? -1 : preset); | ||
576 | } | ||
577 | } | ||
578 | |||
579 | static config_item *cfg = NULL; | ||
580 | static int cfg_which; | ||
581 | |||
582 | /* | ||
583 | * Set up a dialog box. This is pretty easy on the C side; most of the | ||
584 | * work is done in JS. | ||
585 | */ | ||
586 | static void cfg_start(int which) | ||
587 | { | ||
588 | char *title; | ||
589 | int i; | ||
590 | |||
591 | cfg = midend_get_config(me, which, &title); | ||
592 | cfg_which = which; | ||
593 | |||
594 | js_dialog_init(title); | ||
595 | sfree(title); | ||
596 | |||
597 | for (i = 0; cfg[i].type != C_END; i++) { | ||
598 | switch (cfg[i].type) { | ||
599 | case C_STRING: | ||
600 | js_dialog_string(i, cfg[i].name, cfg[i].sval); | ||
601 | break; | ||
602 | case C_BOOLEAN: | ||
603 | js_dialog_boolean(i, cfg[i].name, cfg[i].ival); | ||
604 | break; | ||
605 | case C_CHOICES: | ||
606 | js_dialog_choices(i, cfg[i].name, cfg[i].sval, cfg[i].ival); | ||
607 | break; | ||
608 | } | ||
609 | } | ||
610 | |||
611 | js_dialog_launch(); | ||
612 | } | ||
613 | |||
614 | /* | ||
615 | * Callbacks from JS when the OK button is clicked, to return the | ||
616 | * final state of each control. | ||
617 | */ | ||
618 | void dlg_return_sval(int index, const char *val) | ||
619 | { | ||
620 | sfree(cfg[index].sval); | ||
621 | cfg[index].sval = dupstr(val); | ||
622 | } | ||
623 | void dlg_return_ival(int index, int val) | ||
624 | { | ||
625 | cfg[index].ival = val; | ||
626 | } | ||
627 | |||
628 | /* | ||
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. | ||
632 | */ | ||
633 | static void cfg_end(int use_results) | ||
634 | { | ||
635 | if (use_results) { | ||
636 | /* | ||
637 | * User hit OK. | ||
638 | */ | ||
639 | char *err = midend_set_config(me, cfg_which, cfg); | ||
640 | |||
641 | if (err) { | ||
642 | /* | ||
643 | * The settings were unacceptable, so leave the config box | ||
644 | * open for the user to adjust them and try again. | ||
645 | */ | ||
646 | js_error_box(err); | ||
647 | } else { | ||
648 | /* | ||
649 | * New settings are fine; start a new game and close the | ||
650 | * dialog. | ||
651 | */ | ||
652 | select_appropriate_preset(); | ||
653 | midend_new_game(me); | ||
654 | resize(); | ||
655 | midend_redraw(me); | ||
656 | free_cfg(cfg); | ||
657 | js_dialog_cleanup(); | ||
658 | } | ||
659 | } else { | ||
660 | /* | ||
661 | * User hit Cancel. Close the dialog, but also we must still | ||
662 | * reselect the right element of the dropdown list. | ||
663 | * | ||
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.) | ||
672 | */ | ||
673 | select_appropriate_preset(); | ||
674 | |||
675 | free_cfg(cfg); | ||
676 | js_dialog_cleanup(); | ||
677 | } | ||
678 | } | ||
679 | |||
680 | /* ---------------------------------------------------------------------- | ||
681 | * Called from JS when a command is given to the puzzle by clicking a | ||
682 | * button or control of some sort. | ||
683 | */ | ||
684 | void command(int n) | ||
685 | { | ||
686 | switch (n) { | ||
687 | case 0: /* specific game ID */ | ||
688 | cfg_start(CFG_DESC); | ||
689 | break; | ||
690 | case 1: /* random game seed */ | ||
691 | cfg_start(CFG_SEED); | ||
692 | break; | ||
693 | case 2: /* game parameter dropdown changed */ | ||
694 | { | ||
695 | int i = js_get_selected_preset(); | ||
696 | if (i < 0) { | ||
697 | /* | ||
698 | * The user selected 'Custom', so launch the config | ||
699 | * box. | ||
700 | */ | ||
701 | if (thegame.can_configure) /* (double-check just in case) */ | ||
702 | cfg_start(CFG_SETTINGS); | ||
703 | } else { | ||
704 | /* | ||
705 | * The user selected a preset, so just switch straight | ||
706 | * to that. | ||
707 | */ | ||
708 | assert(i < npresets); | ||
709 | midend_set_params(me, presets[i]); | ||
710 | midend_new_game(me); | ||
711 | resize(); | ||
712 | midend_redraw(me); | ||
713 | update_undo_redo(); | ||
714 | js_focus_canvas(); | ||
715 | select_appropriate_preset(); | ||
716 | } | ||
717 | } | ||
718 | break; | ||
719 | case 3: /* OK clicked in a config box */ | ||
720 | cfg_end(TRUE); | ||
721 | update_undo_redo(); | ||
722 | break; | ||
723 | case 4: /* Cancel clicked in a config box */ | ||
724 | cfg_end(FALSE); | ||
725 | update_undo_redo(); | ||
726 | break; | ||
727 | case 5: /* New Game */ | ||
728 | midend_process_key(me, 0, 0, 'n'); | ||
729 | update_undo_redo(); | ||
730 | js_focus_canvas(); | ||
731 | break; | ||
732 | case 6: /* Restart */ | ||
733 | midend_restart_game(me); | ||
734 | update_undo_redo(); | ||
735 | js_focus_canvas(); | ||
736 | break; | ||
737 | case 7: /* Undo */ | ||
738 | midend_process_key(me, 0, 0, 'u'); | ||
739 | update_undo_redo(); | ||
740 | js_focus_canvas(); | ||
741 | break; | ||
742 | case 8: /* Redo */ | ||
743 | midend_process_key(me, 0, 0, 'r'); | ||
744 | update_undo_redo(); | ||
745 | js_focus_canvas(); | ||
746 | break; | ||
747 | case 9: /* Solve */ | ||
748 | if (thegame.can_solve) { | ||
749 | char *msg = midend_solve(me); | ||
750 | if (msg) | ||
751 | js_error_box(msg); | ||
752 | } | ||
753 | update_undo_redo(); | ||
754 | js_focus_canvas(); | ||
755 | break; | ||
756 | } | ||
757 | } | ||
758 | |||
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. | ||
766 | */ | ||
767 | int main(int argc, char **argv) | ||
768 | { | ||
769 | char *param_err; | ||
770 | float *colours; | ||
771 | int i; | ||
772 | |||
773 | /* | ||
774 | * Instantiate a midend. | ||
775 | */ | ||
776 | me = midend_new(NULL, &thegame, &js_drawing, NULL); | ||
777 | |||
778 | /* | ||
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. | ||
783 | */ | ||
784 | if (argc > 1 && argv[1][0] == '#' && argv[1][1] != '\0') | ||
785 | param_err = midend_game_id(me, argv[1] + 1); | ||
786 | else | ||
787 | param_err = NULL; | ||
788 | |||
789 | /* | ||
790 | * Create either a random game or the specified one, and set the | ||
791 | * canvas size appropriately. | ||
792 | */ | ||
793 | midend_new_game(me); | ||
794 | resize(); | ||
795 | |||
796 | /* | ||
797 | * Create a status bar, if needed. | ||
798 | */ | ||
799 | if (midend_wants_statusbar(me)) | ||
800 | js_canvas_make_statusbar(); | ||
801 | |||
802 | /* | ||
803 | * Set up the game-type dropdown with presets and/or the Custom | ||
804 | * option. | ||
805 | */ | ||
806 | { | ||
807 | struct preset_menu *menu = midend_get_presets(me, &npresets); | ||
808 | presets = snewn(npresets, game_params *); | ||
809 | for (i = 0; i < npresets; i++) | ||
810 | presets[i] = NULL; | ||
811 | |||
812 | populate_js_preset_menu(0, menu); | ||
813 | |||
814 | if (thegame.can_configure) | ||
815 | js_add_preset(0, "Custom", -1); | ||
816 | |||
817 | have_presets_dropdown = TRUE; | ||
818 | |||
819 | /* | ||
820 | * Now ensure the appropriate element of the presets menu | ||
821 | * starts off selected, in case it isn't the first one in the | ||
822 | * list (e.g. Slant). | ||
823 | */ | ||
824 | select_appropriate_preset(); | ||
825 | } | ||
826 | |||
827 | /* | ||
828 | * Remove the Solve button if the game doesn't support it. | ||
829 | */ | ||
830 | if (!thegame.can_solve) | ||
831 | js_remove_solve_button(); | ||
832 | |||
833 | /* | ||
834 | * Retrieve the game's colours, and convert them into #abcdef type | ||
835 | * hex ID strings. | ||
836 | */ | ||
837 | colours = midend_colours(me, &ncolours); | ||
838 | colour_strings = snewn(ncolours, char *); | ||
839 | for (i = 0; i < ncolours; i++) { | ||
840 | char col[40]; | ||
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); | ||
846 | } | ||
847 | |||
848 | /* | ||
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. | ||
852 | */ | ||
853 | midend_request_id_changes(me, ids_changed, NULL); | ||
854 | |||
855 | /* | ||
856 | * Draw the puzzle's initial state, and set up the permalinks and | ||
857 | * undo/redo greying out. | ||
858 | */ | ||
859 | midend_redraw(me); | ||
860 | update_permalinks(); | ||
861 | update_undo_redo(); | ||
862 | |||
863 | /* | ||
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 | ||
867 | * puzzle for them. | ||
868 | */ | ||
869 | if (param_err) | ||
870 | js_error_box(param_err); | ||
871 | |||
872 | /* | ||
873 | * Done. Return to JS, and await callbacks! | ||
874 | */ | ||
875 | return 0; | ||
876 | } | ||