diff options
Diffstat (limited to 'apps/plugins/puzzles/src/emcc.c')
-rw-r--r-- | apps/plugins/puzzles/src/emcc.c | 1149 |
1 files changed, 0 insertions, 1149 deletions
diff --git a/apps/plugins/puzzles/src/emcc.c b/apps/plugins/puzzles/src/emcc.c deleted file mode 100644 index 6aa9c6b093..0000000000 --- a/apps/plugins/puzzles/src/emcc.c +++ /dev/null | |||
@@ -1,1149 +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 should think about whether these webified puzzles can support | ||
16 | * touchscreen-based tablet browsers. | ||
17 | * | ||
18 | * - think about making use of localStorage. It might be useful to | ||
19 | * let the user save games into there as an alternative to disk | ||
20 | * files - disk files are all very well for getting the save right | ||
21 | * out of your browser to (e.g.) email to me as a bug report, but | ||
22 | * for just resuming a game you were in the middle of, you'd | ||
23 | * probably rather have a nice simple 'quick save' and 'quick load' | ||
24 | * button pair. | ||
25 | * | ||
26 | * - this is a downright silly idea, but it does occur to me that if | ||
27 | * I were to write a PDF output driver for the Puzzles printing | ||
28 | * API, then I might be able to implement a sort of 'printing' | ||
29 | * feature in this front end, using data: URIs again. (Ask the user | ||
30 | * exactly what they want printed, then construct an appropriate | ||
31 | * PDF and embed it in a gigantic data: URI. Then they can print | ||
32 | * that using whatever they normally use to print PDFs!) | ||
33 | */ | ||
34 | |||
35 | #include <assert.h> | ||
36 | #include <stdio.h> | ||
37 | #include <string.h> | ||
38 | #include <stdarg.h> | ||
39 | |||
40 | #include "puzzles.h" | ||
41 | |||
42 | /* | ||
43 | * Extern references to Javascript functions provided in emcclib.js. | ||
44 | */ | ||
45 | extern void js_init_puzzle(void); | ||
46 | extern void js_post_init(void); | ||
47 | extern void js_debug(const char *); | ||
48 | extern void js_error_box(const char *message); | ||
49 | extern void js_remove_type_dropdown(void); | ||
50 | extern void js_remove_solve_button(void); | ||
51 | extern void js_add_preset(int menuid, const char *name, int value); | ||
52 | extern int js_add_preset_submenu(int menuid, const char *name); | ||
53 | extern int js_get_selected_preset(void); | ||
54 | extern void js_select_preset(int n); | ||
55 | extern void js_default_colour(float *output); | ||
56 | extern void js_set_colour(int colour_number, const char *colour_string); | ||
57 | extern void js_get_date_64(unsigned *p); | ||
58 | extern void js_update_permalinks(const char *desc, const char *seed); | ||
59 | extern void js_enable_undo_redo(bool undo, bool redo); | ||
60 | extern void js_update_key_labels(const char *lsk, const char *csk); | ||
61 | extern void js_activate_timer(void); | ||
62 | extern void js_deactivate_timer(void); | ||
63 | extern void js_canvas_start_draw(void); | ||
64 | extern void js_canvas_draw_update(int x, int y, int w, int h); | ||
65 | extern void js_canvas_end_draw(void); | ||
66 | extern void js_canvas_draw_rect(int x, int y, int w, int h, int colour); | ||
67 | extern void js_canvas_clip_rect(int x, int y, int w, int h); | ||
68 | extern void js_canvas_unclip(void); | ||
69 | extern void js_canvas_draw_line(float x1, float y1, float x2, float y2, | ||
70 | int width, int colour); | ||
71 | extern void js_canvas_draw_poly(const int *points, int npoints, | ||
72 | int fillcolour, int outlinecolour); | ||
73 | extern void js_canvas_draw_circle(int x, int y, int r, | ||
74 | int fillcolour, int outlinecolour); | ||
75 | extern int js_canvas_find_font_midpoint(int height, bool monospaced); | ||
76 | extern void js_canvas_draw_text(int x, int y, int halign, | ||
77 | int colour, int height, | ||
78 | bool monospaced, const char *text); | ||
79 | extern int js_canvas_new_blitter(int w, int h); | ||
80 | extern void js_canvas_free_blitter(int id); | ||
81 | extern void js_canvas_copy_to_blitter(int id, int x, int y, int w, int h); | ||
82 | extern void js_canvas_copy_from_blitter(int id, int x, int y, int w, int h); | ||
83 | extern void js_canvas_remove_statusbar(void); | ||
84 | extern void js_canvas_set_statusbar(const char *text); | ||
85 | extern bool js_canvas_get_preferred_size(int *wp, int *hp); | ||
86 | extern void js_canvas_set_size(int w, int h); | ||
87 | extern double js_get_device_pixel_ratio(void); | ||
88 | |||
89 | extern void js_dialog_init(const char *title); | ||
90 | extern void js_dialog_string(int i, const char *title, const char *initvalue); | ||
91 | extern void js_dialog_choices(int i, const char *title, const char *choicelist, | ||
92 | int initvalue); | ||
93 | extern void js_dialog_boolean(int i, const char *title, bool initvalue); | ||
94 | extern void js_dialog_launch(void); | ||
95 | extern void js_dialog_cleanup(void); | ||
96 | extern void js_focus_canvas(void); | ||
97 | |||
98 | extern bool js_savefile_read(void *buf, int len); | ||
99 | |||
100 | extern void js_save_prefs(const char *); | ||
101 | extern void js_load_prefs(midend *); | ||
102 | |||
103 | /* | ||
104 | * These functions are called from JavaScript, so their prototypes | ||
105 | * need to be kept in sync with emccpre.js. | ||
106 | */ | ||
107 | bool mouseup(int x, int y, int button); | ||
108 | bool mousedown(int x, int y, int button); | ||
109 | bool mousemove(int x, int y, int buttons); | ||
110 | bool key(int keycode, const char *key, const char *chr, int location, | ||
111 | bool shift, bool ctrl); | ||
112 | void timer_callback(double tplus); | ||
113 | void command(int n); | ||
114 | char *get_text_format(void); | ||
115 | void free_save_file(char *buffer); | ||
116 | char *get_save_file(void); | ||
117 | void free_save_file(char *buffer); | ||
118 | void load_game(void); | ||
119 | void dlg_return_sval(int index, const char *val); | ||
120 | void dlg_return_ival(int index, int val); | ||
121 | void resize_puzzle(int w, int h); | ||
122 | void restore_puzzle_size(int w, int h); | ||
123 | void rescale_puzzle(void); | ||
124 | |||
125 | /* | ||
126 | * Internal forward references. | ||
127 | */ | ||
128 | static void save_prefs(midend *me); | ||
129 | |||
130 | /* | ||
131 | * Call JS to get the date, and use that to initialise our random | ||
132 | * number generator to invent the first game seed. | ||
133 | */ | ||
134 | void get_random_seed(void **randseed, int *randseedsize) | ||
135 | { | ||
136 | unsigned *ret = snewn(2, unsigned); | ||
137 | js_get_date_64(ret); | ||
138 | *randseed = ret; | ||
139 | *randseedsize = 2*sizeof(unsigned); | ||
140 | } | ||
141 | |||
142 | /* | ||
143 | * Fatal error, called in cases of complete despair such as when | ||
144 | * malloc() has returned NULL. | ||
145 | */ | ||
146 | void fatal(const char *fmt, ...) | ||
147 | { | ||
148 | char buf[512]; | ||
149 | va_list ap; | ||
150 | |||
151 | strcpy(buf, "puzzle fatal error: "); | ||
152 | |||
153 | va_start(ap, fmt); | ||
154 | vsnprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), fmt, ap); | ||
155 | va_end(ap); | ||
156 | |||
157 | js_error_box(buf); | ||
158 | } | ||
159 | |||
160 | #ifdef DEBUGGING | ||
161 | void debug_printf(const char *fmt, ...) | ||
162 | { | ||
163 | char buf[512]; | ||
164 | va_list ap; | ||
165 | va_start(ap, fmt); | ||
166 | vsnprintf(buf, sizeof(buf), fmt, ap); | ||
167 | va_end(ap); | ||
168 | js_debug(buf); | ||
169 | } | ||
170 | #endif | ||
171 | |||
172 | /* | ||
173 | * Helper function that makes it easy to test strings that might be | ||
174 | * NULL. | ||
175 | */ | ||
176 | static int strnullcmp(const char *a, const char *b) | ||
177 | { | ||
178 | if (a == NULL || b == NULL) | ||
179 | return a != NULL ? +1 : b != NULL ? -1 : 0; | ||
180 | return strcmp(a, b); | ||
181 | } | ||
182 | |||
183 | /* | ||
184 | * The global midend object. | ||
185 | */ | ||
186 | static midend *me; | ||
187 | |||
188 | /* ---------------------------------------------------------------------- | ||
189 | * Timing functions. | ||
190 | */ | ||
191 | static bool timer_active = false; | ||
192 | void deactivate_timer(frontend *fe) | ||
193 | { | ||
194 | js_deactivate_timer(); | ||
195 | timer_active = false; | ||
196 | } | ||
197 | void activate_timer(frontend *fe) | ||
198 | { | ||
199 | if (!timer_active) { | ||
200 | js_activate_timer(); | ||
201 | timer_active = true; | ||
202 | } | ||
203 | } | ||
204 | void timer_callback(double tplus) | ||
205 | { | ||
206 | if (timer_active) | ||
207 | midend_timer(me, tplus); | ||
208 | } | ||
209 | |||
210 | /* ---------------------------------------------------------------------- | ||
211 | * Helper functions to resize the canvas, and variables to remember | ||
212 | * its size for other functions (e.g. trimming blitter rectangles). | ||
213 | */ | ||
214 | static int canvas_w, canvas_h; | ||
215 | |||
216 | /* | ||
217 | * Called when we resize as a result of changing puzzle settings | ||
218 | * or device pixel ratio. | ||
219 | */ | ||
220 | static void resize(void) | ||
221 | { | ||
222 | int w, h; | ||
223 | bool user; | ||
224 | w = h = INT_MAX; | ||
225 | user = js_canvas_get_preferred_size(&w, &h); | ||
226 | midend_size(me, &w, &h, user, js_get_device_pixel_ratio()); | ||
227 | js_canvas_set_size(w, h); | ||
228 | canvas_w = w; | ||
229 | canvas_h = h; | ||
230 | } | ||
231 | |||
232 | /* Called from JS when the device pixel ratio changes */ | ||
233 | void rescale_puzzle(void) | ||
234 | { | ||
235 | resize(); | ||
236 | midend_force_redraw(me); | ||
237 | } | ||
238 | |||
239 | /* Called from JS when the user uses the resize handle */ | ||
240 | void resize_puzzle(int w, int h) | ||
241 | { | ||
242 | midend_size(me, &w, &h, true, js_get_device_pixel_ratio()); | ||
243 | if (canvas_w != w || canvas_h != h) { | ||
244 | js_canvas_set_size(w, h); | ||
245 | canvas_w = w; | ||
246 | canvas_h = h; | ||
247 | midend_force_redraw(me); | ||
248 | } | ||
249 | } | ||
250 | |||
251 | /* Called from JS when the user uses the restore button */ | ||
252 | void restore_puzzle_size(int w, int h) | ||
253 | { | ||
254 | midend_reset_tilesize(me); | ||
255 | resize(); | ||
256 | midend_force_redraw(me); | ||
257 | } | ||
258 | |||
259 | /* | ||
260 | * Try to extract a background colour from the canvas's CSS. In case | ||
261 | * it doesn't have a usable one, make up a lightish grey ourselves. | ||
262 | */ | ||
263 | void frontend_default_colour(frontend *fe, float *output) | ||
264 | { | ||
265 | output[0] = output[1] = output[2] = 0.9F; | ||
266 | js_default_colour(output); | ||
267 | } | ||
268 | |||
269 | /* | ||
270 | * Helper function called from all over the place to ensure the undo | ||
271 | * and redo buttons get properly enabled and disabled after every move | ||
272 | * or undo or new-game event. | ||
273 | */ | ||
274 | static void post_move(void) | ||
275 | { | ||
276 | js_enable_undo_redo(midend_can_undo(me), midend_can_redo(me)); | ||
277 | js_update_key_labels(midend_current_key_label(me, CURSOR_SELECT2), | ||
278 | midend_current_key_label(me, CURSOR_SELECT)); | ||
279 | } | ||
280 | |||
281 | /* | ||
282 | * Mouse event handlers called from JS. | ||
283 | */ | ||
284 | bool mousedown(int x, int y, int button) | ||
285 | { | ||
286 | bool handled; | ||
287 | |||
288 | button = (button == 0 ? LEFT_BUTTON : | ||
289 | button == 1 ? MIDDLE_BUTTON : RIGHT_BUTTON); | ||
290 | handled = midend_process_key(me, x, y, button) != PKR_UNUSED; | ||
291 | post_move(); | ||
292 | return handled; | ||
293 | } | ||
294 | |||
295 | bool mouseup(int x, int y, int button) | ||
296 | { | ||
297 | bool handled; | ||
298 | |||
299 | button = (button == 0 ? LEFT_RELEASE : | ||
300 | button == 1 ? MIDDLE_RELEASE : RIGHT_RELEASE); | ||
301 | handled = midend_process_key(me, x, y, button) != PKR_UNUSED; | ||
302 | post_move(); | ||
303 | return handled; | ||
304 | } | ||
305 | |||
306 | bool mousemove(int x, int y, int buttons) | ||
307 | { | ||
308 | int button = (buttons & 2 ? MIDDLE_DRAG : | ||
309 | buttons & 4 ? RIGHT_DRAG : LEFT_DRAG); | ||
310 | bool handled; | ||
311 | |||
312 | handled = midend_process_key(me, x, y, button) != PKR_UNUSED; | ||
313 | post_move(); | ||
314 | return handled; | ||
315 | } | ||
316 | |||
317 | /* | ||
318 | * Keyboard handler called from JS. Returns true if the key was | ||
319 | * handled and hence the keydown event should be cancelled. | ||
320 | */ | ||
321 | bool key(int keycode, const char *key, const char *chr, int location, | ||
322 | bool shift, bool ctrl) | ||
323 | { | ||
324 | /* Key location constants from JavaScript. */ | ||
325 | #define DOM_KEY_LOCATION_STANDARD 0 | ||
326 | #define DOM_KEY_LOCATION_LEFT 1 | ||
327 | #define DOM_KEY_LOCATION_RIGHT 2 | ||
328 | #define DOM_KEY_LOCATION_NUMPAD 3 | ||
329 | int keyevent = -1; | ||
330 | int process_key_result; | ||
331 | |||
332 | if (!strnullcmp(key, "Backspace") || !strnullcmp(key, "Delete") || | ||
333 | !strnullcmp(key, "Del")) | ||
334 | keyevent = 127; /* Backspace / Delete */ | ||
335 | else if (!strnullcmp(key, "Enter")) | ||
336 | keyevent = 13; /* return */ | ||
337 | else if (!strnullcmp(key, "Spacebar")) | ||
338 | keyevent = ' '; | ||
339 | else if (!strnullcmp(key, "Escape")) | ||
340 | keyevent = 27; | ||
341 | else if (!strnullcmp(key, "ArrowLeft") || !strnullcmp(key, "Left")) | ||
342 | keyevent = CURSOR_LEFT; | ||
343 | else if (!strnullcmp(key, "ArrowUp") || !strnullcmp(key, "Up")) | ||
344 | keyevent = CURSOR_UP; | ||
345 | else if (!strnullcmp(key, "ArrowRight") || !strnullcmp(key, "Right")) | ||
346 | keyevent = CURSOR_RIGHT; | ||
347 | else if (!strnullcmp(key, "ArrowDown") || !strnullcmp(key, "Down")) | ||
348 | keyevent = CURSOR_DOWN; | ||
349 | else if (!strnullcmp(key, "SoftLeft")) | ||
350 | /* Left soft key on KaiOS. */ | ||
351 | keyevent = CURSOR_SELECT2; | ||
352 | else if (!strnullcmp(key, "End")) | ||
353 | /* | ||
354 | * We interpret Home, End, PgUp and PgDn as numeric keypad | ||
355 | * controls regardless of whether they're the ones on the | ||
356 | * numeric keypad (since we can't tell). The effect of | ||
357 | * this should only be that the non-numeric-pad versions | ||
358 | * of those keys generate directions in 8-way movement | ||
359 | * puzzles like Cube and Inertia. | ||
360 | */ | ||
361 | keyevent = MOD_NUM_KEYPAD | '1'; | ||
362 | else if (!strnullcmp(key, "PageDown")) | ||
363 | keyevent = MOD_NUM_KEYPAD | '3'; | ||
364 | else if (!strnullcmp(key, "Home")) | ||
365 | keyevent = MOD_NUM_KEYPAD | '7'; | ||
366 | else if (!strnullcmp(key, "PageUp")) | ||
367 | keyevent = MOD_NUM_KEYPAD | '9'; | ||
368 | else if (shift && ctrl && (!strnullcmp(key, "Z") || !strnullcmp(key, "z"))) | ||
369 | keyevent = UI_REDO; | ||
370 | else if (key && (unsigned char)key[0] < 0x80 && key[1] == '\0') | ||
371 | /* Key generating a single ASCII character. */ | ||
372 | keyevent = key[0]; | ||
373 | /* | ||
374 | * In modern browsers (since about 2017), all keys that Puzzles | ||
375 | * cares about should be matched by one of the clauses above. The | ||
376 | * code below that checks keycode and chr should be relavent only | ||
377 | * in older browsers. | ||
378 | */ | ||
379 | else if (keycode == 8 || keycode == 46) | ||
380 | keyevent = 127; /* Backspace / Delete */ | ||
381 | else if (keycode == 13) | ||
382 | keyevent = 13; /* return */ | ||
383 | else if (keycode == 37) | ||
384 | keyevent = CURSOR_LEFT; | ||
385 | else if (keycode == 38) | ||
386 | keyevent = CURSOR_UP; | ||
387 | else if (keycode == 39) | ||
388 | keyevent = CURSOR_RIGHT; | ||
389 | else if (keycode == 40) | ||
390 | keyevent = CURSOR_DOWN; | ||
391 | else if (keycode == 35) | ||
392 | keyevent = MOD_NUM_KEYPAD | '1'; | ||
393 | else if (keycode == 34) | ||
394 | keyevent = MOD_NUM_KEYPAD | '3'; | ||
395 | else if (keycode == 36) | ||
396 | keyevent = MOD_NUM_KEYPAD | '7'; | ||
397 | else if (keycode == 33) | ||
398 | keyevent = MOD_NUM_KEYPAD | '9'; | ||
399 | else if (shift && ctrl && (keycode & 0x1F) == 26) | ||
400 | keyevent = UI_REDO; | ||
401 | else if (chr && chr[0] && !chr[1]) | ||
402 | keyevent = chr[0] & 0xFF; | ||
403 | else if (keycode >= 96 && keycode < 106) | ||
404 | keyevent = MOD_NUM_KEYPAD | ('0' + keycode - 96); | ||
405 | else if (keycode >= 65 && keycode <= 90) | ||
406 | keyevent = keycode + (shift ? 0 : 32); | ||
407 | else if (keycode >= 48 && keycode <= 57) | ||
408 | keyevent = keycode; | ||
409 | else if (keycode == 32) /* space / CURSOR_SELECT2 */ | ||
410 | keyevent = keycode; | ||
411 | |||
412 | if (keyevent >= 0) { | ||
413 | if (shift) keyevent |= MOD_SHFT; | ||
414 | if (ctrl) keyevent |= MOD_CTRL; | ||
415 | if (location == DOM_KEY_LOCATION_NUMPAD) keyevent |= MOD_NUM_KEYPAD; | ||
416 | |||
417 | process_key_result = midend_process_key(me, 0, 0, keyevent); | ||
418 | post_move(); | ||
419 | /* | ||
420 | * Treat Backspace specially because that's expected on KaiOS. | ||
421 | * https://developer.kaiostech.com/docs/design-guide/key | ||
422 | */ | ||
423 | if (process_key_result == PKR_NO_EFFECT && | ||
424 | !strnullcmp(key, "Backspace")) | ||
425 | return false; | ||
426 | return process_key_result != PKR_UNUSED; | ||
427 | } | ||
428 | return false; /* Event not handled, because we don't even recognise it. */ | ||
429 | } | ||
430 | |||
431 | /* | ||
432 | * Helper function called from several places to update the permalinks | ||
433 | * whenever a new game is created. | ||
434 | */ | ||
435 | static void update_permalinks(void) | ||
436 | { | ||
437 | char *desc, *seed; | ||
438 | desc = midend_get_game_id(me); | ||
439 | seed = midend_get_random_seed(me); | ||
440 | js_update_permalinks(desc, seed); | ||
441 | sfree(desc); | ||
442 | sfree(seed); | ||
443 | } | ||
444 | |||
445 | /* | ||
446 | * Callback from the midend when the game ids change, so we can update | ||
447 | * the permalinks. | ||
448 | */ | ||
449 | static void ids_changed(void *ignored) | ||
450 | { | ||
451 | update_permalinks(); | ||
452 | } | ||
453 | |||
454 | /* ---------------------------------------------------------------------- | ||
455 | * Implementation of the drawing API by calling Javascript canvas | ||
456 | * drawing functions. (Well, half of it; the other half is on the JS | ||
457 | * side.) | ||
458 | */ | ||
459 | static void js_start_draw(void *handle) | ||
460 | { | ||
461 | js_canvas_start_draw(); | ||
462 | } | ||
463 | |||
464 | static void js_clip(void *handle, int x, int y, int w, int h) | ||
465 | { | ||
466 | js_canvas_clip_rect(x, y, w, h); | ||
467 | } | ||
468 | |||
469 | static void js_unclip(void *handle) | ||
470 | { | ||
471 | js_canvas_unclip(); | ||
472 | } | ||
473 | |||
474 | static void js_draw_text(void *handle, int x, int y, int fonttype, | ||
475 | int fontsize, int align, int colour, | ||
476 | const char *text) | ||
477 | { | ||
478 | int halign; | ||
479 | |||
480 | if (align & ALIGN_VCENTRE) | ||
481 | y += js_canvas_find_font_midpoint(fontsize, fonttype == FONT_FIXED); | ||
482 | |||
483 | if (align & ALIGN_HCENTRE) | ||
484 | halign = 1; | ||
485 | else if (align & ALIGN_HRIGHT) | ||
486 | halign = 2; | ||
487 | else | ||
488 | halign = 0; | ||
489 | |||
490 | js_canvas_draw_text(x, y, halign, colour, | ||
491 | fontsize, fonttype == FONT_FIXED, text); | ||
492 | } | ||
493 | |||
494 | static void js_draw_rect(void *handle, int x, int y, int w, int h, int colour) | ||
495 | { | ||
496 | js_canvas_draw_rect(x, y, w, h, colour); | ||
497 | } | ||
498 | |||
499 | static void js_draw_line(void *handle, int x1, int y1, int x2, int y2, | ||
500 | int colour) | ||
501 | { | ||
502 | js_canvas_draw_line(x1, y1, x2, y2, 1, colour); | ||
503 | } | ||
504 | |||
505 | static void js_draw_thick_line(void *handle, float thickness, | ||
506 | float x1, float y1, float x2, float y2, | ||
507 | int colour) | ||
508 | { | ||
509 | js_canvas_draw_line(x1, y1, x2, y2, thickness, colour); | ||
510 | } | ||
511 | |||
512 | static void js_draw_poly(void *handle, const int *coords, int npoints, | ||
513 | int fillcolour, int outlinecolour) | ||
514 | { | ||
515 | js_canvas_draw_poly(coords, npoints, fillcolour, outlinecolour); | ||
516 | } | ||
517 | |||
518 | static void js_draw_circle(void *handle, int cx, int cy, int radius, | ||
519 | int fillcolour, int outlinecolour) | ||
520 | { | ||
521 | js_canvas_draw_circle(cx, cy, radius, fillcolour, outlinecolour); | ||
522 | } | ||
523 | |||
524 | struct blitter { | ||
525 | int id; /* allocated on the js side */ | ||
526 | int w, h; /* easier to retain here */ | ||
527 | }; | ||
528 | |||
529 | static blitter *js_blitter_new(void *handle, int w, int h) | ||
530 | { | ||
531 | blitter *bl = snew(blitter); | ||
532 | bl->w = w; | ||
533 | bl->h = h; | ||
534 | bl->id = js_canvas_new_blitter(w, h); | ||
535 | return bl; | ||
536 | } | ||
537 | |||
538 | static void js_blitter_free(void *handle, blitter *bl) | ||
539 | { | ||
540 | js_canvas_free_blitter(bl->id); | ||
541 | sfree(bl); | ||
542 | } | ||
543 | |||
544 | static void trim_rect(int *x, int *y, int *w, int *h) | ||
545 | { | ||
546 | int x0, x1, y0, y1; | ||
547 | |||
548 | /* | ||
549 | * Reduce the size of the copied rectangle to stop it going | ||
550 | * outside the bounds of the canvas. | ||
551 | */ | ||
552 | |||
553 | /* Transform from x,y,w,h form into coordinates of all edges */ | ||
554 | x0 = *x; | ||
555 | y0 = *y; | ||
556 | x1 = *x + *w; | ||
557 | y1 = *y + *h; | ||
558 | |||
559 | /* Clip each coordinate at both extremes of the canvas */ | ||
560 | x0 = (x0 < 0 ? 0 : x0 > canvas_w ? canvas_w : x0); | ||
561 | x1 = (x1 < 0 ? 0 : x1 > canvas_w ? canvas_w : x1); | ||
562 | y0 = (y0 < 0 ? 0 : y0 > canvas_h ? canvas_h : y0); | ||
563 | y1 = (y1 < 0 ? 0 : y1 > canvas_h ? canvas_h : y1); | ||
564 | |||
565 | /* Transform back into x,y,w,h to return */ | ||
566 | *x = x0; | ||
567 | *y = y0; | ||
568 | *w = x1 - x0; | ||
569 | *h = y1 - y0; | ||
570 | } | ||
571 | |||
572 | static void js_blitter_save(void *handle, blitter *bl, int x, int y) | ||
573 | { | ||
574 | int w = bl->w, h = bl->h; | ||
575 | trim_rect(&x, &y, &w, &h); | ||
576 | if (w > 0 && h > 0) | ||
577 | js_canvas_copy_to_blitter(bl->id, x, y, w, h); | ||
578 | } | ||
579 | |||
580 | static void js_blitter_load(void *handle, blitter *bl, int x, int y) | ||
581 | { | ||
582 | int w = bl->w, h = bl->h; | ||
583 | trim_rect(&x, &y, &w, &h); | ||
584 | if (w > 0 && h > 0) | ||
585 | js_canvas_copy_from_blitter(bl->id, x, y, w, h); | ||
586 | } | ||
587 | |||
588 | static void js_draw_update(void *handle, int x, int y, int w, int h) | ||
589 | { | ||
590 | trim_rect(&x, &y, &w, &h); | ||
591 | if (w > 0 && h > 0) | ||
592 | js_canvas_draw_update(x, y, w, h); | ||
593 | } | ||
594 | |||
595 | static void js_end_draw(void *handle) | ||
596 | { | ||
597 | js_canvas_end_draw(); | ||
598 | } | ||
599 | |||
600 | static void js_status_bar(void *handle, const char *text) | ||
601 | { | ||
602 | js_canvas_set_statusbar(text); | ||
603 | } | ||
604 | |||
605 | static char *js_text_fallback(void *handle, const char *const *strings, | ||
606 | int nstrings) | ||
607 | { | ||
608 | return dupstr(strings[0]); /* Emscripten has no trouble with UTF-8 */ | ||
609 | } | ||
610 | |||
611 | static const struct drawing_api js_drawing = { | ||
612 | js_draw_text, | ||
613 | js_draw_rect, | ||
614 | js_draw_line, | ||
615 | js_draw_poly, | ||
616 | js_draw_circle, | ||
617 | js_draw_update, | ||
618 | js_clip, | ||
619 | js_unclip, | ||
620 | js_start_draw, | ||
621 | js_end_draw, | ||
622 | js_status_bar, | ||
623 | js_blitter_new, | ||
624 | js_blitter_free, | ||
625 | js_blitter_save, | ||
626 | js_blitter_load, | ||
627 | NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */ | ||
628 | NULL, NULL, /* line_width, line_dotted */ | ||
629 | js_text_fallback, | ||
630 | js_draw_thick_line, | ||
631 | }; | ||
632 | |||
633 | /* ---------------------------------------------------------------------- | ||
634 | * Presets and game-configuration dialog support. | ||
635 | */ | ||
636 | static game_params **presets; | ||
637 | static int npresets; | ||
638 | static bool have_presets_dropdown; | ||
639 | |||
640 | static void populate_js_preset_menu(int menuid, struct preset_menu *menu) | ||
641 | { | ||
642 | int i; | ||
643 | for (i = 0; i < menu->n_entries; i++) { | ||
644 | struct preset_menu_entry *entry = &menu->entries[i]; | ||
645 | if (entry->params) { | ||
646 | presets[entry->id] = entry->params; | ||
647 | js_add_preset(menuid, entry->title, entry->id); | ||
648 | } else { | ||
649 | int js_submenu = js_add_preset_submenu(menuid, entry->title); | ||
650 | populate_js_preset_menu(js_submenu, entry->submenu); | ||
651 | } | ||
652 | } | ||
653 | } | ||
654 | |||
655 | static void select_appropriate_preset(void) | ||
656 | { | ||
657 | if (have_presets_dropdown) { | ||
658 | int preset = midend_which_preset(me); | ||
659 | js_select_preset(preset < 0 ? -1 : preset); | ||
660 | } | ||
661 | } | ||
662 | |||
663 | static config_item *cfg = NULL; | ||
664 | static int cfg_which; | ||
665 | |||
666 | /* | ||
667 | * Set up a dialog box. This is pretty easy on the C side; most of the | ||
668 | * work is done in JS. | ||
669 | */ | ||
670 | static void cfg_start(int which) | ||
671 | { | ||
672 | char *title; | ||
673 | int i; | ||
674 | |||
675 | cfg = midend_get_config(me, which, &title); | ||
676 | cfg_which = which; | ||
677 | |||
678 | js_dialog_init(title); | ||
679 | sfree(title); | ||
680 | |||
681 | for (i = 0; cfg[i].type != C_END; i++) { | ||
682 | switch (cfg[i].type) { | ||
683 | case C_STRING: | ||
684 | js_dialog_string(i, cfg[i].name, cfg[i].u.string.sval); | ||
685 | break; | ||
686 | case C_BOOLEAN: | ||
687 | js_dialog_boolean(i, cfg[i].name, cfg[i].u.boolean.bval); | ||
688 | break; | ||
689 | case C_CHOICES: | ||
690 | js_dialog_choices(i, cfg[i].name, cfg[i].u.choices.choicenames, | ||
691 | cfg[i].u.choices.selected); | ||
692 | break; | ||
693 | } | ||
694 | } | ||
695 | |||
696 | js_dialog_launch(); | ||
697 | } | ||
698 | |||
699 | /* | ||
700 | * Callbacks from JS when the OK button is clicked, to return the | ||
701 | * final state of each control. | ||
702 | */ | ||
703 | void dlg_return_sval(int index, const char *val) | ||
704 | { | ||
705 | config_item *i = cfg + index; | ||
706 | switch (i->type) { | ||
707 | case C_STRING: | ||
708 | sfree(i->u.string.sval); | ||
709 | i->u.string.sval = dupstr(val); | ||
710 | break; | ||
711 | default: | ||
712 | assert(0 && "Bad type for return_sval"); | ||
713 | } | ||
714 | } | ||
715 | void dlg_return_ival(int index, int val) | ||
716 | { | ||
717 | config_item *i = cfg + index; | ||
718 | switch (i->type) { | ||
719 | case C_BOOLEAN: | ||
720 | i->u.boolean.bval = val; | ||
721 | break; | ||
722 | case C_CHOICES: | ||
723 | i->u.choices.selected = val; | ||
724 | break; | ||
725 | default: | ||
726 | assert(0 && "Bad type for return_ival"); | ||
727 | } | ||
728 | } | ||
729 | |||
730 | /* | ||
731 | * Called when the user clicks OK or Cancel. use_results will be true | ||
732 | * or false respectively, in those cases. We terminate the dialog box, | ||
733 | * unless the user selected an invalid combination of parameters. | ||
734 | */ | ||
735 | static void cfg_end(bool use_results) | ||
736 | { | ||
737 | if (use_results) { | ||
738 | /* | ||
739 | * User hit OK. | ||
740 | */ | ||
741 | const char *err = midend_set_config(me, cfg_which, cfg); | ||
742 | |||
743 | if (err) { | ||
744 | /* | ||
745 | * The settings were unacceptable, so leave the config box | ||
746 | * open for the user to adjust them and try again. | ||
747 | */ | ||
748 | js_error_box(err); | ||
749 | } else if (cfg_which == CFG_PREFS) { | ||
750 | /* | ||
751 | * Acceptable settings for user preferences: enact them | ||
752 | * without blowing away the current game. | ||
753 | */ | ||
754 | resize(); | ||
755 | midend_redraw(me); | ||
756 | free_cfg(cfg); | ||
757 | js_dialog_cleanup(); | ||
758 | save_prefs(me); | ||
759 | } else { | ||
760 | /* | ||
761 | * Acceptable settings for the remaining configuration | ||
762 | * types: start a new game and close the dialog. | ||
763 | */ | ||
764 | select_appropriate_preset(); | ||
765 | midend_new_game(me); | ||
766 | resize(); | ||
767 | midend_redraw(me); | ||
768 | free_cfg(cfg); | ||
769 | js_dialog_cleanup(); | ||
770 | } | ||
771 | } else { | ||
772 | /* | ||
773 | * User hit Cancel. Close the dialog, but also we must still | ||
774 | * reselect the right element of the dropdown list. | ||
775 | * | ||
776 | * (Because: imagine you have a preset selected, and then you | ||
777 | * select Custom from the list, but change your mind and hit | ||
778 | * Esc. The Custom option will now still be selected in the | ||
779 | * list, whereas obviously it should show the preset you still | ||
780 | * _actually_ have selected.) | ||
781 | */ | ||
782 | select_appropriate_preset(); | ||
783 | |||
784 | free_cfg(cfg); | ||
785 | js_dialog_cleanup(); | ||
786 | } | ||
787 | } | ||
788 | |||
789 | /* ---------------------------------------------------------------------- | ||
790 | * Called from JS when a command is given to the puzzle by clicking a | ||
791 | * button or control of some sort. | ||
792 | */ | ||
793 | void command(int n) | ||
794 | { | ||
795 | switch (n) { | ||
796 | case 0: /* specific game ID */ | ||
797 | cfg_start(CFG_DESC); | ||
798 | break; | ||
799 | case 1: /* random game seed */ | ||
800 | cfg_start(CFG_SEED); | ||
801 | break; | ||
802 | case 2: /* game parameter dropdown changed */ | ||
803 | { | ||
804 | int i = js_get_selected_preset(); | ||
805 | if (i < 0) { | ||
806 | /* | ||
807 | * The user selected 'Custom', so launch the config | ||
808 | * box. | ||
809 | */ | ||
810 | if (thegame.can_configure) /* (double-check just in case) */ | ||
811 | cfg_start(CFG_SETTINGS); | ||
812 | } else { | ||
813 | /* | ||
814 | * The user selected a preset, so just switch straight | ||
815 | * to that. | ||
816 | */ | ||
817 | assert(i < npresets); | ||
818 | midend_set_params(me, presets[i]); | ||
819 | midend_new_game(me); | ||
820 | resize(); | ||
821 | midend_redraw(me); | ||
822 | post_move(); | ||
823 | js_focus_canvas(); | ||
824 | select_appropriate_preset(); | ||
825 | } | ||
826 | } | ||
827 | break; | ||
828 | case 3: /* OK clicked in a config box */ | ||
829 | cfg_end(true); | ||
830 | post_move(); | ||
831 | break; | ||
832 | case 4: /* Cancel clicked in a config box */ | ||
833 | cfg_end(false); | ||
834 | post_move(); | ||
835 | break; | ||
836 | case 5: /* New Game */ | ||
837 | midend_process_key(me, 0, 0, UI_NEWGAME); | ||
838 | post_move(); | ||
839 | js_focus_canvas(); | ||
840 | break; | ||
841 | case 6: /* Restart */ | ||
842 | midend_restart_game(me); | ||
843 | post_move(); | ||
844 | js_focus_canvas(); | ||
845 | break; | ||
846 | case 7: /* Undo */ | ||
847 | midend_process_key(me, 0, 0, UI_UNDO); | ||
848 | post_move(); | ||
849 | js_focus_canvas(); | ||
850 | break; | ||
851 | case 8: /* Redo */ | ||
852 | midend_process_key(me, 0, 0, UI_REDO); | ||
853 | post_move(); | ||
854 | js_focus_canvas(); | ||
855 | break; | ||
856 | case 9: /* Solve */ | ||
857 | if (thegame.can_solve) { | ||
858 | const char *msg = midend_solve(me); | ||
859 | if (msg) | ||
860 | js_error_box(msg); | ||
861 | } | ||
862 | post_move(); | ||
863 | js_focus_canvas(); | ||
864 | break; | ||
865 | case 10: /* user preferences */ | ||
866 | cfg_start(CFG_PREFS); | ||
867 | break; | ||
868 | } | ||
869 | } | ||
870 | |||
871 | char *get_text_format(void) | ||
872 | { | ||
873 | return midend_text_format(me); | ||
874 | } | ||
875 | |||
876 | void free_text_format(char *buffer) | ||
877 | { | ||
878 | sfree(buffer); | ||
879 | } | ||
880 | |||
881 | /* ---------------------------------------------------------------------- | ||
882 | * Called from JS to prepare a save-game file, and free one after it's | ||
883 | * been used. | ||
884 | */ | ||
885 | |||
886 | struct savefile_write_ctx { | ||
887 | char *buffer; | ||
888 | size_t pos; | ||
889 | }; | ||
890 | |||
891 | static void savefile_write(void *vctx, const void *buf, int len) | ||
892 | { | ||
893 | struct savefile_write_ctx *ctx = (struct savefile_write_ctx *)vctx; | ||
894 | if (ctx->buffer) | ||
895 | memcpy(ctx->buffer + ctx->pos, buf, len); | ||
896 | ctx->pos += len; | ||
897 | } | ||
898 | |||
899 | char *get_save_file(void) | ||
900 | { | ||
901 | struct savefile_write_ctx ctx; | ||
902 | size_t size; | ||
903 | |||
904 | /* First pass, to count up the size */ | ||
905 | ctx.buffer = NULL; | ||
906 | ctx.pos = 0; | ||
907 | midend_serialise(me, savefile_write, &ctx); | ||
908 | size = ctx.pos; | ||
909 | |||
910 | /* Second pass, to actually write out the data. We have to put a | ||
911 | * terminating \0 on the end (which we expect never to show up in | ||
912 | * the actual serialisation format - it's text, not binary) so | ||
913 | * that the Javascript side can easily find out the length. */ | ||
914 | ctx.buffer = snewn(size+1, char); | ||
915 | ctx.pos = 0; | ||
916 | midend_serialise(me, savefile_write, &ctx); | ||
917 | assert(ctx.pos == size); | ||
918 | ctx.buffer[ctx.pos] = '\0'; | ||
919 | |||
920 | return ctx.buffer; | ||
921 | } | ||
922 | |||
923 | void free_save_file(char *buffer) | ||
924 | { | ||
925 | sfree(buffer); | ||
926 | } | ||
927 | |||
928 | static bool savefile_read(void *vctx, void *buf, int len) | ||
929 | { | ||
930 | return js_savefile_read(buf, len); | ||
931 | } | ||
932 | |||
933 | void load_game(void) | ||
934 | { | ||
935 | const char *err; | ||
936 | |||
937 | /* | ||
938 | * savefile_read_callback in JavaScript was set up by our caller | ||
939 | * as a closure that knows what file we're loading. | ||
940 | */ | ||
941 | err = midend_deserialise(me, savefile_read, NULL); | ||
942 | |||
943 | if (err) { | ||
944 | js_error_box(err); | ||
945 | } else { | ||
946 | select_appropriate_preset(); | ||
947 | resize(); | ||
948 | midend_redraw(me); | ||
949 | update_permalinks(); | ||
950 | post_move(); | ||
951 | } | ||
952 | } | ||
953 | |||
954 | /* ---------------------------------------------------------------------- | ||
955 | * Functions to load and save preferences, calling out to JS to access | ||
956 | * the appropriate localStorage slot. | ||
957 | */ | ||
958 | |||
959 | static void save_prefs(midend *me) | ||
960 | { | ||
961 | struct savefile_write_ctx ctx; | ||
962 | size_t size; | ||
963 | |||
964 | /* First pass, to count up the size */ | ||
965 | ctx.buffer = NULL; | ||
966 | ctx.pos = 0; | ||
967 | midend_save_prefs(me, savefile_write, &ctx); | ||
968 | size = ctx.pos; | ||
969 | |||
970 | /* Second pass, to actually write out the data. As with | ||
971 | * get_save_file, we append a terminating \0. */ | ||
972 | ctx.buffer = snewn(size+1, char); | ||
973 | ctx.pos = 0; | ||
974 | midend_save_prefs(me, savefile_write, &ctx); | ||
975 | assert(ctx.pos == size); | ||
976 | ctx.buffer[ctx.pos] = '\0'; | ||
977 | |||
978 | js_save_prefs(ctx.buffer); | ||
979 | |||
980 | sfree(ctx.buffer); | ||
981 | } | ||
982 | |||
983 | struct prefs_read_ctx { | ||
984 | const char *buffer; | ||
985 | size_t pos, len; | ||
986 | }; | ||
987 | |||
988 | static bool prefs_read(void *vctx, void *buf, int len) | ||
989 | { | ||
990 | struct prefs_read_ctx *ctx = (struct prefs_read_ctx *)vctx; | ||
991 | |||
992 | if (len < 0) | ||
993 | return false; | ||
994 | if (ctx->len - ctx->pos < len) | ||
995 | return false; | ||
996 | memcpy(buf, ctx->buffer + ctx->pos, len); | ||
997 | ctx->pos += len; | ||
998 | return true; | ||
999 | } | ||
1000 | |||
1001 | void prefs_load_callback(midend *me, const char *prefs) | ||
1002 | { | ||
1003 | struct prefs_read_ctx ctx; | ||
1004 | |||
1005 | ctx.buffer = prefs; | ||
1006 | ctx.len = strlen(prefs); | ||
1007 | ctx.pos = 0; | ||
1008 | |||
1009 | midend_load_prefs(me, prefs_read, &ctx); | ||
1010 | } | ||
1011 | |||
1012 | /* ---------------------------------------------------------------------- | ||
1013 | * Setup function called at page load time. It's called main() because | ||
1014 | * that's the most convenient thing in Emscripten, but it's not main() | ||
1015 | * in the usual sense of bounding the program's entire execution. | ||
1016 | * Instead, this function returns once the initial puzzle is set up | ||
1017 | * and working, and everything thereafter happens by means of JS event | ||
1018 | * handlers sending us callbacks. | ||
1019 | */ | ||
1020 | int main(int argc, char **argv) | ||
1021 | { | ||
1022 | const char *param_err; | ||
1023 | float *colours; | ||
1024 | int i, ncolours; | ||
1025 | |||
1026 | /* | ||
1027 | * Initialise JavaScript event handlers. | ||
1028 | */ | ||
1029 | js_init_puzzle(); | ||
1030 | |||
1031 | /* | ||
1032 | * Instantiate a midend. | ||
1033 | */ | ||
1034 | me = midend_new(NULL, &thegame, &js_drawing, NULL); | ||
1035 | js_load_prefs(me); | ||
1036 | |||
1037 | /* | ||
1038 | * Chuck in the HTML fragment ID if we have one (trimming the | ||
1039 | * leading # off the front first). If that's invalid, we retain | ||
1040 | * the error message and will display it at the end, after setting | ||
1041 | * up a random puzzle as usual. | ||
1042 | */ | ||
1043 | if (argc > 1 && argv[1][0] == '#' && argv[1][1] != '\0') | ||
1044 | param_err = midend_game_id(me, argv[1] + 1); | ||
1045 | else | ||
1046 | param_err = NULL; | ||
1047 | |||
1048 | /* | ||
1049 | * Create either a random game or the specified one, and set the | ||
1050 | * canvas size appropriately. | ||
1051 | */ | ||
1052 | midend_new_game(me); | ||
1053 | resize(); | ||
1054 | |||
1055 | /* | ||
1056 | * Remove the status bar, if not needed. | ||
1057 | */ | ||
1058 | if (!midend_wants_statusbar(me)) | ||
1059 | js_canvas_remove_statusbar(); | ||
1060 | |||
1061 | /* | ||
1062 | * Set up the game-type dropdown with presets and/or the Custom | ||
1063 | * option. | ||
1064 | */ | ||
1065 | { | ||
1066 | struct preset_menu *menu = midend_get_presets(me, &npresets); | ||
1067 | bool may_configure = false; | ||
1068 | presets = snewn(npresets, game_params *); | ||
1069 | for (i = 0; i < npresets; i++) | ||
1070 | presets[i] = NULL; | ||
1071 | |||
1072 | populate_js_preset_menu(0, menu); | ||
1073 | |||
1074 | /* | ||
1075 | * Crude hack to allow the "Custom..." item to be hidden on | ||
1076 | * KaiOS, where dialogs don't yet work. | ||
1077 | */ | ||
1078 | if (thegame.can_configure && getenv_bool("PUZZLES_ALLOW_CUSTOM", true)) | ||
1079 | may_configure = true; | ||
1080 | if (may_configure) | ||
1081 | js_add_preset(0, "Custom...", -1); | ||
1082 | |||
1083 | have_presets_dropdown = npresets > 1 || may_configure; | ||
1084 | |||
1085 | if (have_presets_dropdown) | ||
1086 | /* | ||
1087 | * Now ensure the appropriate element of the presets menu | ||
1088 | * starts off selected, in case it isn't the first one in the | ||
1089 | * list (e.g. Slant). | ||
1090 | */ | ||
1091 | select_appropriate_preset(); | ||
1092 | else | ||
1093 | js_remove_type_dropdown(); | ||
1094 | } | ||
1095 | |||
1096 | /* | ||
1097 | * Remove the Solve button if the game doesn't support it. | ||
1098 | */ | ||
1099 | if (!thegame.can_solve) | ||
1100 | js_remove_solve_button(); | ||
1101 | |||
1102 | /* | ||
1103 | * Retrieve the game's colours, and convert them into #abcdef type | ||
1104 | * hex ID strings. | ||
1105 | */ | ||
1106 | colours = midend_colours(me, &ncolours); | ||
1107 | for (i = 0; i < ncolours; i++) { | ||
1108 | char col[40]; | ||
1109 | sprintf(col, "#%02x%02x%02x", | ||
1110 | (unsigned)(0.5F + 255 * colours[i*3+0]), | ||
1111 | (unsigned)(0.5F + 255 * colours[i*3+1]), | ||
1112 | (unsigned)(0.5F + 255 * colours[i*3+2])); | ||
1113 | js_set_colour(i, col); | ||
1114 | } | ||
1115 | |||
1116 | /* | ||
1117 | * Request notification when the game ids change (e.g. if the user | ||
1118 | * presses 'n', and also when Mines supersedes its game | ||
1119 | * description), so that we can proactively update the permalink. | ||
1120 | */ | ||
1121 | midend_request_id_changes(me, ids_changed, NULL); | ||
1122 | |||
1123 | /* | ||
1124 | * Draw the puzzle's initial state, and set up the permalinks and | ||
1125 | * undo/redo greying out. | ||
1126 | */ | ||
1127 | midend_redraw(me); | ||
1128 | update_permalinks(); | ||
1129 | post_move(); | ||
1130 | |||
1131 | /* | ||
1132 | * If we were given an erroneous game ID in argv[1], now's the | ||
1133 | * time to put up the error box about it, after we've fully set up | ||
1134 | * a random puzzle. Then when the user clicks 'ok', we have a | ||
1135 | * puzzle for them. | ||
1136 | */ | ||
1137 | if (param_err) | ||
1138 | js_error_box(param_err); | ||
1139 | |||
1140 | /* | ||
1141 | * Reveal the puzzle! | ||
1142 | */ | ||
1143 | js_post_init(); | ||
1144 | |||
1145 | /* | ||
1146 | * Done. Return to JS, and await callbacks! | ||
1147 | */ | ||
1148 | return 0; | ||
1149 | } | ||