diff options
Diffstat (limited to 'apps/plugins/puzzles/src/emcc.c')
-rw-r--r-- | apps/plugins/puzzles/src/emcc.c | 978 |
1 files changed, 0 insertions, 978 deletions
diff --git a/apps/plugins/puzzles/src/emcc.c b/apps/plugins/puzzles/src/emcc.c deleted file mode 100644 index 9a6c742482..0000000000 --- a/apps/plugins/puzzles/src/emcc.c +++ /dev/null | |||
@@ -1,978 +0,0 @@ | |||
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(bool undo, bool 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, bool 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(const 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(const 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 | bool 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 | bool shift, bool 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 (shift && ctrl && (keycode & 0x1F) == 26) { | ||
314 | keyevent = UI_REDO; | ||
315 | } else if (chr && chr[0] && !chr[1]) { | ||
316 | keyevent = chr[0] & 0xFF; | ||
317 | } else if (keycode >= 96 && keycode < 106) { | ||
318 | keyevent = MOD_NUM_KEYPAD | ('0' + keycode - 96); | ||
319 | } else if (keycode >= 65 && keycode <= 90) { | ||
320 | keyevent = keycode + (shift ? 0 : 32); | ||
321 | } else if (keycode >= 48 && keycode <= 57) { | ||
322 | keyevent = keycode; | ||
323 | } else if (keycode == 32) { /* space / CURSOR_SELECT2 */ | ||
324 | keyevent = keycode; | ||
325 | } | ||
326 | |||
327 | if (keyevent >= 0) { | ||
328 | if (shift && (keyevent >= 0x100 && !IS_UI_FAKE_KEY(keyevent))) | ||
329 | keyevent |= MOD_SHFT; | ||
330 | |||
331 | if (ctrl && !IS_UI_FAKE_KEY(keyevent)) { | ||
332 | if (keyevent >= 0x100) | ||
333 | keyevent |= MOD_CTRL; | ||
334 | else | ||
335 | keyevent &= 0x1F; | ||
336 | } | ||
337 | |||
338 | midend_process_key(me, 0, 0, keyevent); | ||
339 | update_undo_redo(); | ||
340 | } | ||
341 | } | ||
342 | |||
343 | /* | ||
344 | * Helper function called from several places to update the permalinks | ||
345 | * whenever a new game is created. | ||
346 | */ | ||
347 | static void update_permalinks(void) | ||
348 | { | ||
349 | char *desc, *seed; | ||
350 | desc = midend_get_game_id(me); | ||
351 | seed = midend_get_random_seed(me); | ||
352 | js_update_permalinks(desc, seed); | ||
353 | sfree(desc); | ||
354 | sfree(seed); | ||
355 | } | ||
356 | |||
357 | /* | ||
358 | * Callback from the midend when the game ids change, so we can update | ||
359 | * the permalinks. | ||
360 | */ | ||
361 | static void ids_changed(void *ignored) | ||
362 | { | ||
363 | update_permalinks(); | ||
364 | } | ||
365 | |||
366 | /* ---------------------------------------------------------------------- | ||
367 | * Implementation of the drawing API by calling Javascript canvas | ||
368 | * drawing functions. (Well, half of it; the other half is on the JS | ||
369 | * side.) | ||
370 | */ | ||
371 | static void js_start_draw(void *handle) | ||
372 | { | ||
373 | js_canvas_start_draw(); | ||
374 | } | ||
375 | |||
376 | static void js_clip(void *handle, int x, int y, int w, int h) | ||
377 | { | ||
378 | js_canvas_clip_rect(x, y, w, h); | ||
379 | } | ||
380 | |||
381 | static void js_unclip(void *handle) | ||
382 | { | ||
383 | js_canvas_unclip(); | ||
384 | } | ||
385 | |||
386 | static void js_draw_text(void *handle, int x, int y, int fonttype, | ||
387 | int fontsize, int align, int colour, | ||
388 | const char *text) | ||
389 | { | ||
390 | char fontstyle[80]; | ||
391 | int halign; | ||
392 | |||
393 | sprintf(fontstyle, "%dpx %s", fontsize, | ||
394 | fonttype == FONT_FIXED ? "monospace" : "sans-serif"); | ||
395 | |||
396 | if (align & ALIGN_VCENTRE) | ||
397 | y += js_canvas_find_font_midpoint(fontsize, fontstyle); | ||
398 | |||
399 | if (align & ALIGN_HCENTRE) | ||
400 | halign = 1; | ||
401 | else if (align & ALIGN_HRIGHT) | ||
402 | halign = 2; | ||
403 | else | ||
404 | halign = 0; | ||
405 | |||
406 | js_canvas_draw_text(x, y, halign, colour_strings[colour], fontstyle, text); | ||
407 | } | ||
408 | |||
409 | static void js_draw_rect(void *handle, int x, int y, int w, int h, int colour) | ||
410 | { | ||
411 | js_canvas_draw_rect(x, y, w, h, colour_strings[colour]); | ||
412 | } | ||
413 | |||
414 | static void js_draw_line(void *handle, int x1, int y1, int x2, int y2, | ||
415 | int colour) | ||
416 | { | ||
417 | js_canvas_draw_line(x1, y1, x2, y2, 1, colour_strings[colour]); | ||
418 | } | ||
419 | |||
420 | static void js_draw_thick_line(void *handle, float thickness, | ||
421 | float x1, float y1, float x2, float y2, | ||
422 | int colour) | ||
423 | { | ||
424 | js_canvas_draw_line(x1, y1, x2, y2, thickness, colour_strings[colour]); | ||
425 | } | ||
426 | |||
427 | static void js_draw_poly(void *handle, int *coords, int npoints, | ||
428 | int fillcolour, int outlinecolour) | ||
429 | { | ||
430 | js_canvas_draw_poly(coords, npoints, | ||
431 | fillcolour >= 0 ? colour_strings[fillcolour] : NULL, | ||
432 | colour_strings[outlinecolour]); | ||
433 | } | ||
434 | |||
435 | static void js_draw_circle(void *handle, int cx, int cy, int radius, | ||
436 | int fillcolour, int outlinecolour) | ||
437 | { | ||
438 | js_canvas_draw_circle(cx, cy, radius, | ||
439 | fillcolour >= 0 ? colour_strings[fillcolour] : NULL, | ||
440 | colour_strings[outlinecolour]); | ||
441 | } | ||
442 | |||
443 | struct blitter { | ||
444 | int id; /* allocated on the js side */ | ||
445 | int w, h; /* easier to retain here */ | ||
446 | }; | ||
447 | |||
448 | static blitter *js_blitter_new(void *handle, int w, int h) | ||
449 | { | ||
450 | blitter *bl = snew(blitter); | ||
451 | bl->w = w; | ||
452 | bl->h = h; | ||
453 | bl->id = js_canvas_new_blitter(w, h); | ||
454 | return bl; | ||
455 | } | ||
456 | |||
457 | static void js_blitter_free(void *handle, blitter *bl) | ||
458 | { | ||
459 | js_canvas_free_blitter(bl->id); | ||
460 | sfree(bl); | ||
461 | } | ||
462 | |||
463 | static void trim_rect(int *x, int *y, int *w, int *h) | ||
464 | { | ||
465 | int x0, x1, y0, y1; | ||
466 | |||
467 | /* | ||
468 | * Reduce the size of the copied rectangle to stop it going | ||
469 | * outside the bounds of the canvas. | ||
470 | */ | ||
471 | |||
472 | /* Transform from x,y,w,h form into coordinates of all edges */ | ||
473 | x0 = *x; | ||
474 | y0 = *y; | ||
475 | x1 = *x + *w; | ||
476 | y1 = *y + *h; | ||
477 | |||
478 | /* Clip each coordinate at both extremes of the canvas */ | ||
479 | x0 = (x0 < 0 ? 0 : x0 > canvas_w ? canvas_w : x0); | ||
480 | x1 = (x1 < 0 ? 0 : x1 > canvas_w ? canvas_w : x1); | ||
481 | y0 = (y0 < 0 ? 0 : y0 > canvas_h ? canvas_h : y0); | ||
482 | y1 = (y1 < 0 ? 0 : y1 > canvas_h ? canvas_h : y1); | ||
483 | |||
484 | /* Transform back into x,y,w,h to return */ | ||
485 | *x = x0; | ||
486 | *y = y0; | ||
487 | *w = x1 - x0; | ||
488 | *h = y1 - y0; | ||
489 | } | ||
490 | |||
491 | static void js_blitter_save(void *handle, blitter *bl, int x, int y) | ||
492 | { | ||
493 | int w = bl->w, h = bl->h; | ||
494 | trim_rect(&x, &y, &w, &h); | ||
495 | if (w > 0 && h > 0) | ||
496 | js_canvas_copy_to_blitter(bl->id, x, y, w, h); | ||
497 | } | ||
498 | |||
499 | static void js_blitter_load(void *handle, blitter *bl, int x, int y) | ||
500 | { | ||
501 | int w = bl->w, h = bl->h; | ||
502 | trim_rect(&x, &y, &w, &h); | ||
503 | if (w > 0 && h > 0) | ||
504 | js_canvas_copy_from_blitter(bl->id, x, y, w, h); | ||
505 | } | ||
506 | |||
507 | static void js_draw_update(void *handle, int x, int y, int w, int h) | ||
508 | { | ||
509 | trim_rect(&x, &y, &w, &h); | ||
510 | if (w > 0 && h > 0) | ||
511 | js_canvas_draw_update(x, y, w, h); | ||
512 | } | ||
513 | |||
514 | static void js_end_draw(void *handle) | ||
515 | { | ||
516 | js_canvas_end_draw(); | ||
517 | } | ||
518 | |||
519 | static void js_status_bar(void *handle, const char *text) | ||
520 | { | ||
521 | js_canvas_set_statusbar(text); | ||
522 | } | ||
523 | |||
524 | static char *js_text_fallback(void *handle, const char *const *strings, | ||
525 | int nstrings) | ||
526 | { | ||
527 | return dupstr(strings[0]); /* Emscripten has no trouble with UTF-8 */ | ||
528 | } | ||
529 | |||
530 | const struct drawing_api js_drawing = { | ||
531 | js_draw_text, | ||
532 | js_draw_rect, | ||
533 | js_draw_line, | ||
534 | js_draw_poly, | ||
535 | js_draw_circle, | ||
536 | js_draw_update, | ||
537 | js_clip, | ||
538 | js_unclip, | ||
539 | js_start_draw, | ||
540 | js_end_draw, | ||
541 | js_status_bar, | ||
542 | js_blitter_new, | ||
543 | js_blitter_free, | ||
544 | js_blitter_save, | ||
545 | js_blitter_load, | ||
546 | NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */ | ||
547 | NULL, NULL, /* line_width, line_dotted */ | ||
548 | js_text_fallback, | ||
549 | js_draw_thick_line, | ||
550 | }; | ||
551 | |||
552 | /* ---------------------------------------------------------------------- | ||
553 | * Presets and game-configuration dialog support. | ||
554 | */ | ||
555 | static game_params **presets; | ||
556 | static int npresets; | ||
557 | bool have_presets_dropdown; | ||
558 | |||
559 | void populate_js_preset_menu(int menuid, struct preset_menu *menu) | ||
560 | { | ||
561 | int i; | ||
562 | for (i = 0; i < menu->n_entries; i++) { | ||
563 | struct preset_menu_entry *entry = &menu->entries[i]; | ||
564 | if (entry->params) { | ||
565 | presets[entry->id] = entry->params; | ||
566 | js_add_preset(menuid, entry->title, entry->id); | ||
567 | } else { | ||
568 | int js_submenu = js_add_preset_submenu(menuid, entry->title); | ||
569 | populate_js_preset_menu(js_submenu, entry->submenu); | ||
570 | } | ||
571 | } | ||
572 | } | ||
573 | |||
574 | void select_appropriate_preset(void) | ||
575 | { | ||
576 | if (have_presets_dropdown) { | ||
577 | int preset = midend_which_preset(me); | ||
578 | js_select_preset(preset < 0 ? -1 : preset); | ||
579 | } | ||
580 | } | ||
581 | |||
582 | static config_item *cfg = NULL; | ||
583 | static int cfg_which; | ||
584 | |||
585 | /* | ||
586 | * Set up a dialog box. This is pretty easy on the C side; most of the | ||
587 | * work is done in JS. | ||
588 | */ | ||
589 | static void cfg_start(int which) | ||
590 | { | ||
591 | char *title; | ||
592 | int i; | ||
593 | |||
594 | cfg = midend_get_config(me, which, &title); | ||
595 | cfg_which = which; | ||
596 | |||
597 | js_dialog_init(title); | ||
598 | sfree(title); | ||
599 | |||
600 | for (i = 0; cfg[i].type != C_END; i++) { | ||
601 | switch (cfg[i].type) { | ||
602 | case C_STRING: | ||
603 | js_dialog_string(i, cfg[i].name, cfg[i].u.string.sval); | ||
604 | break; | ||
605 | case C_BOOLEAN: | ||
606 | js_dialog_boolean(i, cfg[i].name, cfg[i].u.boolean.bval); | ||
607 | break; | ||
608 | case C_CHOICES: | ||
609 | js_dialog_choices(i, cfg[i].name, cfg[i].u.choices.choicenames, | ||
610 | cfg[i].u.choices.selected); | ||
611 | break; | ||
612 | } | ||
613 | } | ||
614 | |||
615 | js_dialog_launch(); | ||
616 | } | ||
617 | |||
618 | /* | ||
619 | * Callbacks from JS when the OK button is clicked, to return the | ||
620 | * final state of each control. | ||
621 | */ | ||
622 | void dlg_return_sval(int index, const char *val) | ||
623 | { | ||
624 | config_item *i = cfg + index; | ||
625 | switch (i->type) { | ||
626 | case C_STRING: | ||
627 | sfree(i->u.string.sval); | ||
628 | i->u.string.sval = dupstr(val); | ||
629 | break; | ||
630 | default: | ||
631 | assert(0 && "Bad type for return_sval"); | ||
632 | } | ||
633 | } | ||
634 | void dlg_return_ival(int index, int val) | ||
635 | { | ||
636 | config_item *i = cfg + index; | ||
637 | switch (i->type) { | ||
638 | case C_BOOLEAN: | ||
639 | i->u.boolean.bval = val; | ||
640 | break; | ||
641 | case C_CHOICES: | ||
642 | i->u.choices.selected = val; | ||
643 | break; | ||
644 | default: | ||
645 | assert(0 && "Bad type for return_ival"); | ||
646 | } | ||
647 | } | ||
648 | |||
649 | /* | ||
650 | * Called when the user clicks OK or Cancel. use_results will be true | ||
651 | * or false respectively, in those cases. We terminate the dialog box, | ||
652 | * unless the user selected an invalid combination of parameters. | ||
653 | */ | ||
654 | static void cfg_end(bool use_results) | ||
655 | { | ||
656 | if (use_results) { | ||
657 | /* | ||
658 | * User hit OK. | ||
659 | */ | ||
660 | const char *err = midend_set_config(me, cfg_which, cfg); | ||
661 | |||
662 | if (err) { | ||
663 | /* | ||
664 | * The settings were unacceptable, so leave the config box | ||
665 | * open for the user to adjust them and try again. | ||
666 | */ | ||
667 | js_error_box(err); | ||
668 | } else { | ||
669 | /* | ||
670 | * New settings are fine; start a new game and close the | ||
671 | * dialog. | ||
672 | */ | ||
673 | select_appropriate_preset(); | ||
674 | midend_new_game(me); | ||
675 | resize(); | ||
676 | midend_redraw(me); | ||
677 | free_cfg(cfg); | ||
678 | js_dialog_cleanup(); | ||
679 | } | ||
680 | } else { | ||
681 | /* | ||
682 | * User hit Cancel. Close the dialog, but also we must still | ||
683 | * reselect the right element of the dropdown list. | ||
684 | * | ||
685 | * (Because: imagine you have a preset selected, and then you | ||
686 | * select Custom from the list, but change your mind and hit | ||
687 | * Esc. The Custom option will now still be selected in the | ||
688 | * list, whereas obviously it should show the preset you still | ||
689 | * _actually_ have selected. Worse still, it'll be the visible | ||
690 | * rather than invisible Custom option - see the comment in | ||
691 | * js_add_preset in emcclib.js - so you won't even be able to | ||
692 | * select Custom without a faffy workaround.) | ||
693 | */ | ||
694 | select_appropriate_preset(); | ||
695 | |||
696 | free_cfg(cfg); | ||
697 | js_dialog_cleanup(); | ||
698 | } | ||
699 | } | ||
700 | |||
701 | /* ---------------------------------------------------------------------- | ||
702 | * Called from JS when a command is given to the puzzle by clicking a | ||
703 | * button or control of some sort. | ||
704 | */ | ||
705 | void command(int n) | ||
706 | { | ||
707 | switch (n) { | ||
708 | case 0: /* specific game ID */ | ||
709 | cfg_start(CFG_DESC); | ||
710 | break; | ||
711 | case 1: /* random game seed */ | ||
712 | cfg_start(CFG_SEED); | ||
713 | break; | ||
714 | case 2: /* game parameter dropdown changed */ | ||
715 | { | ||
716 | int i = js_get_selected_preset(); | ||
717 | if (i < 0) { | ||
718 | /* | ||
719 | * The user selected 'Custom', so launch the config | ||
720 | * box. | ||
721 | */ | ||
722 | if (thegame.can_configure) /* (double-check just in case) */ | ||
723 | cfg_start(CFG_SETTINGS); | ||
724 | } else { | ||
725 | /* | ||
726 | * The user selected a preset, so just switch straight | ||
727 | * to that. | ||
728 | */ | ||
729 | assert(i < npresets); | ||
730 | midend_set_params(me, presets[i]); | ||
731 | midend_new_game(me); | ||
732 | resize(); | ||
733 | midend_redraw(me); | ||
734 | update_undo_redo(); | ||
735 | js_focus_canvas(); | ||
736 | select_appropriate_preset(); | ||
737 | } | ||
738 | } | ||
739 | break; | ||
740 | case 3: /* OK clicked in a config box */ | ||
741 | cfg_end(true); | ||
742 | update_undo_redo(); | ||
743 | break; | ||
744 | case 4: /* Cancel clicked in a config box */ | ||
745 | cfg_end(false); | ||
746 | update_undo_redo(); | ||
747 | break; | ||
748 | case 5: /* New Game */ | ||
749 | midend_process_key(me, 0, 0, UI_NEWGAME); | ||
750 | update_undo_redo(); | ||
751 | js_focus_canvas(); | ||
752 | break; | ||
753 | case 6: /* Restart */ | ||
754 | midend_restart_game(me); | ||
755 | update_undo_redo(); | ||
756 | js_focus_canvas(); | ||
757 | break; | ||
758 | case 7: /* Undo */ | ||
759 | midend_process_key(me, 0, 0, UI_UNDO); | ||
760 | update_undo_redo(); | ||
761 | js_focus_canvas(); | ||
762 | break; | ||
763 | case 8: /* Redo */ | ||
764 | midend_process_key(me, 0, 0, UI_REDO); | ||
765 | update_undo_redo(); | ||
766 | js_focus_canvas(); | ||
767 | break; | ||
768 | case 9: /* Solve */ | ||
769 | if (thegame.can_solve) { | ||
770 | const char *msg = midend_solve(me); | ||
771 | if (msg) | ||
772 | js_error_box(msg); | ||
773 | } | ||
774 | update_undo_redo(); | ||
775 | js_focus_canvas(); | ||
776 | break; | ||
777 | } | ||
778 | } | ||
779 | |||
780 | /* ---------------------------------------------------------------------- | ||
781 | * Called from JS to prepare a save-game file, and free one after it's | ||
782 | * been used. | ||
783 | */ | ||
784 | |||
785 | struct savefile_write_ctx { | ||
786 | char *buffer; | ||
787 | size_t pos; | ||
788 | }; | ||
789 | |||
790 | static void savefile_write(void *vctx, const void *buf, int len) | ||
791 | { | ||
792 | struct savefile_write_ctx *ctx = (struct savefile_write_ctx *)vctx; | ||
793 | if (ctx->buffer) | ||
794 | memcpy(ctx->buffer + ctx->pos, buf, len); | ||
795 | ctx->pos += len; | ||
796 | } | ||
797 | |||
798 | char *get_save_file(void) | ||
799 | { | ||
800 | struct savefile_write_ctx ctx; | ||
801 | size_t size; | ||
802 | |||
803 | /* First pass, to count up the size */ | ||
804 | ctx.buffer = NULL; | ||
805 | ctx.pos = 0; | ||
806 | midend_serialise(me, savefile_write, &ctx); | ||
807 | size = ctx.pos; | ||
808 | |||
809 | /* Second pass, to actually write out the data. We have to put a | ||
810 | * terminating \0 on the end (which we expect never to show up in | ||
811 | * the actual serialisation format - it's text, not binary) so | ||
812 | * that the Javascript side can easily find out the length. */ | ||
813 | ctx.buffer = snewn(size+1, char); | ||
814 | ctx.pos = 0; | ||
815 | midend_serialise(me, savefile_write, &ctx); | ||
816 | assert(ctx.pos == size); | ||
817 | ctx.buffer[ctx.pos] = '\0'; | ||
818 | |||
819 | return ctx.buffer; | ||
820 | } | ||
821 | |||
822 | void free_save_file(char *buffer) | ||
823 | { | ||
824 | sfree(buffer); | ||
825 | } | ||
826 | |||
827 | struct savefile_read_ctx { | ||
828 | const char *buffer; | ||
829 | int len_remaining; | ||
830 | }; | ||
831 | |||
832 | static bool savefile_read(void *vctx, void *buf, int len) | ||
833 | { | ||
834 | struct savefile_read_ctx *ctx = (struct savefile_read_ctx *)vctx; | ||
835 | if (ctx->len_remaining < len) | ||
836 | return false; | ||
837 | memcpy(buf, ctx->buffer, len); | ||
838 | ctx->len_remaining -= len; | ||
839 | ctx->buffer += len; | ||
840 | return true; | ||
841 | } | ||
842 | |||
843 | void load_game(const char *buffer, int len) | ||
844 | { | ||
845 | struct savefile_read_ctx ctx; | ||
846 | const char *err; | ||
847 | |||
848 | ctx.buffer = buffer; | ||
849 | ctx.len_remaining = len; | ||
850 | err = midend_deserialise(me, savefile_read, &ctx); | ||
851 | |||
852 | if (err) { | ||
853 | js_error_box(err); | ||
854 | } else { | ||
855 | select_appropriate_preset(); | ||
856 | resize(); | ||
857 | midend_redraw(me); | ||
858 | } | ||
859 | } | ||
860 | |||
861 | /* ---------------------------------------------------------------------- | ||
862 | * Setup function called at page load time. It's called main() because | ||
863 | * that's the most convenient thing in Emscripten, but it's not main() | ||
864 | * in the usual sense of bounding the program's entire execution. | ||
865 | * Instead, this function returns once the initial puzzle is set up | ||
866 | * and working, and everything thereafter happens by means of JS event | ||
867 | * handlers sending us callbacks. | ||
868 | */ | ||
869 | int main(int argc, char **argv) | ||
870 | { | ||
871 | const char *param_err; | ||
872 | float *colours; | ||
873 | int i; | ||
874 | |||
875 | /* | ||
876 | * Instantiate a midend. | ||
877 | */ | ||
878 | me = midend_new(NULL, &thegame, &js_drawing, NULL); | ||
879 | |||
880 | /* | ||
881 | * Chuck in the HTML fragment ID if we have one (trimming the | ||
882 | * leading # off the front first). If that's invalid, we retain | ||
883 | * the error message and will display it at the end, after setting | ||
884 | * up a random puzzle as usual. | ||
885 | */ | ||
886 | if (argc > 1 && argv[1][0] == '#' && argv[1][1] != '\0') | ||
887 | param_err = midend_game_id(me, argv[1] + 1); | ||
888 | else | ||
889 | param_err = NULL; | ||
890 | |||
891 | /* | ||
892 | * Create either a random game or the specified one, and set the | ||
893 | * canvas size appropriately. | ||
894 | */ | ||
895 | midend_new_game(me); | ||
896 | resize(); | ||
897 | |||
898 | /* | ||
899 | * Create a status bar, if needed. | ||
900 | */ | ||
901 | if (midend_wants_statusbar(me)) | ||
902 | js_canvas_make_statusbar(); | ||
903 | |||
904 | /* | ||
905 | * Set up the game-type dropdown with presets and/or the Custom | ||
906 | * option. | ||
907 | */ | ||
908 | { | ||
909 | struct preset_menu *menu = midend_get_presets(me, &npresets); | ||
910 | presets = snewn(npresets, game_params *); | ||
911 | for (i = 0; i < npresets; i++) | ||
912 | presets[i] = NULL; | ||
913 | |||
914 | populate_js_preset_menu(0, menu); | ||
915 | |||
916 | if (thegame.can_configure) | ||
917 | js_add_preset(0, "Custom", -1); | ||
918 | |||
919 | have_presets_dropdown = true; | ||
920 | |||
921 | /* | ||
922 | * Now ensure the appropriate element of the presets menu | ||
923 | * starts off selected, in case it isn't the first one in the | ||
924 | * list (e.g. Slant). | ||
925 | */ | ||
926 | select_appropriate_preset(); | ||
927 | } | ||
928 | |||
929 | /* | ||
930 | * Remove the Solve button if the game doesn't support it. | ||
931 | */ | ||
932 | if (!thegame.can_solve) | ||
933 | js_remove_solve_button(); | ||
934 | |||
935 | /* | ||
936 | * Retrieve the game's colours, and convert them into #abcdef type | ||
937 | * hex ID strings. | ||
938 | */ | ||
939 | colours = midend_colours(me, &ncolours); | ||
940 | colour_strings = snewn(ncolours, char *); | ||
941 | for (i = 0; i < ncolours; i++) { | ||
942 | char col[40]; | ||
943 | sprintf(col, "#%02x%02x%02x", | ||
944 | (unsigned)(0.5 + 255 * colours[i*3+0]), | ||
945 | (unsigned)(0.5 + 255 * colours[i*3+1]), | ||
946 | (unsigned)(0.5 + 255 * colours[i*3+2])); | ||
947 | colour_strings[i] = dupstr(col); | ||
948 | } | ||
949 | |||
950 | /* | ||
951 | * Request notification when the game ids change (e.g. if the user | ||
952 | * presses 'n', and also when Mines supersedes its game | ||
953 | * description), so that we can proactively update the permalink. | ||
954 | */ | ||
955 | midend_request_id_changes(me, ids_changed, NULL); | ||
956 | |||
957 | /* | ||
958 | * Draw the puzzle's initial state, and set up the permalinks and | ||
959 | * undo/redo greying out. | ||
960 | */ | ||
961 | midend_redraw(me); | ||
962 | update_permalinks(); | ||
963 | update_undo_redo(); | ||
964 | |||
965 | /* | ||
966 | * If we were given an erroneous game ID in argv[1], now's the | ||
967 | * time to put up the error box about it, after we've fully set up | ||
968 | * a random puzzle. Then when the user clicks 'ok', we have a | ||
969 | * puzzle for them. | ||
970 | */ | ||
971 | if (param_err) | ||
972 | js_error_box(param_err); | ||
973 | |||
974 | /* | ||
975 | * Done. Return to JS, and await callbacks! | ||
976 | */ | ||
977 | return 0; | ||
978 | } | ||