diff options
author | Franklin Wei <frankhwei536@gmail.com> | 2016-11-20 15:16:41 -0500 |
---|---|---|
committer | Franklin Wei <me@fwei.tk> | 2016-12-18 18:13:22 +0100 |
commit | 1a6a8b52f7aa4e2da6f4c34a0c743c760b8cfd99 (patch) | |
tree | 8e7f2d6b0cbdb5d15c13457b2c3e1de69f598440 /apps/plugins/puzzles/emcc.c | |
parent | 3ee79724f6fb033d50e26ef37b33d3f8cedf0c5b (diff) | |
download | rockbox-1a6a8b52f7aa4e2da6f4c34a0c743c760b8cfd99.tar.gz rockbox-1a6a8b52f7aa4e2da6f4c34a0c743c760b8cfd99.zip |
Port of Simon Tatham's Puzzle Collection
Original revision: 5123b1bf68777ffa86e651f178046b26a87cf2d9
MIT Licensed. Some games still crash and others are unplayable due to
issues with controls. Still need a "real" polygon filling algorithm.
Currently builds one plugin per puzzle (about 40 in total, around 100K
each on ARM), but can easily be made to build a single monolithic
overlay (800K or so on ARM).
The following games are at least partially broken for various reasons,
and have been disabled on this commit:
Cube: failed assertion with "Icosahedron" setting
Keen: input issues
Mines: weird stuff happens on target
Palisade: input issues
Solo: input issues, occasional crash on target
Towers: input issues
Undead: input issues
Unequal: input and drawing issues (concave polys)
Untangle: input issues
Features left to do:
- In-game help system
- Figure out the weird bugs
Change-Id: I7c69b6860ab115f973c8d76799502e9bb3d52368
Diffstat (limited to 'apps/plugins/puzzles/emcc.c')
-rw-r--r-- | apps/plugins/puzzles/emcc.c | 867 |
1 files changed, 867 insertions, 0 deletions
diff --git a/apps/plugins/puzzles/emcc.c b/apps/plugins/puzzles/emcc.c new file mode 100644 index 0000000000..5ffcb0b793 --- /dev/null +++ b/apps/plugins/puzzles/emcc.c | |||
@@ -0,0 +1,867 @@ | |||
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 "rbassert.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(const char *name); | ||
65 | extern int js_get_selected_preset(void); | ||
66 | extern void js_select_preset(int n); | ||
67 | extern void js_get_date_64(unsigned *p); | ||
68 | extern void js_update_permalinks(const char *desc, const char *seed); | ||
69 | extern void js_enable_undo_redo(int undo, int redo); | ||
70 | extern void js_activate_timer(); | ||
71 | extern void js_deactivate_timer(); | ||
72 | extern void js_canvas_start_draw(void); | ||
73 | extern void js_canvas_draw_update(int x, int y, int w, int h); | ||
74 | extern void js_canvas_end_draw(void); | ||
75 | extern void js_canvas_draw_rect(int x, int y, int w, int h, | ||
76 | const char *colour); | ||
77 | extern void js_canvas_clip_rect(int x, int y, int w, int h); | ||
78 | extern void js_canvas_unclip(void); | ||
79 | extern void js_canvas_draw_line(float x1, float y1, float x2, float y2, | ||
80 | int width, const char *colour); | ||
81 | extern void js_canvas_draw_poly(int *points, int npoints, | ||
82 | const char *fillcolour, | ||
83 | const char *outlinecolour); | ||
84 | extern void js_canvas_draw_circle(int x, int y, int r, | ||
85 | const char *fillcolour, | ||
86 | const char *outlinecolour); | ||
87 | extern int js_canvas_find_font_midpoint(int height, const char *fontptr); | ||
88 | extern void js_canvas_draw_text(int x, int y, int halign, | ||
89 | const char *colptr, const char *fontptr, | ||
90 | const char *text); | ||
91 | extern int js_canvas_new_blitter(int w, int h); | ||
92 | extern void js_canvas_free_blitter(int id); | ||
93 | extern void js_canvas_copy_to_blitter(int id, int x, int y, int w, int h); | ||
94 | extern void js_canvas_copy_from_blitter(int id, int x, int y, int w, int h); | ||
95 | extern void js_canvas_make_statusbar(void); | ||
96 | extern void js_canvas_set_statusbar(const char *text); | ||
97 | extern void js_canvas_set_size(int w, int h); | ||
98 | |||
99 | extern void js_dialog_init(const char *title); | ||
100 | extern void js_dialog_string(int i, const char *title, const char *initvalue); | ||
101 | extern void js_dialog_choices(int i, const char *title, const char *choicelist, | ||
102 | int initvalue); | ||
103 | extern void js_dialog_boolean(int i, const char *title, int initvalue); | ||
104 | extern void js_dialog_launch(void); | ||
105 | extern void js_dialog_cleanup(void); | ||
106 | extern void js_focus_canvas(void); | ||
107 | |||
108 | /* | ||
109 | * Call JS to get the date, and use that to initialise our random | ||
110 | * number generator to invent the first game seed. | ||
111 | */ | ||
112 | void get_random_seed(void **randseed, int *randseedsize) | ||
113 | { | ||
114 | unsigned *ret = snewn(2, unsigned); | ||
115 | js_get_date_64(ret); | ||
116 | *randseed = ret; | ||
117 | *randseedsize = 2*sizeof(unsigned); | ||
118 | } | ||
119 | |||
120 | /* | ||
121 | * Fatal error, called in cases of complete despair such as when | ||
122 | * malloc() has returned NULL. | ||
123 | */ | ||
124 | void fatal(char *fmt, ...) | ||
125 | { | ||
126 | char buf[512]; | ||
127 | va_list ap; | ||
128 | |||
129 | strcpy(buf, "puzzle fatal error: "); | ||
130 | |||
131 | va_start(ap, fmt); | ||
132 | vsnprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), fmt, ap); | ||
133 | va_end(ap); | ||
134 | |||
135 | js_error_box(buf); | ||
136 | } | ||
137 | |||
138 | void debug_printf(char *fmt, ...) | ||
139 | { | ||
140 | char buf[512]; | ||
141 | va_list ap; | ||
142 | va_start(ap, fmt); | ||
143 | vsnprintf(buf, sizeof(buf), fmt, ap); | ||
144 | va_end(ap); | ||
145 | js_debug(buf); | ||
146 | } | ||
147 | |||
148 | /* | ||
149 | * Helper function that makes it easy to test strings that might be | ||
150 | * NULL. | ||
151 | */ | ||
152 | int strnullcmp(const char *a, const char *b) | ||
153 | { | ||
154 | if (a == NULL || b == NULL) | ||
155 | return a != NULL ? +1 : b != NULL ? -1 : 0; | ||
156 | return strcmp(a, b); | ||
157 | } | ||
158 | |||
159 | /* | ||
160 | * HTMLish names for the colours allocated by the puzzle. | ||
161 | */ | ||
162 | char **colour_strings; | ||
163 | int ncolours; | ||
164 | |||
165 | /* | ||
166 | * The global midend object. | ||
167 | */ | ||
168 | midend *me; | ||
169 | |||
170 | /* ---------------------------------------------------------------------- | ||
171 | * Timing functions. | ||
172 | */ | ||
173 | int timer_active = FALSE; | ||
174 | void deactivate_timer(frontend *fe) | ||
175 | { | ||
176 | js_deactivate_timer(); | ||
177 | timer_active = FALSE; | ||
178 | } | ||
179 | void activate_timer(frontend *fe) | ||
180 | { | ||
181 | if (!timer_active) { | ||
182 | js_activate_timer(); | ||
183 | timer_active = TRUE; | ||
184 | } | ||
185 | } | ||
186 | void timer_callback(double tplus) | ||
187 | { | ||
188 | if (timer_active) | ||
189 | midend_timer(me, tplus); | ||
190 | } | ||
191 | |||
192 | /* ---------------------------------------------------------------------- | ||
193 | * Helper functions to resize the canvas, and variables to remember | ||
194 | * its size for other functions (e.g. trimming blitter rectangles). | ||
195 | */ | ||
196 | static int canvas_w, canvas_h; | ||
197 | |||
198 | /* Called when we resize as a result of changing puzzle settings */ | ||
199 | static void resize(void) | ||
200 | { | ||
201 | int w, h; | ||
202 | w = h = INT_MAX; | ||
203 | midend_size(me, &w, &h, FALSE); | ||
204 | js_canvas_set_size(w, h); | ||
205 | canvas_w = w; | ||
206 | canvas_h = h; | ||
207 | } | ||
208 | |||
209 | /* Called from JS when the user uses the resize handle */ | ||
210 | void resize_puzzle(int w, int h) | ||
211 | { | ||
212 | midend_size(me, &w, &h, TRUE); | ||
213 | if (canvas_w != w || canvas_h != h) { | ||
214 | js_canvas_set_size(w, h); | ||
215 | canvas_w = w; | ||
216 | canvas_h = h; | ||
217 | midend_force_redraw(me); | ||
218 | } | ||
219 | } | ||
220 | |||
221 | /* Called from JS when the user uses the restore button */ | ||
222 | void restore_puzzle_size(int w, int h) | ||
223 | { | ||
224 | midend_reset_tilesize(me); | ||
225 | resize(); | ||
226 | midend_force_redraw(me); | ||
227 | } | ||
228 | |||
229 | /* | ||
230 | * HTML doesn't give us a default frontend colour of its own, so we | ||
231 | * just make up a lightish grey ourselves. | ||
232 | */ | ||
233 | void frontend_default_colour(frontend *fe, float *output) | ||
234 | { | ||
235 | output[0] = output[1] = output[2] = 0.9F; | ||
236 | } | ||
237 | |||
238 | /* | ||
239 | * Helper function called from all over the place to ensure the undo | ||
240 | * and redo buttons get properly enabled and disabled after every move | ||
241 | * or undo or new-game event. | ||
242 | */ | ||
243 | static void update_undo_redo(void) | ||
244 | { | ||
245 | js_enable_undo_redo(midend_can_undo(me), midend_can_redo(me)); | ||
246 | } | ||
247 | |||
248 | /* | ||
249 | * Mouse event handlers called from JS. | ||
250 | */ | ||
251 | void mousedown(int x, int y, int button) | ||
252 | { | ||
253 | button = (button == 0 ? LEFT_BUTTON : | ||
254 | button == 1 ? MIDDLE_BUTTON : RIGHT_BUTTON); | ||
255 | midend_process_key(me, x, y, button); | ||
256 | update_undo_redo(); | ||
257 | } | ||
258 | |||
259 | void mouseup(int x, int y, int button) | ||
260 | { | ||
261 | button = (button == 0 ? LEFT_RELEASE : | ||
262 | button == 1 ? MIDDLE_RELEASE : RIGHT_RELEASE); | ||
263 | midend_process_key(me, x, y, button); | ||
264 | update_undo_redo(); | ||
265 | } | ||
266 | |||
267 | void mousemove(int x, int y, int buttons) | ||
268 | { | ||
269 | int button = (buttons & 2 ? MIDDLE_DRAG : | ||
270 | buttons & 4 ? RIGHT_DRAG : LEFT_DRAG); | ||
271 | midend_process_key(me, x, y, button); | ||
272 | update_undo_redo(); | ||
273 | } | ||
274 | |||
275 | /* | ||
276 | * Keyboard handler called from JS. | ||
277 | */ | ||
278 | void key(int keycode, int charcode, const char *key, const char *chr, | ||
279 | int shift, int ctrl) | ||
280 | { | ||
281 | int keyevent = -1; | ||
282 | |||
283 | if (!strnullcmp(key, "Backspace") || !strnullcmp(key, "Del") || | ||
284 | keycode == 8 || keycode == 46) { | ||
285 | keyevent = 127; /* Backspace / Delete */ | ||
286 | } else if (!strnullcmp(key, "Enter") || keycode == 13) { | ||
287 | keyevent = 13; /* return */ | ||
288 | } else if (!strnullcmp(key, "Left") || keycode == 37) { | ||
289 | keyevent = CURSOR_LEFT; | ||
290 | } else if (!strnullcmp(key, "Up") || keycode == 38) { | ||
291 | keyevent = CURSOR_UP; | ||
292 | } else if (!strnullcmp(key, "Right") || keycode == 39) { | ||
293 | keyevent = CURSOR_RIGHT; | ||
294 | } else if (!strnullcmp(key, "Down") || keycode == 40) { | ||
295 | keyevent = CURSOR_DOWN; | ||
296 | } else if (!strnullcmp(key, "End") || keycode == 35) { | ||
297 | /* | ||
298 | * We interpret Home, End, PgUp and PgDn as numeric keypad | ||
299 | * controls regardless of whether they're the ones on the | ||
300 | * numeric keypad (since we can't tell). The effect of | ||
301 | * this should only be that the non-numeric-pad versions | ||
302 | * of those keys generate directions in 8-way movement | ||
303 | * puzzles like Cube and Inertia. | ||
304 | */ | ||
305 | keyevent = MOD_NUM_KEYPAD | '1'; | ||
306 | } else if (!strnullcmp(key, "PageDown") || keycode==34) { | ||
307 | keyevent = MOD_NUM_KEYPAD | '3'; | ||
308 | } else if (!strnullcmp(key, "Home") || keycode==36) { | ||
309 | keyevent = MOD_NUM_KEYPAD | '7'; | ||
310 | } else if (!strnullcmp(key, "PageUp") || keycode==33) { | ||
311 | keyevent = MOD_NUM_KEYPAD | '9'; | ||
312 | } else if (chr && chr[0] && !chr[1]) { | ||
313 | keyevent = chr[0] & 0xFF; | ||
314 | } else if (keycode >= 96 && keycode < 106) { | ||
315 | keyevent = MOD_NUM_KEYPAD | ('0' + keycode - 96); | ||
316 | } else if (keycode >= 65 && keycode <= 90) { | ||
317 | keyevent = keycode + (shift ? 0 : 32); | ||
318 | } else if (keycode >= 48 && keycode <= 57) { | ||
319 | keyevent = keycode; | ||
320 | } else if (keycode == 32) { /* space / CURSOR_SELECT2 */ | ||
321 | keyevent = keycode; | ||
322 | } | ||
323 | |||
324 | if (keyevent >= 0) { | ||
325 | if (shift && keyevent >= 0x100) | ||
326 | keyevent |= MOD_SHFT; | ||
327 | |||
328 | if (ctrl) { | ||
329 | if (keyevent >= 0x100) | ||
330 | keyevent |= MOD_CTRL; | ||
331 | else | ||
332 | keyevent &= 0x1F; | ||
333 | } | ||
334 | |||
335 | midend_process_key(me, 0, 0, keyevent); | ||
336 | update_undo_redo(); | ||
337 | } | ||
338 | } | ||
339 | |||
340 | /* | ||
341 | * Helper function called from several places to update the permalinks | ||
342 | * whenever a new game is created. | ||
343 | */ | ||
344 | static void update_permalinks(void) | ||
345 | { | ||
346 | char *desc, *seed; | ||
347 | desc = midend_get_game_id(me); | ||
348 | seed = midend_get_random_seed(me); | ||
349 | js_update_permalinks(desc, seed); | ||
350 | sfree(desc); | ||
351 | sfree(seed); | ||
352 | } | ||
353 | |||
354 | /* | ||
355 | * Callback from the midend when the game ids change, so we can update | ||
356 | * the permalinks. | ||
357 | */ | ||
358 | static void ids_changed(void *ignored) | ||
359 | { | ||
360 | update_permalinks(); | ||
361 | } | ||
362 | |||
363 | /* ---------------------------------------------------------------------- | ||
364 | * Implementation of the drawing API by calling Javascript canvas | ||
365 | * drawing functions. (Well, half of it; the other half is on the JS | ||
366 | * side.) | ||
367 | */ | ||
368 | static void js_start_draw(void *handle) | ||
369 | { | ||
370 | js_canvas_start_draw(); | ||
371 | } | ||
372 | |||
373 | static void js_clip(void *handle, int x, int y, int w, int h) | ||
374 | { | ||
375 | js_canvas_clip_rect(x, y, w, h); | ||
376 | } | ||
377 | |||
378 | static void js_unclip(void *handle) | ||
379 | { | ||
380 | js_canvas_unclip(); | ||
381 | } | ||
382 | |||
383 | static void js_draw_text(void *handle, int x, int y, int fonttype, | ||
384 | int fontsize, int align, int colour, char *text) | ||
385 | { | ||
386 | char fontstyle[80]; | ||
387 | int halign; | ||
388 | |||
389 | sprintf(fontstyle, "%dpx %s", fontsize, | ||
390 | fonttype == FONT_FIXED ? "monospace" : "sans-serif"); | ||
391 | |||
392 | if (align & ALIGN_VCENTRE) | ||
393 | y += js_canvas_find_font_midpoint(fontsize, fontstyle); | ||
394 | |||
395 | if (align & ALIGN_HCENTRE) | ||
396 | halign = 1; | ||
397 | else if (align & ALIGN_HRIGHT) | ||
398 | halign = 2; | ||
399 | else | ||
400 | halign = 0; | ||
401 | |||
402 | js_canvas_draw_text(x, y, halign, colour_strings[colour], fontstyle, text); | ||
403 | } | ||
404 | |||
405 | static void js_draw_rect(void *handle, int x, int y, int w, int h, int colour) | ||
406 | { | ||
407 | js_canvas_draw_rect(x, y, w, h, colour_strings[colour]); | ||
408 | } | ||
409 | |||
410 | static void js_draw_line(void *handle, int x1, int y1, int x2, int y2, | ||
411 | int colour) | ||
412 | { | ||
413 | js_canvas_draw_line(x1, y1, x2, y2, 1, colour_strings[colour]); | ||
414 | } | ||
415 | |||
416 | static void js_draw_thick_line(void *handle, float thickness, | ||
417 | float x1, float y1, float x2, float y2, | ||
418 | int colour) | ||
419 | { | ||
420 | js_canvas_draw_line(x1, y1, x2, y2, thickness, colour_strings[colour]); | ||
421 | } | ||
422 | |||
423 | static void js_draw_poly(void *handle, int *coords, int npoints, | ||
424 | int fillcolour, int outlinecolour) | ||
425 | { | ||
426 | js_canvas_draw_poly(coords, npoints, | ||
427 | fillcolour >= 0 ? colour_strings[fillcolour] : NULL, | ||
428 | colour_strings[outlinecolour]); | ||
429 | } | ||
430 | |||
431 | static void js_draw_circle(void *handle, int cx, int cy, int radius, | ||
432 | int fillcolour, int outlinecolour) | ||
433 | { | ||
434 | js_canvas_draw_circle(cx, cy, radius, | ||
435 | fillcolour >= 0 ? colour_strings[fillcolour] : NULL, | ||
436 | colour_strings[outlinecolour]); | ||
437 | } | ||
438 | |||
439 | struct blitter { | ||
440 | int id; /* allocated on the js side */ | ||
441 | int w, h; /* easier to retain here */ | ||
442 | }; | ||
443 | |||
444 | static blitter *js_blitter_new(void *handle, int w, int h) | ||
445 | { | ||
446 | blitter *bl = snew(blitter); | ||
447 | bl->w = w; | ||
448 | bl->h = h; | ||
449 | bl->id = js_canvas_new_blitter(w, h); | ||
450 | return bl; | ||
451 | } | ||
452 | |||
453 | static void js_blitter_free(void *handle, blitter *bl) | ||
454 | { | ||
455 | js_canvas_free_blitter(bl->id); | ||
456 | sfree(bl); | ||
457 | } | ||
458 | |||
459 | static void trim_rect(int *x, int *y, int *w, int *h) | ||
460 | { | ||
461 | int x0, x1, y0, y1; | ||
462 | |||
463 | /* | ||
464 | * Reduce the size of the copied rectangle to stop it going | ||
465 | * outside the bounds of the canvas. | ||
466 | */ | ||
467 | |||
468 | /* Transform from x,y,w,h form into coordinates of all edges */ | ||
469 | x0 = *x; | ||
470 | y0 = *y; | ||
471 | x1 = *x + *w; | ||
472 | y1 = *y + *h; | ||
473 | |||
474 | /* Clip each coordinate at both extremes of the canvas */ | ||
475 | x0 = (x0 < 0 ? 0 : x0 > canvas_w ? canvas_w : x0); | ||
476 | x1 = (x1 < 0 ? 0 : x1 > canvas_w ? canvas_w : x1); | ||
477 | y0 = (y0 < 0 ? 0 : y0 > canvas_h ? canvas_h : y0); | ||
478 | y1 = (y1 < 0 ? 0 : y1 > canvas_h ? canvas_h : y1); | ||
479 | |||
480 | /* Transform back into x,y,w,h to return */ | ||
481 | *x = x0; | ||
482 | *y = y0; | ||
483 | *w = x1 - x0; | ||
484 | *h = y1 - y0; | ||
485 | } | ||
486 | |||
487 | static void js_blitter_save(void *handle, blitter *bl, int x, int y) | ||
488 | { | ||
489 | int w = bl->w, h = bl->h; | ||
490 | trim_rect(&x, &y, &w, &h); | ||
491 | if (w > 0 && h > 0) | ||
492 | js_canvas_copy_to_blitter(bl->id, x, y, w, h); | ||
493 | } | ||
494 | |||
495 | static void js_blitter_load(void *handle, blitter *bl, int x, int y) | ||
496 | { | ||
497 | int w = bl->w, h = bl->h; | ||
498 | trim_rect(&x, &y, &w, &h); | ||
499 | if (w > 0 && h > 0) | ||
500 | js_canvas_copy_from_blitter(bl->id, x, y, w, h); | ||
501 | } | ||
502 | |||
503 | static void js_draw_update(void *handle, int x, int y, int w, int h) | ||
504 | { | ||
505 | trim_rect(&x, &y, &w, &h); | ||
506 | if (w > 0 && h > 0) | ||
507 | js_canvas_draw_update(x, y, w, h); | ||
508 | } | ||
509 | |||
510 | static void js_end_draw(void *handle) | ||
511 | { | ||
512 | js_canvas_end_draw(); | ||
513 | } | ||
514 | |||
515 | static void js_status_bar(void *handle, char *text) | ||
516 | { | ||
517 | js_canvas_set_statusbar(text); | ||
518 | } | ||
519 | |||
520 | static char *js_text_fallback(void *handle, const char *const *strings, | ||
521 | int nstrings) | ||
522 | { | ||
523 | return dupstr(strings[0]); /* Emscripten has no trouble with UTF-8 */ | ||
524 | } | ||
525 | |||
526 | const struct drawing_api js_drawing = { | ||
527 | js_draw_text, | ||
528 | js_draw_rect, | ||
529 | js_draw_line, | ||
530 | js_draw_poly, | ||
531 | js_draw_circle, | ||
532 | js_draw_update, | ||
533 | js_clip, | ||
534 | js_unclip, | ||
535 | js_start_draw, | ||
536 | js_end_draw, | ||
537 | js_status_bar, | ||
538 | js_blitter_new, | ||
539 | js_blitter_free, | ||
540 | js_blitter_save, | ||
541 | js_blitter_load, | ||
542 | NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */ | ||
543 | NULL, NULL, /* line_width, line_dotted */ | ||
544 | js_text_fallback, | ||
545 | js_draw_thick_line, | ||
546 | }; | ||
547 | |||
548 | /* ---------------------------------------------------------------------- | ||
549 | * Presets and game-configuration dialog support. | ||
550 | */ | ||
551 | static game_params **presets; | ||
552 | static int npresets; | ||
553 | int have_presets_dropdown; | ||
554 | |||
555 | void select_appropriate_preset(void) | ||
556 | { | ||
557 | if (have_presets_dropdown) { | ||
558 | int preset = midend_which_preset(me); | ||
559 | js_select_preset(preset < 0 ? -1 : preset); | ||
560 | } | ||
561 | } | ||
562 | |||
563 | static config_item *cfg = NULL; | ||
564 | static int cfg_which; | ||
565 | |||
566 | /* | ||
567 | * Set up a dialog box. This is pretty easy on the C side; most of the | ||
568 | * work is done in JS. | ||
569 | */ | ||
570 | static void cfg_start(int which) | ||
571 | { | ||
572 | char *title; | ||
573 | int i; | ||
574 | |||
575 | cfg = midend_get_config(me, which, &title); | ||
576 | cfg_which = which; | ||
577 | |||
578 | js_dialog_init(title); | ||
579 | sfree(title); | ||
580 | |||
581 | for (i = 0; cfg[i].type != C_END; i++) { | ||
582 | switch (cfg[i].type) { | ||
583 | case C_STRING: | ||
584 | js_dialog_string(i, cfg[i].name, cfg[i].sval); | ||
585 | break; | ||
586 | case C_BOOLEAN: | ||
587 | js_dialog_boolean(i, cfg[i].name, cfg[i].ival); | ||
588 | break; | ||
589 | case C_CHOICES: | ||
590 | js_dialog_choices(i, cfg[i].name, cfg[i].sval, cfg[i].ival); | ||
591 | break; | ||
592 | } | ||
593 | } | ||
594 | |||
595 | js_dialog_launch(); | ||
596 | } | ||
597 | |||
598 | /* | ||
599 | * Callbacks from JS when the OK button is clicked, to return the | ||
600 | * final state of each control. | ||
601 | */ | ||
602 | void dlg_return_sval(int index, const char *val) | ||
603 | { | ||
604 | sfree(cfg[index].sval); | ||
605 | cfg[index].sval = dupstr(val); | ||
606 | } | ||
607 | void dlg_return_ival(int index, int val) | ||
608 | { | ||
609 | cfg[index].ival = val; | ||
610 | } | ||
611 | |||
612 | /* | ||
613 | * Called when the user clicks OK or Cancel. use_results will be TRUE | ||
614 | * or FALSE respectively, in those cases. We terminate the dialog box, | ||
615 | * unless the user selected an invalid combination of parameters. | ||
616 | */ | ||
617 | static void cfg_end(int use_results) | ||
618 | { | ||
619 | if (use_results) { | ||
620 | /* | ||
621 | * User hit OK. | ||
622 | */ | ||
623 | char *err = midend_set_config(me, cfg_which, cfg); | ||
624 | |||
625 | if (err) { | ||
626 | /* | ||
627 | * The settings were unacceptable, so leave the config box | ||
628 | * open for the user to adjust them and try again. | ||
629 | */ | ||
630 | js_error_box(err); | ||
631 | } else { | ||
632 | /* | ||
633 | * New settings are fine; start a new game and close the | ||
634 | * dialog. | ||
635 | */ | ||
636 | select_appropriate_preset(); | ||
637 | midend_new_game(me); | ||
638 | resize(); | ||
639 | midend_redraw(me); | ||
640 | free_cfg(cfg); | ||
641 | js_dialog_cleanup(); | ||
642 | } | ||
643 | } else { | ||
644 | /* | ||
645 | * User hit Cancel. Close the dialog, but also we must still | ||
646 | * reselect the right element of the dropdown list. | ||
647 | * | ||
648 | * (Because: imagine you have a preset selected, and then you | ||
649 | * select Custom from the list, but change your mind and hit | ||
650 | * Esc. The Custom option will now still be selected in the | ||
651 | * list, whereas obviously it should show the preset you still | ||
652 | * _actually_ have selected. Worse still, it'll be the visible | ||
653 | * rather than invisible Custom option - see the comment in | ||
654 | * js_add_preset in emcclib.js - so you won't even be able to | ||
655 | * select Custom without a faffy workaround.) | ||
656 | */ | ||
657 | select_appropriate_preset(); | ||
658 | |||
659 | free_cfg(cfg); | ||
660 | js_dialog_cleanup(); | ||
661 | } | ||
662 | } | ||
663 | |||
664 | /* ---------------------------------------------------------------------- | ||
665 | * Called from JS when a command is given to the puzzle by clicking a | ||
666 | * button or control of some sort. | ||
667 | */ | ||
668 | void command(int n) | ||
669 | { | ||
670 | switch (n) { | ||
671 | case 0: /* specific game ID */ | ||
672 | cfg_start(CFG_DESC); | ||
673 | break; | ||
674 | case 1: /* random game seed */ | ||
675 | cfg_start(CFG_SEED); | ||
676 | break; | ||
677 | case 2: /* game parameter dropdown changed */ | ||
678 | { | ||
679 | int i = js_get_selected_preset(); | ||
680 | if (i < 0) { | ||
681 | /* | ||
682 | * The user selected 'Custom', so launch the config | ||
683 | * box. | ||
684 | */ | ||
685 | if (thegame.can_configure) /* (double-check just in case) */ | ||
686 | cfg_start(CFG_SETTINGS); | ||
687 | } else { | ||
688 | /* | ||
689 | * The user selected a preset, so just switch straight | ||
690 | * to that. | ||
691 | */ | ||
692 | assert(i < npresets); | ||
693 | midend_set_params(me, presets[i]); | ||
694 | midend_new_game(me); | ||
695 | resize(); | ||
696 | midend_redraw(me); | ||
697 | update_undo_redo(); | ||
698 | js_focus_canvas(); | ||
699 | select_appropriate_preset(); /* sort out Custom/Customise */ | ||
700 | } | ||
701 | } | ||
702 | break; | ||
703 | case 3: /* OK clicked in a config box */ | ||
704 | cfg_end(TRUE); | ||
705 | update_undo_redo(); | ||
706 | break; | ||
707 | case 4: /* Cancel clicked in a config box */ | ||
708 | cfg_end(FALSE); | ||
709 | update_undo_redo(); | ||
710 | break; | ||
711 | case 5: /* New Game */ | ||
712 | midend_process_key(me, 0, 0, 'n'); | ||
713 | update_undo_redo(); | ||
714 | js_focus_canvas(); | ||
715 | break; | ||
716 | case 6: /* Restart */ | ||
717 | midend_restart_game(me); | ||
718 | update_undo_redo(); | ||
719 | js_focus_canvas(); | ||
720 | break; | ||
721 | case 7: /* Undo */ | ||
722 | midend_process_key(me, 0, 0, 'u'); | ||
723 | update_undo_redo(); | ||
724 | js_focus_canvas(); | ||
725 | break; | ||
726 | case 8: /* Redo */ | ||
727 | midend_process_key(me, 0, 0, 'r'); | ||
728 | update_undo_redo(); | ||
729 | js_focus_canvas(); | ||
730 | break; | ||
731 | case 9: /* Solve */ | ||
732 | if (thegame.can_solve) { | ||
733 | char *msg = midend_solve(me); | ||
734 | if (msg) | ||
735 | js_error_box(msg); | ||
736 | } | ||
737 | update_undo_redo(); | ||
738 | js_focus_canvas(); | ||
739 | break; | ||
740 | } | ||
741 | } | ||
742 | |||
743 | /* ---------------------------------------------------------------------- | ||
744 | * Setup function called at page load time. It's called main() because | ||
745 | * that's the most convenient thing in Emscripten, but it's not main() | ||
746 | * in the usual sense of bounding the program's entire execution. | ||
747 | * Instead, this function returns once the initial puzzle is set up | ||
748 | * and working, and everything thereafter happens by means of JS event | ||
749 | * handlers sending us callbacks. | ||
750 | */ | ||
751 | int main(int argc, char **argv) | ||
752 | { | ||
753 | char *param_err; | ||
754 | float *colours; | ||
755 | int i; | ||
756 | |||
757 | /* | ||
758 | * Instantiate a midend. | ||
759 | */ | ||
760 | me = midend_new(NULL, &thegame, &js_drawing, NULL); | ||
761 | |||
762 | /* | ||
763 | * Chuck in the HTML fragment ID if we have one (trimming the | ||
764 | * leading # off the front first). If that's invalid, we retain | ||
765 | * the error message and will display it at the end, after setting | ||
766 | * up a random puzzle as usual. | ||
767 | */ | ||
768 | if (argc > 1 && argv[1][0] == '#' && argv[1][1] != '\0') | ||
769 | param_err = midend_game_id(me, argv[1] + 1); | ||
770 | else | ||
771 | param_err = NULL; | ||
772 | |||
773 | /* | ||
774 | * Create either a random game or the specified one, and set the | ||
775 | * canvas size appropriately. | ||
776 | */ | ||
777 | midend_new_game(me); | ||
778 | resize(); | ||
779 | |||
780 | /* | ||
781 | * Create a status bar, if needed. | ||
782 | */ | ||
783 | if (midend_wants_statusbar(me)) | ||
784 | js_canvas_make_statusbar(); | ||
785 | |||
786 | /* | ||
787 | * Set up the game-type dropdown with presets and/or the Custom | ||
788 | * option. | ||
789 | */ | ||
790 | npresets = midend_num_presets(me); | ||
791 | if (npresets == 0) { | ||
792 | /* | ||
793 | * This puzzle doesn't have selectable game types at all. | ||
794 | * Completely remove the drop-down list from the page. | ||
795 | */ | ||
796 | js_remove_type_dropdown(); | ||
797 | have_presets_dropdown = FALSE; | ||
798 | } else { | ||
799 | presets = snewn(npresets, game_params *); | ||
800 | for (i = 0; i < npresets; i++) { | ||
801 | char *name; | ||
802 | midend_fetch_preset(me, i, &name, &presets[i]); | ||
803 | js_add_preset(name); | ||
804 | } | ||
805 | if (thegame.can_configure) | ||
806 | js_add_preset(NULL); /* the 'Custom' entry in the dropdown */ | ||
807 | |||
808 | have_presets_dropdown = TRUE; | ||
809 | |||
810 | /* | ||
811 | * Now ensure the appropriate element of the presets menu | ||
812 | * starts off selected, in case it isn't the first one in the | ||
813 | * list (e.g. Slant). | ||
814 | */ | ||
815 | select_appropriate_preset(); | ||
816 | } | ||
817 | |||
818 | /* | ||
819 | * Remove the Solve button if the game doesn't support it. | ||
820 | */ | ||
821 | if (!thegame.can_solve) | ||
822 | js_remove_solve_button(); | ||
823 | |||
824 | /* | ||
825 | * Retrieve the game's colours, and convert them into #abcdef type | ||
826 | * hex ID strings. | ||
827 | */ | ||
828 | colours = midend_colours(me, &ncolours); | ||
829 | colour_strings = snewn(ncolours, char *); | ||
830 | for (i = 0; i < ncolours; i++) { | ||
831 | char col[40]; | ||
832 | sprintf(col, "#%02x%02x%02x", | ||
833 | (unsigned)(0.5 + 255 * colours[i*3+0]), | ||
834 | (unsigned)(0.5 + 255 * colours[i*3+1]), | ||
835 | (unsigned)(0.5 + 255 * colours[i*3+2])); | ||
836 | colour_strings[i] = dupstr(col); | ||
837 | } | ||
838 | |||
839 | /* | ||
840 | * Request notification when the game ids change (e.g. if the user | ||
841 | * presses 'n', and also when Mines supersedes its game | ||
842 | * description), so that we can proactively update the permalink. | ||
843 | */ | ||
844 | midend_request_id_changes(me, ids_changed, NULL); | ||
845 | |||
846 | /* | ||
847 | * Draw the puzzle's initial state, and set up the permalinks and | ||
848 | * undo/redo greying out. | ||
849 | */ | ||
850 | midend_redraw(me); | ||
851 | update_permalinks(); | ||
852 | update_undo_redo(); | ||
853 | |||
854 | /* | ||
855 | * If we were given an erroneous game ID in argv[1], now's the | ||
856 | * time to put up the error box about it, after we've fully set up | ||
857 | * a random puzzle. Then when the user clicks 'ok', we have a | ||
858 | * puzzle for them. | ||
859 | */ | ||
860 | if (param_err) | ||
861 | js_error_box(param_err); | ||
862 | |||
863 | /* | ||
864 | * Done. Return to JS, and await callbacks! | ||
865 | */ | ||
866 | return 0; | ||
867 | } | ||