diff options
Diffstat (limited to 'apps/plugins/puzzles/src/midend.c')
-rw-r--r-- | apps/plugins/puzzles/src/midend.c | 797 |
1 files changed, 695 insertions, 102 deletions
diff --git a/apps/plugins/puzzles/src/midend.c b/apps/plugins/puzzles/src/midend.c index f2d27d3e31..30fdf1b830 100644 --- a/apps/plugins/puzzles/src/midend.c +++ b/apps/plugins/puzzles/src/midend.c | |||
@@ -30,6 +30,11 @@ struct midend_serialise_buf { | |||
30 | int len, size; | 30 | int len, size; |
31 | }; | 31 | }; |
32 | 32 | ||
33 | struct midend_serialise_buf_read_ctx { | ||
34 | struct midend_serialise_buf *ser; | ||
35 | int len, pos; | ||
36 | }; | ||
37 | |||
33 | struct midend { | 38 | struct midend { |
34 | frontend *frontend; | 39 | frontend *frontend; |
35 | random_state *random; | 40 | random_state *random; |
@@ -73,6 +78,7 @@ struct midend { | |||
73 | 78 | ||
74 | game_params *params, *curparams; | 79 | game_params *params, *curparams; |
75 | game_drawstate *drawstate; | 80 | game_drawstate *drawstate; |
81 | bool first_draw; | ||
76 | game_ui *ui; | 82 | game_ui *ui; |
77 | 83 | ||
78 | game_state *oldstate; | 84 | game_state *oldstate; |
@@ -88,10 +94,15 @@ struct midend { | |||
88 | 94 | ||
89 | int pressed_mouse_button; | 95 | int pressed_mouse_button; |
90 | 96 | ||
91 | int preferred_tilesize, tilesize, winwidth, winheight; | 97 | struct midend_serialise_buf be_prefs; |
98 | |||
99 | int preferred_tilesize, preferred_tilesize_dpr, tilesize; | ||
100 | int winwidth, winheight; | ||
92 | 101 | ||
93 | void (*game_id_change_notify_function)(void *); | 102 | void (*game_id_change_notify_function)(void *); |
94 | void *game_id_change_notify_ctx; | 103 | void *game_id_change_notify_ctx; |
104 | |||
105 | bool one_key_shortcuts; | ||
95 | }; | 106 | }; |
96 | 107 | ||
97 | #define ensure(me) do { \ | 108 | #define ensure(me) do { \ |
@@ -119,21 +130,33 @@ struct deserialise_data { | |||
119 | }; | 130 | }; |
120 | 131 | ||
121 | /* | 132 | /* |
122 | * Forward reference. | 133 | * Forward references. |
123 | */ | 134 | */ |
124 | static const char *midend_deserialise_internal( | 135 | static const char *midend_deserialise_internal( |
125 | midend *me, bool (*read)(void *ctx, void *buf, int len), void *rctx, | 136 | midend *me, bool (*read)(void *ctx, void *buf, int len), void *rctx, |
126 | const char *(*check)(void *ctx, midend *, const struct deserialise_data *), | 137 | const char *(*check)(void *ctx, midend *, const struct deserialise_data *), |
127 | void *cctx); | 138 | void *cctx); |
139 | static void midend_serialise_prefs( | ||
140 | midend *me, game_ui *ui, | ||
141 | void (*write)(void *ctx, const void *buf, int len), void *wctx); | ||
142 | static const char *midend_deserialise_prefs( | ||
143 | midend *me, game_ui *ui, | ||
144 | bool (*read)(void *ctx, void *buf, int len), void *rctx); | ||
145 | static config_item *midend_get_prefs(midend *me, game_ui *ui); | ||
146 | static void midend_set_prefs(midend *me, game_ui *ui, config_item *all_prefs); | ||
147 | static void midend_apply_prefs(midend *me, game_ui *ui); | ||
128 | 148 | ||
129 | void midend_reset_tilesize(midend *me) | 149 | void midend_reset_tilesize(midend *me) |
130 | { | 150 | { |
131 | me->preferred_tilesize = me->ourgame->preferred_tilesize; | 151 | me->preferred_tilesize = me->ourgame->preferred_tilesize; |
152 | me->preferred_tilesize_dpr = 1.0; | ||
132 | { | 153 | { |
133 | /* | 154 | /* |
134 | * Allow an environment-based override for the default tile | 155 | * Allow an environment-based override for the default tile |
135 | * size by defining a variable along the lines of | 156 | * size by defining a variable along the lines of |
136 | * `NET_TILESIZE=15'. | 157 | * `NET_TILESIZE=15'. |
158 | * | ||
159 | * XXX How should this interact with DPR? | ||
137 | */ | 160 | */ |
138 | 161 | ||
139 | char buf[80], *e; | 162 | char buf[80], *e; |
@@ -196,6 +219,7 @@ midend *midend_new(frontend *fe, const game *ourgame, | |||
196 | me->aux_info = NULL; | 219 | me->aux_info = NULL; |
197 | me->genmode = GOT_NOTHING; | 220 | me->genmode = GOT_NOTHING; |
198 | me->drawstate = NULL; | 221 | me->drawstate = NULL; |
222 | me->first_draw = true; | ||
199 | me->oldstate = NULL; | 223 | me->oldstate = NULL; |
200 | me->preset_menu = NULL; | 224 | me->preset_menu = NULL; |
201 | me->anim_time = me->anim_pos = 0.0F; | 225 | me->anim_time = me->anim_pos = 0.0F; |
@@ -212,6 +236,11 @@ midend *midend_new(frontend *fe, const game *ourgame, | |||
212 | else | 236 | else |
213 | me->drawing = NULL; | 237 | me->drawing = NULL; |
214 | 238 | ||
239 | me->be_prefs.buf = NULL; | ||
240 | me->be_prefs.size = me->be_prefs.len = 0; | ||
241 | |||
242 | me->one_key_shortcuts = true; | ||
243 | |||
215 | midend_reset_tilesize(me); | 244 | midend_reset_tilesize(me); |
216 | 245 | ||
217 | sfree(randseed); | 246 | sfree(randseed); |
@@ -280,6 +309,7 @@ void midend_free(midend *me) | |||
280 | sfree(me->privdesc); | 309 | sfree(me->privdesc); |
281 | sfree(me->seedstr); | 310 | sfree(me->seedstr); |
282 | sfree(me->aux_info); | 311 | sfree(me->aux_info); |
312 | sfree(me->be_prefs.buf); | ||
283 | me->ourgame->free_params(me->params); | 313 | me->ourgame->free_params(me->params); |
284 | midend_free_preset_menu(me, me->preset_menu); | 314 | midend_free_preset_menu(me, me->preset_menu); |
285 | if (me->ui) | 315 | if (me->ui) |
@@ -297,14 +327,56 @@ static void midend_size_new_drawstate(midend *me) | |||
297 | * anyway yet. | 327 | * anyway yet. |
298 | */ | 328 | */ |
299 | if (me->tilesize > 0) { | 329 | if (me->tilesize > 0) { |
300 | me->ourgame->compute_size(me->params, me->tilesize, | 330 | me->ourgame->compute_size(me->params, me->tilesize, me->ui, |
301 | &me->winwidth, &me->winheight); | 331 | &me->winwidth, &me->winheight); |
302 | me->ourgame->set_size(me->drawing, me->drawstate, | 332 | me->ourgame->set_size(me->drawing, me->drawstate, |
303 | me->params, me->tilesize); | 333 | me->params, me->tilesize); |
304 | } | 334 | } |
305 | } | 335 | } |
306 | 336 | ||
307 | void midend_size(midend *me, int *x, int *y, bool user_size) | 337 | /* |
338 | * There is no one correct way to convert tilesizes between device | ||
339 | * pixel ratios, because there's only a loosely-defined relationship | ||
340 | * between tilesize and the actual size of a puzzle. We define this | ||
341 | * function as the canonical conversion function so everything in the | ||
342 | * midend will be consistent. | ||
343 | */ | ||
344 | static int convert_tilesize(midend *me, int old_tilesize, | ||
345 | double old_dpr, double new_dpr) | ||
346 | { | ||
347 | int x, y, rx, ry, min, max; | ||
348 | game_params *defaults; | ||
349 | |||
350 | if (new_dpr == old_dpr) | ||
351 | return old_tilesize; | ||
352 | |||
353 | defaults = me->ourgame->default_params(); | ||
354 | |||
355 | me->ourgame->compute_size(defaults, old_tilesize, me->ui, &x, &y); | ||
356 | x *= new_dpr / old_dpr; | ||
357 | y *= new_dpr / old_dpr; | ||
358 | |||
359 | min = max = 1; | ||
360 | do { | ||
361 | max *= 2; | ||
362 | me->ourgame->compute_size(defaults, max, me->ui, &rx, &ry); | ||
363 | } while (rx <= x && ry <= y); | ||
364 | |||
365 | while (max - min > 1) { | ||
366 | int mid = (max + min) / 2; | ||
367 | me->ourgame->compute_size(defaults, mid, me->ui, &rx, &ry); | ||
368 | if (rx <= x && ry <= y) | ||
369 | min = mid; | ||
370 | else | ||
371 | max = mid; | ||
372 | } | ||
373 | |||
374 | me->ourgame->free_params(defaults); | ||
375 | return min; | ||
376 | } | ||
377 | |||
378 | void midend_size(midend *me, int *x, int *y, bool user_size, | ||
379 | double device_pixel_ratio) | ||
308 | { | 380 | { |
309 | int min, max; | 381 | int min, max; |
310 | int rx, ry; | 382 | int rx, ry; |
@@ -318,6 +390,7 @@ void midend_size(midend *me, int *x, int *y, bool user_size) | |||
318 | me->ourgame->free_drawstate(me->drawing, me->drawstate); | 390 | me->ourgame->free_drawstate(me->drawing, me->drawstate); |
319 | me->drawstate = me->ourgame->new_drawstate(me->drawing, | 391 | me->drawstate = me->ourgame->new_drawstate(me->drawing, |
320 | me->states[0].state); | 392 | me->states[0].state); |
393 | me->first_draw = true; | ||
321 | } | 394 | } |
322 | 395 | ||
323 | /* | 396 | /* |
@@ -333,10 +406,12 @@ void midend_size(midend *me, int *x, int *y, bool user_size) | |||
333 | max = 1; | 406 | max = 1; |
334 | do { | 407 | do { |
335 | max *= 2; | 408 | max *= 2; |
336 | me->ourgame->compute_size(me->params, max, &rx, &ry); | 409 | me->ourgame->compute_size(me->params, max, me->ui, &rx, &ry); |
337 | } while (rx <= *x && ry <= *y); | 410 | } while (rx <= *x && ry <= *y); |
338 | } else | 411 | } else |
339 | max = me->preferred_tilesize + 1; | 412 | max = convert_tilesize(me, me->preferred_tilesize, |
413 | me->preferred_tilesize_dpr, | ||
414 | device_pixel_ratio) + 1; | ||
340 | min = 1; | 415 | min = 1; |
341 | 416 | ||
342 | /* | 417 | /* |
@@ -347,7 +422,7 @@ void midend_size(midend *me, int *x, int *y, bool user_size) | |||
347 | */ | 422 | */ |
348 | while (max - min > 1) { | 423 | while (max - min > 1) { |
349 | int mid = (max + min) / 2; | 424 | int mid = (max + min) / 2; |
350 | me->ourgame->compute_size(me->params, mid, &rx, &ry); | 425 | me->ourgame->compute_size(me->params, mid, me->ui, &rx, &ry); |
351 | if (rx <= *x && ry <= *y) | 426 | if (rx <= *x && ry <= *y) |
352 | min = mid; | 427 | min = mid; |
353 | else | 428 | else |
@@ -359,9 +434,11 @@ void midend_size(midend *me, int *x, int *y, bool user_size) | |||
359 | */ | 434 | */ |
360 | 435 | ||
361 | me->tilesize = min; | 436 | me->tilesize = min; |
362 | if (user_size) | 437 | if (user_size) { |
363 | /* If the user requested a change in size, make it permanent. */ | 438 | /* If the user requested a change in size, make it permanent. */ |
364 | me->preferred_tilesize = me->tilesize; | 439 | me->preferred_tilesize = me->tilesize; |
440 | me->preferred_tilesize_dpr = device_pixel_ratio; | ||
441 | } | ||
365 | midend_size_new_drawstate(me); | 442 | midend_size_new_drawstate(me); |
366 | *x = me->winwidth; | 443 | *x = me->winwidth; |
367 | *y = me->winheight; | 444 | *y = me->winheight; |
@@ -380,6 +457,28 @@ game_params *midend_get_params(midend *me) | |||
380 | return me->ourgame->dup_params(me->params); | 457 | return me->ourgame->dup_params(me->params); |
381 | } | 458 | } |
382 | 459 | ||
460 | static char *encode_params(midend *me, const game_params *params, bool full) | ||
461 | { | ||
462 | char *encoded = me->ourgame->encode_params(params, full); | ||
463 | int i; | ||
464 | |||
465 | /* Assert that the params consist of printable ASCII containing | ||
466 | * neither '#' nor ':'. */ | ||
467 | for (i = 0; encoded[i]; i++) | ||
468 | assert(encoded[i] >= 32 && encoded[i] < 127 && | ||
469 | encoded[i] != '#' && encoded[i] != ':'); | ||
470 | return encoded; | ||
471 | } | ||
472 | |||
473 | static void assert_printable_ascii(char const *s) | ||
474 | { | ||
475 | /* Assert that s is entirely printable ASCII, and hence safe for | ||
476 | * writing in a save file. */ | ||
477 | int i; | ||
478 | for (i = 0; s[i]; i++) | ||
479 | assert(s[i] >= 32 && s[i] < 127); | ||
480 | } | ||
481 | |||
383 | static void midend_set_timer(midend *me) | 482 | static void midend_set_timer(midend *me) |
384 | { | 483 | { |
385 | me->timing = (me->ourgame->is_timed && | 484 | me->timing = (me->ourgame->is_timed && |
@@ -397,11 +496,12 @@ void midend_force_redraw(midend *me) | |||
397 | me->ourgame->free_drawstate(me->drawing, me->drawstate); | 496 | me->ourgame->free_drawstate(me->drawing, me->drawstate); |
398 | me->drawstate = me->ourgame->new_drawstate(me->drawing, | 497 | me->drawstate = me->ourgame->new_drawstate(me->drawing, |
399 | me->states[0].state); | 498 | me->states[0].state); |
499 | me->first_draw = true; | ||
400 | midend_size_new_drawstate(me); | 500 | midend_size_new_drawstate(me); |
401 | midend_redraw(me); | 501 | midend_redraw(me); |
402 | } | 502 | } |
403 | 503 | ||
404 | static void newgame_serialise_write(void *ctx, const void *buf, int len) | 504 | static void midend_serialise_buf_write(void *ctx, const void *buf, int len) |
405 | { | 505 | { |
406 | struct midend_serialise_buf *ser = (struct midend_serialise_buf *)ctx; | 506 | struct midend_serialise_buf *ser = (struct midend_serialise_buf *)ctx; |
407 | int new_len; | 507 | int new_len; |
@@ -416,6 +516,18 @@ static void newgame_serialise_write(void *ctx, const void *buf, int len) | |||
416 | ser->len = new_len; | 516 | ser->len = new_len; |
417 | } | 517 | } |
418 | 518 | ||
519 | static bool midend_serialise_buf_read(void *ctx, void *buf, int len) | ||
520 | { | ||
521 | struct midend_serialise_buf_read_ctx *const rctx = ctx; | ||
522 | |||
523 | if (len > rctx->len - rctx->pos) | ||
524 | return false; | ||
525 | |||
526 | memcpy(buf, rctx->ser->buf + rctx->pos, len); | ||
527 | rctx->pos += len; | ||
528 | return true; | ||
529 | } | ||
530 | |||
419 | void midend_new_game(midend *me) | 531 | void midend_new_game(midend *me) |
420 | { | 532 | { |
421 | me->newgame_undo.len = 0; | 533 | me->newgame_undo.len = 0; |
@@ -435,7 +547,7 @@ void midend_new_game(midend *me) | |||
435 | * worse, valid but wrong. | 547 | * worse, valid but wrong. |
436 | */ | 548 | */ |
437 | midend_purge_states(me); | 549 | midend_purge_states(me); |
438 | midend_serialise(me, newgame_serialise_write, &me->newgame_undo); | 550 | midend_serialise(me, midend_serialise_buf_write, &me->newgame_undo); |
439 | } | 551 | } |
440 | 552 | ||
441 | midend_stop_anim(me); | 553 | midend_stop_anim(me); |
@@ -487,6 +599,7 @@ void midend_new_game(midend *me) | |||
487 | */ | 599 | */ |
488 | me->desc = me->ourgame->new_desc(me->curparams, rs, | 600 | me->desc = me->ourgame->new_desc(me->curparams, rs, |
489 | &me->aux_info, (me->drawing != NULL)); | 601 | &me->aux_info, (me->drawing != NULL)); |
602 | assert_printable_ascii(me->desc); | ||
490 | me->privdesc = NULL; | 603 | me->privdesc = NULL; |
491 | random_free(rs); | 604 | random_free(rs); |
492 | } | 605 | } |
@@ -536,6 +649,7 @@ void midend_new_game(midend *me) | |||
536 | me->statepos = 1; | 649 | me->statepos = 1; |
537 | me->drawstate = me->ourgame->new_drawstate(me->drawing, | 650 | me->drawstate = me->ourgame->new_drawstate(me->drawing, |
538 | me->states[0].state); | 651 | me->states[0].state); |
652 | me->first_draw = true; | ||
539 | midend_size_new_drawstate(me); | 653 | midend_size_new_drawstate(me); |
540 | me->elapsed = 0.0F; | 654 | me->elapsed = 0.0F; |
541 | me->flash_pos = me->flash_time = 0.0F; | 655 | me->flash_pos = me->flash_time = 0.0F; |
@@ -543,6 +657,7 @@ void midend_new_game(midend *me) | |||
543 | if (me->ui) | 657 | if (me->ui) |
544 | me->ourgame->free_ui(me->ui); | 658 | me->ourgame->free_ui(me->ui); |
545 | me->ui = me->ourgame->new_ui(me->states[0].state); | 659 | me->ui = me->ourgame->new_ui(me->states[0].state); |
660 | midend_apply_prefs(me, me->ui); | ||
546 | midend_set_timer(me); | 661 | midend_set_timer(me); |
547 | me->pressed_mouse_button = 0; | 662 | me->pressed_mouse_button = 0; |
548 | 663 | ||
@@ -552,31 +667,28 @@ void midend_new_game(midend *me) | |||
552 | me->newgame_can_store_undo = true; | 667 | me->newgame_can_store_undo = true; |
553 | } | 668 | } |
554 | 669 | ||
555 | bool midend_can_undo(midend *me) | 670 | const char *midend_load_prefs( |
671 | midend *me, bool (*read)(void *ctx, void *buf, int len), void *rctx) | ||
556 | { | 672 | { |
557 | return (me->statepos > 1 || me->newgame_undo.len); | 673 | const char *err = midend_deserialise_prefs(me, NULL, read, rctx); |
674 | return err; | ||
558 | } | 675 | } |
559 | 676 | ||
560 | bool midend_can_redo(midend *me) | 677 | void midend_save_prefs(midend *me, |
678 | void (*write)(void *ctx, const void *buf, int len), | ||
679 | void *wctx) | ||
561 | { | 680 | { |
562 | return (me->statepos < me->nstates || me->newgame_redo.len); | 681 | midend_serialise_prefs(me, NULL, write, wctx); |
563 | } | 682 | } |
564 | 683 | ||
565 | struct newgame_undo_deserialise_read_ctx { | 684 | bool midend_can_undo(midend *me) |
566 | struct midend_serialise_buf *ser; | ||
567 | int len, pos; | ||
568 | }; | ||
569 | |||
570 | static bool newgame_undo_deserialise_read(void *ctx, void *buf, int len) | ||
571 | { | 685 | { |
572 | struct newgame_undo_deserialise_read_ctx *const rctx = ctx; | 686 | return (me->statepos > 1 || me->newgame_undo.len); |
573 | 687 | } | |
574 | if (len > rctx->len - rctx->pos) | ||
575 | return false; | ||
576 | 688 | ||
577 | memcpy(buf, rctx->ser->buf + rctx->pos, len); | 689 | bool midend_can_redo(midend *me) |
578 | rctx->pos += len; | 690 | { |
579 | return true; | 691 | return (me->statepos < me->nstates || me->newgame_redo.len); |
580 | } | 692 | } |
581 | 693 | ||
582 | struct newgame_undo_deserialise_check_ctx { | 694 | struct newgame_undo_deserialise_check_ctx { |
@@ -613,8 +725,8 @@ static const char *newgame_undo_deserialise_check( | |||
613 | * We check both params and cparams, to be as safe as possible. | 725 | * We check both params and cparams, to be as safe as possible. |
614 | */ | 726 | */ |
615 | 727 | ||
616 | old = me->ourgame->encode_params(me->params, true); | 728 | old = encode_params(me, me->params, true); |
617 | new = me->ourgame->encode_params(data->params, true); | 729 | new = encode_params(me, data->params, true); |
618 | if (strcmp(old, new)) { | 730 | if (strcmp(old, new)) { |
619 | /* Set a flag to distinguish this deserialise failure | 731 | /* Set a flag to distinguish this deserialise failure |
620 | * from one due to faulty decoding */ | 732 | * from one due to faulty decoding */ |
@@ -622,8 +734,8 @@ static const char *newgame_undo_deserialise_check( | |||
622 | return "Undoing this new-game operation would change params"; | 734 | return "Undoing this new-game operation would change params"; |
623 | } | 735 | } |
624 | 736 | ||
625 | old = me->ourgame->encode_params(me->curparams, true); | 737 | old = encode_params(me, me->curparams, true); |
626 | new = me->ourgame->encode_params(data->cparams, true); | 738 | new = encode_params(me, data->cparams, true); |
627 | if (strcmp(old, new)) { | 739 | if (strcmp(old, new)) { |
628 | ctx->refused = true; | 740 | ctx->refused = true; |
629 | return "Undoing this new-game operation would change params"; | 741 | return "Undoing this new-game operation would change params"; |
@@ -648,7 +760,7 @@ static bool midend_undo(midend *me) | |||
648 | me->dir = -1; | 760 | me->dir = -1; |
649 | return true; | 761 | return true; |
650 | } else if (me->newgame_undo.len) { | 762 | } else if (me->newgame_undo.len) { |
651 | struct newgame_undo_deserialise_read_ctx rctx; | 763 | struct midend_serialise_buf_read_ctx rctx; |
652 | struct newgame_undo_deserialise_check_ctx cctx; | 764 | struct newgame_undo_deserialise_check_ctx cctx; |
653 | struct midend_serialise_buf serbuf; | 765 | struct midend_serialise_buf serbuf; |
654 | 766 | ||
@@ -659,14 +771,14 @@ static bool midend_undo(midend *me) | |||
659 | */ | 771 | */ |
660 | serbuf.buf = NULL; | 772 | serbuf.buf = NULL; |
661 | serbuf.len = serbuf.size = 0; | 773 | serbuf.len = serbuf.size = 0; |
662 | midend_serialise(me, newgame_serialise_write, &serbuf); | 774 | midend_serialise(me, midend_serialise_buf_write, &serbuf); |
663 | 775 | ||
664 | rctx.ser = &me->newgame_undo; | 776 | rctx.ser = &me->newgame_undo; |
665 | rctx.len = me->newgame_undo.len; /* copy for reentrancy safety */ | 777 | rctx.len = me->newgame_undo.len; /* copy for reentrancy safety */ |
666 | rctx.pos = 0; | 778 | rctx.pos = 0; |
667 | cctx.refused = false; | 779 | cctx.refused = false; |
668 | deserialise_error = midend_deserialise_internal( | 780 | deserialise_error = midend_deserialise_internal( |
669 | me, newgame_undo_deserialise_read, &rctx, | 781 | me, midend_serialise_buf_read, &rctx, |
670 | newgame_undo_deserialise_check, &cctx); | 782 | newgame_undo_deserialise_check, &cctx); |
671 | if (cctx.refused) { | 783 | if (cctx.refused) { |
672 | /* | 784 | /* |
@@ -699,7 +811,8 @@ static bool midend_undo(midend *me) | |||
699 | * the midend so that we can redo back into it later. | 811 | * the midend so that we can redo back into it later. |
700 | */ | 812 | */ |
701 | me->newgame_redo.len = 0; | 813 | me->newgame_redo.len = 0; |
702 | newgame_serialise_write(&me->newgame_redo, serbuf.buf, serbuf.len); | 814 | midend_serialise_buf_write(&me->newgame_redo, |
815 | serbuf.buf, serbuf.len); | ||
703 | 816 | ||
704 | sfree(serbuf.buf); | 817 | sfree(serbuf.buf); |
705 | return true; | 818 | return true; |
@@ -721,7 +834,7 @@ static bool midend_redo(midend *me) | |||
721 | me->dir = +1; | 834 | me->dir = +1; |
722 | return true; | 835 | return true; |
723 | } else if (me->newgame_redo.len) { | 836 | } else if (me->newgame_redo.len) { |
724 | struct newgame_undo_deserialise_read_ctx rctx; | 837 | struct midend_serialise_buf_read_ctx rctx; |
725 | struct newgame_undo_deserialise_check_ctx cctx; | 838 | struct newgame_undo_deserialise_check_ctx cctx; |
726 | struct midend_serialise_buf serbuf; | 839 | struct midend_serialise_buf serbuf; |
727 | 840 | ||
@@ -732,14 +845,14 @@ static bool midend_redo(midend *me) | |||
732 | */ | 845 | */ |
733 | serbuf.buf = NULL; | 846 | serbuf.buf = NULL; |
734 | serbuf.len = serbuf.size = 0; | 847 | serbuf.len = serbuf.size = 0; |
735 | midend_serialise(me, newgame_serialise_write, &serbuf); | 848 | midend_serialise(me, midend_serialise_buf_write, &serbuf); |
736 | 849 | ||
737 | rctx.ser = &me->newgame_redo; | 850 | rctx.ser = &me->newgame_redo; |
738 | rctx.len = me->newgame_redo.len; /* copy for reentrancy safety */ | 851 | rctx.len = me->newgame_redo.len; /* copy for reentrancy safety */ |
739 | rctx.pos = 0; | 852 | rctx.pos = 0; |
740 | cctx.refused = false; | 853 | cctx.refused = false; |
741 | deserialise_error = midend_deserialise_internal( | 854 | deserialise_error = midend_deserialise_internal( |
742 | me, newgame_undo_deserialise_read, &rctx, | 855 | me, midend_serialise_buf_read, &rctx, |
743 | newgame_undo_deserialise_check, &cctx); | 856 | newgame_undo_deserialise_check, &cctx); |
744 | if (cctx.refused) { | 857 | if (cctx.refused) { |
745 | /* | 858 | /* |
@@ -772,7 +885,8 @@ static bool midend_redo(midend *me) | |||
772 | * the midend so that we can undo back into it later. | 885 | * the midend so that we can undo back into it later. |
773 | */ | 886 | */ |
774 | me->newgame_undo.len = 0; | 887 | me->newgame_undo.len = 0; |
775 | newgame_serialise_write(&me->newgame_undo, serbuf.buf, serbuf.len); | 888 | midend_serialise_buf_write(&me->newgame_undo, |
889 | serbuf.buf, serbuf.len); | ||
776 | 890 | ||
777 | sfree(serbuf.buf); | 891 | sfree(serbuf.buf); |
778 | return true; | 892 | return true; |
@@ -859,12 +973,13 @@ void midend_restart_game(midend *me) | |||
859 | midend_set_timer(me); | 973 | midend_set_timer(me); |
860 | } | 974 | } |
861 | 975 | ||
862 | static bool midend_really_process_key(midend *me, int x, int y, int button) | 976 | static int midend_really_process_key(midend *me, int x, int y, int button) |
863 | { | 977 | { |
864 | game_state *oldstate = | 978 | game_state *oldstate = |
865 | me->ourgame->dup_game(me->states[me->statepos - 1].state); | 979 | me->ourgame->dup_game(me->states[me->statepos - 1].state); |
866 | int type = MOVE; | 980 | int type = MOVE; |
867 | bool gottype = false, ret = true; | 981 | bool gottype = false; |
982 | int ret = PKR_NO_EFFECT; | ||
868 | float anim_time; | 983 | float anim_time; |
869 | game_state *s; | 984 | game_state *s; |
870 | char *movestr = NULL; | 985 | char *movestr = NULL; |
@@ -875,40 +990,51 @@ static bool midend_really_process_key(midend *me, int x, int y, int button) | |||
875 | me->ui, me->drawstate, x, y, button); | 990 | me->ui, me->drawstate, x, y, button); |
876 | } | 991 | } |
877 | 992 | ||
878 | if (!movestr) { | 993 | if (movestr == NULL || movestr == MOVE_UNUSED) { |
879 | if (button == 'n' || button == 'N' || button == '\x0E' || | 994 | if ((me->one_key_shortcuts && (button == 'n' || button == 'N')) || |
880 | button == UI_NEWGAME) { | 995 | button == '\x0E' || button == UI_NEWGAME) { |
881 | midend_new_game(me); | 996 | midend_new_game(me); |
882 | midend_redraw(me); | 997 | midend_redraw(me); |
998 | ret = PKR_SOME_EFFECT; | ||
883 | goto done; /* never animate */ | 999 | goto done; /* never animate */ |
884 | } else if (button == 'u' || button == 'U' || | 1000 | } else if ((me->one_key_shortcuts && (button=='u' || button=='U')) || |
885 | button == '\x1A' || button == '\x1F' || | 1001 | button == '*' || button == '\x1A' || button == '\x1F' || |
886 | button == UI_UNDO) { | 1002 | button == UI_UNDO) { |
887 | midend_stop_anim(me); | 1003 | midend_stop_anim(me); |
888 | type = me->states[me->statepos-1].movetype; | 1004 | type = me->states[me->statepos-1].movetype; |
889 | gottype = true; | 1005 | gottype = true; |
890 | if (!midend_undo(me)) | 1006 | if (!midend_undo(me)) |
891 | goto done; | 1007 | goto done; |
892 | } else if (button == 'r' || button == 'R' || | 1008 | ret = PKR_SOME_EFFECT; |
893 | button == '\x12' || button == '\x19' || | 1009 | } else if ((me->one_key_shortcuts && (button=='r' || button=='R')) || |
1010 | button == '#' || button == '\x12' || button == '\x19' || | ||
894 | button == UI_REDO) { | 1011 | button == UI_REDO) { |
895 | midend_stop_anim(me); | 1012 | midend_stop_anim(me); |
896 | if (!midend_redo(me)) | 1013 | if (!midend_redo(me)) |
897 | goto done; | 1014 | goto done; |
1015 | ret = PKR_SOME_EFFECT; | ||
898 | } else if ((button == '\x13' || button == UI_SOLVE) && | 1016 | } else if ((button == '\x13' || button == UI_SOLVE) && |
899 | me->ourgame->can_solve) { | 1017 | me->ourgame->can_solve) { |
1018 | ret = PKR_SOME_EFFECT; | ||
900 | if (midend_solve(me)) | 1019 | if (midend_solve(me)) |
901 | goto done; | 1020 | goto done; |
902 | } else if (button == 'q' || button == 'Q' || button == '\x11' || | 1021 | } else if ((me->one_key_shortcuts && (button=='q' || button=='Q')) || |
903 | button == UI_QUIT) { | 1022 | button == '\x11' || button == UI_QUIT) { |
904 | ret = false; | 1023 | ret = PKR_QUIT; |
905 | goto done; | 1024 | goto done; |
906 | } else | 1025 | } else { |
1026 | ret = PKR_UNUSED; | ||
907 | goto done; | 1027 | goto done; |
1028 | } | ||
1029 | } else if (movestr == MOVE_NO_EFFECT) { | ||
1030 | ret = PKR_NO_EFFECT; | ||
1031 | goto done; | ||
908 | } else { | 1032 | } else { |
909 | if (movestr == UI_UPDATE) | 1033 | ret = PKR_SOME_EFFECT; |
1034 | if (movestr == MOVE_UI_UPDATE) | ||
910 | s = me->states[me->statepos-1].state; | 1035 | s = me->states[me->statepos-1].state; |
911 | else { | 1036 | else { |
1037 | assert_printable_ascii(movestr); | ||
912 | s = me->ourgame->execute_move(me->states[me->statepos-1].state, | 1038 | s = me->ourgame->execute_move(me->states[me->statepos-1].state, |
913 | movestr); | 1039 | movestr); |
914 | assert(s != NULL); | 1040 | assert(s != NULL); |
@@ -975,9 +1101,9 @@ static bool midend_really_process_key(midend *me, int x, int y, int button) | |||
975 | return ret; | 1101 | return ret; |
976 | } | 1102 | } |
977 | 1103 | ||
978 | bool midend_process_key(midend *me, int x, int y, int button) | 1104 | int midend_process_key(midend *me, int x, int y, int button) |
979 | { | 1105 | { |
980 | bool ret = true; | 1106 | int ret = PKR_UNUSED, ret2; |
981 | 1107 | ||
982 | /* | 1108 | /* |
983 | * Harmonise mouse drag and release messages. | 1109 | * Harmonise mouse drag and release messages. |
@@ -1052,6 +1178,10 @@ bool midend_process_key(midend *me, int x, int y, int button) | |||
1052 | * of '\n' etc for keyboard-based cursors. The choice of buttons | 1178 | * of '\n' etc for keyboard-based cursors. The choice of buttons |
1053 | * here could eventually be controlled by a runtime configuration | 1179 | * here could eventually be controlled by a runtime configuration |
1054 | * option. | 1180 | * option. |
1181 | * | ||
1182 | * We also handle converting MOD_CTRL|'a' etc into '\x01' etc, | ||
1183 | * specially recognising Ctrl+Shift+Z, and stripping modifier | ||
1184 | * flags off keys that aren't meant to have them. | ||
1055 | */ | 1185 | */ |
1056 | if (IS_MOUSE_DRAG(button) || IS_MOUSE_RELEASE(button)) { | 1186 | if (IS_MOUSE_DRAG(button) || IS_MOUSE_RELEASE(button)) { |
1057 | if (me->pressed_mouse_button) { | 1187 | if (me->pressed_mouse_button) { |
@@ -1076,11 +1206,34 @@ bool midend_process_key(midend *me, int x, int y, int button) | |||
1076 | /* | 1206 | /* |
1077 | * Fabricate a button-up for the previously pressed button. | 1207 | * Fabricate a button-up for the previously pressed button. |
1078 | */ | 1208 | */ |
1079 | ret = ret && midend_really_process_key | 1209 | ret2 = midend_really_process_key |
1080 | (me, x, y, (me->pressed_mouse_button + | 1210 | (me, x, y, (me->pressed_mouse_button + |
1081 | (LEFT_RELEASE - LEFT_BUTTON))); | 1211 | (LEFT_RELEASE - LEFT_BUTTON))); |
1082 | } | 1212 | ret = min(ret, ret2); |
1083 | 1213 | } | |
1214 | |||
1215 | /* Canonicalise CTRL+ASCII. */ | ||
1216 | if ((button & MOD_CTRL) && | ||
1217 | STRIP_BUTTON_MODIFIERS(button) >= 0x40 && | ||
1218 | STRIP_BUTTON_MODIFIERS(button) < 0x80) | ||
1219 | button = button & (0x1f | (MOD_MASK & ~MOD_CTRL)); | ||
1220 | /* Special handling to make CTRL+SHFT+Z into REDO. */ | ||
1221 | if ((button & (~MOD_MASK | MOD_SHFT)) == (MOD_SHFT | '\x1A')) | ||
1222 | button = UI_REDO; | ||
1223 | /* interpret_move() expects CTRL and SHFT only on cursor keys, and | ||
1224 | * TAB (added as of 7/2024 to support Untangle). */ | ||
1225 | if (!IS_CURSOR_MOVE(STRIP_BUTTON_MODIFIERS(button))) { | ||
1226 | /* reject CTRL+anything odd */ | ||
1227 | if ((button & MOD_CTRL) && STRIP_BUTTON_MODIFIERS(button) >= 0x20) | ||
1228 | return PKR_UNUSED; | ||
1229 | /* otherwise strip them, except for tab */ | ||
1230 | if (STRIP_BUTTON_MODIFIERS(button) != '\t') | ||
1231 | button &= ~(MOD_CTRL | MOD_SHFT); | ||
1232 | } | ||
1233 | /* interpret_move() expects NUM_KEYPAD only on numbers. */ | ||
1234 | if (STRIP_BUTTON_MODIFIERS(button) < '0' || | ||
1235 | STRIP_BUTTON_MODIFIERS(button) > '9') | ||
1236 | button &= ~MOD_NUM_KEYPAD; | ||
1084 | /* | 1237 | /* |
1085 | * Translate keyboard presses to cursor selection. | 1238 | * Translate keyboard presses to cursor selection. |
1086 | */ | 1239 | */ |
@@ -1101,7 +1254,8 @@ bool midend_process_key(midend *me, int x, int y, int button) | |||
1101 | /* | 1254 | /* |
1102 | * Now send on the event we originally received. | 1255 | * Now send on the event we originally received. |
1103 | */ | 1256 | */ |
1104 | ret = ret && midend_really_process_key(me, x, y, button); | 1257 | ret2 = midend_really_process_key(me, x, y, button); |
1258 | ret = min(ret, ret2); | ||
1105 | 1259 | ||
1106 | /* | 1260 | /* |
1107 | * And update the currently pressed button. | 1261 | * And update the currently pressed button. |
@@ -1121,7 +1275,7 @@ key_label *midend_request_keys(midend *me, int *n) | |||
1121 | 1275 | ||
1122 | if(me->ourgame->request_keys) | 1276 | if(me->ourgame->request_keys) |
1123 | { | 1277 | { |
1124 | keys = me->ourgame->request_keys(midend_get_params(me), &nkeys); | 1278 | keys = me->ourgame->request_keys(me->params, &nkeys); |
1125 | for(i = 0; i < nkeys; ++i) | 1279 | for(i = 0; i < nkeys; ++i) |
1126 | { | 1280 | { |
1127 | if(!keys[i].label) | 1281 | if(!keys[i].label) |
@@ -1135,12 +1289,38 @@ key_label *midend_request_keys(midend *me, int *n) | |||
1135 | return keys; | 1289 | return keys; |
1136 | } | 1290 | } |
1137 | 1291 | ||
1292 | /* Return a good label to show next to a key right now. */ | ||
1293 | const char *midend_current_key_label(midend *me, int button) | ||
1294 | { | ||
1295 | assert(IS_CURSOR_SELECT(button)); | ||
1296 | if (!me->ourgame->current_key_label) return ""; | ||
1297 | return me->ourgame->current_key_label( | ||
1298 | me->ui, me->states[me->statepos-1].state, button); | ||
1299 | } | ||
1300 | |||
1138 | void midend_redraw(midend *me) | 1301 | void midend_redraw(midend *me) |
1139 | { | 1302 | { |
1140 | assert(me->drawing); | 1303 | assert(me->drawing); |
1141 | 1304 | ||
1142 | if (me->statepos > 0 && me->drawstate) { | 1305 | if (me->statepos > 0 && me->drawstate) { |
1306 | bool first_draw = me->first_draw; | ||
1307 | me->first_draw = false; | ||
1308 | |||
1143 | start_draw(me->drawing); | 1309 | start_draw(me->drawing); |
1310 | |||
1311 | if (first_draw) { | ||
1312 | /* | ||
1313 | * The initial contents of the window are not guaranteed | ||
1314 | * by the front end. But we also don't want to require | ||
1315 | * every single game to go to the effort of clearing the | ||
1316 | * window on setup. So we centralise here the operation of | ||
1317 | * covering the whole window with colour 0 (assumed to be | ||
1318 | * the puzzle's background colour) the first time we do a | ||
1319 | * redraw operation with a new drawstate. | ||
1320 | */ | ||
1321 | draw_rect(me->drawing, 0, 0, me->winwidth, me->winheight, 0); | ||
1322 | } | ||
1323 | |||
1144 | if (me->oldstate && me->anim_time > 0 && | 1324 | if (me->oldstate && me->anim_time > 0 && |
1145 | me->anim_pos < me->anim_time) { | 1325 | me->anim_pos < me->anim_time) { |
1146 | assert(me->dir != 0); | 1326 | assert(me->dir != 0); |
@@ -1152,6 +1332,15 @@ void midend_redraw(midend *me) | |||
1152 | me->states[me->statepos-1].state, +1 /*shrug*/, | 1332 | me->states[me->statepos-1].state, +1 /*shrug*/, |
1153 | me->ui, 0.0, me->flash_pos); | 1333 | me->ui, 0.0, me->flash_pos); |
1154 | } | 1334 | } |
1335 | |||
1336 | if (first_draw) { | ||
1337 | /* | ||
1338 | * Call a big draw_update on the whole window, in case the | ||
1339 | * game backend didn't. | ||
1340 | */ | ||
1341 | draw_update(me->drawing, 0, 0, me->winwidth, me->winheight); | ||
1342 | } | ||
1343 | |||
1155 | end_draw(me->drawing); | 1344 | end_draw(me->drawing); |
1156 | } | 1345 | } |
1157 | } | 1346 | } |
@@ -1201,6 +1390,7 @@ float *midend_colours(midend *me, int *ncolours) | |||
1201 | float *ret; | 1390 | float *ret; |
1202 | 1391 | ||
1203 | ret = me->ourgame->colours(me->frontend, ncolours); | 1392 | ret = me->ourgame->colours(me->frontend, ncolours); |
1393 | assert(*ncolours >= 1); | ||
1204 | 1394 | ||
1205 | { | 1395 | { |
1206 | int i; | 1396 | int i; |
@@ -1227,6 +1417,9 @@ float *midend_colours(midend *me, int *ncolours) | |||
1227 | ret[i*3 + 1] = g / 255.0F; | 1417 | ret[i*3 + 1] = g / 255.0F; |
1228 | ret[i*3 + 2] = b / 255.0F; | 1418 | ret[i*3 + 2] = b / 255.0F; |
1229 | } | 1419 | } |
1420 | assert(0.0F <= ret[i*3 + 0] && ret[i*3 + 0] <= 1.0F); | ||
1421 | assert(0.0F <= ret[i*3 + 1] && ret[i*3 + 1] <= 1.0F); | ||
1422 | assert(0.0F <= ret[i*3 + 2] && ret[i*3 + 2] <= 1.0F); | ||
1230 | } | 1423 | } |
1231 | } | 1424 | } |
1232 | 1425 | ||
@@ -1359,7 +1552,7 @@ static void preset_menu_encode_params(midend *me, struct preset_menu *menu) | |||
1359 | for (i = 0; i < menu->n_entries; i++) { | 1552 | for (i = 0; i < menu->n_entries; i++) { |
1360 | if (menu->entries[i].params) { | 1553 | if (menu->entries[i].params) { |
1361 | me->encoded_presets[menu->entries[i].id] = | 1554 | me->encoded_presets[menu->entries[i].id] = |
1362 | me->ourgame->encode_params(menu->entries[i].params, true); | 1555 | encode_params(me, menu->entries[i].params, true); |
1363 | } else { | 1556 | } else { |
1364 | preset_menu_encode_params(me, menu->entries[i].submenu); | 1557 | preset_menu_encode_params(me, menu->entries[i].submenu); |
1365 | } | 1558 | } |
@@ -1438,7 +1631,7 @@ struct preset_menu *midend_get_presets(midend *me, int *id_limit) | |||
1438 | 1631 | ||
1439 | int midend_which_preset(midend *me) | 1632 | int midend_which_preset(midend *me) |
1440 | { | 1633 | { |
1441 | char *encoding = me->ourgame->encode_params(me->params, true); | 1634 | char *encoding = encode_params(me, me->params, true); |
1442 | int i, ret; | 1635 | int i, ret; |
1443 | 1636 | ||
1444 | ret = -1; | 1637 | ret = -1; |
@@ -1496,6 +1689,10 @@ bool midend_get_cursor_location(midend *me, | |||
1496 | void midend_supersede_game_desc(midend *me, const char *desc, | 1689 | void midend_supersede_game_desc(midend *me, const char *desc, |
1497 | const char *privdesc) | 1690 | const char *privdesc) |
1498 | { | 1691 | { |
1692 | /* Assert that the descriptions consists only of printable ASCII. */ | ||
1693 | assert_printable_ascii(desc); | ||
1694 | if (privdesc) | ||
1695 | assert_printable_ascii(privdesc); | ||
1499 | sfree(me->desc); | 1696 | sfree(me->desc); |
1500 | sfree(me->privdesc); | 1697 | sfree(me->privdesc); |
1501 | me->desc = dupstr(desc); | 1698 | me->desc = dupstr(desc); |
@@ -1545,7 +1742,7 @@ config_item *midend_get_config(midend *me, int which, char **wintitle) | |||
1545 | * the former is likely to persist across many code | 1742 | * the former is likely to persist across many code |
1546 | * changes). | 1743 | * changes). |
1547 | */ | 1744 | */ |
1548 | parstr = me->ourgame->encode_params(me->curparams, which == CFG_SEED); | 1745 | parstr = encode_params(me, me->curparams, which == CFG_SEED); |
1549 | assert(parstr); | 1746 | assert(parstr); |
1550 | if (which == CFG_DESC) { | 1747 | if (which == CFG_DESC) { |
1551 | rest = me->desc ? me->desc : ""; | 1748 | rest = me->desc ? me->desc : ""; |
@@ -1562,6 +1759,10 @@ config_item *midend_get_config(midend *me, int which, char **wintitle) | |||
1562 | ret[1].name = NULL; | 1759 | ret[1].name = NULL; |
1563 | 1760 | ||
1564 | return ret; | 1761 | return ret; |
1762 | case CFG_PREFS: | ||
1763 | sprintf(titlebuf, "%s preferences", me->ourgame->name); | ||
1764 | *wintitle = titlebuf; | ||
1765 | return midend_get_prefs(me, NULL); | ||
1565 | } | 1766 | } |
1566 | 1767 | ||
1567 | assert(!"We shouldn't be here"); | 1768 | assert(!"We shouldn't be here"); |
@@ -1670,6 +1871,7 @@ static const char *midend_game_id_int(midend *me, const char *id, int defmode) | |||
1670 | newcurparams = me->ourgame->default_params(); | 1871 | newcurparams = me->ourgame->default_params(); |
1671 | } | 1872 | } |
1672 | me->ourgame->decode_params(newcurparams, par); | 1873 | me->ourgame->decode_params(newcurparams, par); |
1874 | sfree(par); | ||
1673 | error = me->ourgame->validate_params(newcurparams, desc == NULL); | 1875 | error = me->ourgame->validate_params(newcurparams, desc == NULL); |
1674 | if (error) { | 1876 | if (error) { |
1675 | me->ourgame->free_params(newcurparams); | 1877 | me->ourgame->free_params(newcurparams); |
@@ -1689,7 +1891,7 @@ static const char *midend_game_id_int(midend *me, const char *id, int defmode) | |||
1689 | 1891 | ||
1690 | newparams = me->ourgame->dup_params(me->params); | 1892 | newparams = me->ourgame->dup_params(me->params); |
1691 | 1893 | ||
1692 | tmpstr = me->ourgame->encode_params(newcurparams, false); | 1894 | tmpstr = encode_params(me, newcurparams, false); |
1693 | me->ourgame->decode_params(newparams, tmpstr); | 1895 | me->ourgame->decode_params(newparams, tmpstr); |
1694 | 1896 | ||
1695 | sfree(tmpstr); | 1897 | sfree(tmpstr); |
@@ -1745,8 +1947,6 @@ static const char *midend_game_id_int(midend *me, const char *id, int defmode) | |||
1745 | me->genmode = GOT_SEED; | 1947 | me->genmode = GOT_SEED; |
1746 | } | 1948 | } |
1747 | 1949 | ||
1748 | sfree(par); | ||
1749 | |||
1750 | me->newgame_can_store_undo = false; | 1950 | me->newgame_can_store_undo = false; |
1751 | 1951 | ||
1752 | return NULL; | 1952 | return NULL; |
@@ -1761,7 +1961,7 @@ char *midend_get_game_id(midend *me) | |||
1761 | { | 1961 | { |
1762 | char *parstr, *ret; | 1962 | char *parstr, *ret; |
1763 | 1963 | ||
1764 | parstr = me->ourgame->encode_params(me->curparams, false); | 1964 | parstr = encode_params(me, me->curparams, false); |
1765 | assert(parstr); | 1965 | assert(parstr); |
1766 | assert(me->desc); | 1966 | assert(me->desc); |
1767 | ret = snewn(strlen(parstr) + strlen(me->desc) + 2, char); | 1967 | ret = snewn(strlen(parstr) + strlen(me->desc) + 2, char); |
@@ -1777,7 +1977,7 @@ char *midend_get_random_seed(midend *me) | |||
1777 | if (!me->seedstr) | 1977 | if (!me->seedstr) |
1778 | return NULL; | 1978 | return NULL; |
1779 | 1979 | ||
1780 | parstr = me->ourgame->encode_params(me->curparams, true); | 1980 | parstr = encode_params(me, me->curparams, true); |
1781 | assert(parstr); | 1981 | assert(parstr); |
1782 | ret = snewn(strlen(parstr) + strlen(me->seedstr) + 2, char); | 1982 | ret = snewn(strlen(parstr) + strlen(me->seedstr) + 2, char); |
1783 | sprintf(ret, "%s#%s", parstr, me->seedstr); | 1983 | sprintf(ret, "%s#%s", parstr, me->seedstr); |
@@ -1811,6 +2011,10 @@ const char *midend_set_config(midend *me, int which, config_item *cfg) | |||
1811 | if (error) | 2011 | if (error) |
1812 | return error; | 2012 | return error; |
1813 | break; | 2013 | break; |
2014 | |||
2015 | case CFG_PREFS: | ||
2016 | midend_set_prefs(me, me->ui, cfg); | ||
2017 | break; | ||
1814 | } | 2018 | } |
1815 | 2019 | ||
1816 | return NULL; | 2020 | return NULL; |
@@ -1849,12 +2053,13 @@ const char *midend_solve(midend *me) | |||
1849 | movestr = me->ourgame->solve(me->states[0].state, | 2053 | movestr = me->ourgame->solve(me->states[0].state, |
1850 | me->states[me->statepos-1].state, | 2054 | me->states[me->statepos-1].state, |
1851 | me->aux_info, &msg); | 2055 | me->aux_info, &msg); |
1852 | assert(movestr != UI_UPDATE); | 2056 | assert(movestr != MOVE_UI_UPDATE); |
1853 | if (!movestr) { | 2057 | if (!movestr) { |
1854 | if (!msg) | 2058 | if (!msg) |
1855 | msg = "Solve operation failed"; /* _shouldn't_ happen, but can */ | 2059 | msg = "Solve operation failed"; /* _shouldn't_ happen, but can */ |
1856 | return msg; | 2060 | return msg; |
1857 | } | 2061 | } |
2062 | assert_printable_ascii(movestr); | ||
1858 | s = me->ourgame->execute_move(me->states[me->statepos-1].state, movestr); | 2063 | s = me->ourgame->execute_move(me->states[me->statepos-1].state, movestr); |
1859 | assert(s); | 2064 | assert(s); |
1860 | 2065 | ||
@@ -1961,7 +2166,9 @@ void midend_serialise(midend *me, | |||
1961 | char lbuf[9]; \ | 2166 | char lbuf[9]; \ |
1962 | copy_left_justified(lbuf, sizeof(lbuf), h); \ | 2167 | copy_left_justified(lbuf, sizeof(lbuf), h); \ |
1963 | sprintf(hbuf, "%s:%d:", lbuf, (int)strlen(str)); \ | 2168 | sprintf(hbuf, "%s:%d:", lbuf, (int)strlen(str)); \ |
2169 | assert_printable_ascii(hbuf); \ | ||
1964 | write(wctx, hbuf, strlen(hbuf)); \ | 2170 | write(wctx, hbuf, strlen(hbuf)); \ |
2171 | assert_printable_ascii(str); \ | ||
1965 | write(wctx, str, strlen(str)); \ | 2172 | write(wctx, str, strlen(str)); \ |
1966 | write(wctx, "\n", 1); \ | 2173 | write(wctx, "\n", 1); \ |
1967 | } while (0) | 2174 | } while (0) |
@@ -1986,7 +2193,7 @@ void midend_serialise(midend *me, | |||
1986 | * The current long-term parameters structure, in full. | 2193 | * The current long-term parameters structure, in full. |
1987 | */ | 2194 | */ |
1988 | if (me->params) { | 2195 | if (me->params) { |
1989 | char *s = me->ourgame->encode_params(me->params, true); | 2196 | char *s = encode_params(me, me->params, true); |
1990 | wr("PARAMS", s); | 2197 | wr("PARAMS", s); |
1991 | sfree(s); | 2198 | sfree(s); |
1992 | } | 2199 | } |
@@ -1995,7 +2202,7 @@ void midend_serialise(midend *me, | |||
1995 | * The current short-term parameters structure, in full. | 2202 | * The current short-term parameters structure, in full. |
1996 | */ | 2203 | */ |
1997 | if (me->curparams) { | 2204 | if (me->curparams) { |
1998 | char *s = me->ourgame->encode_params(me->curparams, true); | 2205 | char *s = encode_params(me, me->curparams, true); |
1999 | wr("CPARAMS", s); | 2206 | wr("CPARAMS", s); |
2000 | sfree(s); | 2207 | sfree(s); |
2001 | } | 2208 | } |
@@ -2003,8 +2210,27 @@ void midend_serialise(midend *me, | |||
2003 | /* | 2210 | /* |
2004 | * The current game description, the privdesc, and the random seed. | 2211 | * The current game description, the privdesc, and the random seed. |
2005 | */ | 2212 | */ |
2006 | if (me->seedstr) | 2213 | if (me->seedstr) { |
2007 | wr("SEED", me->seedstr); | 2214 | /* |
2215 | * Random seeds are not necessarily printable ASCII. | ||
2216 | * Hex-encode the seed if necessary. Printable ASCII seeds | ||
2217 | * are emitted unencoded for compatibility with older | ||
2218 | * versions. | ||
2219 | */ | ||
2220 | int i; | ||
2221 | |||
2222 | for (i = 0; me->seedstr[i]; i++) | ||
2223 | if (me->seedstr[i] < 32 || me->seedstr[i] >= 127) | ||
2224 | break; | ||
2225 | if (me->seedstr[i]) { | ||
2226 | char *hexseed = bin2hex((unsigned char *)me->seedstr, | ||
2227 | strlen(me->seedstr)); | ||
2228 | |||
2229 | wr("HEXSEED", hexseed); | ||
2230 | sfree(hexseed); | ||
2231 | } else | ||
2232 | wr("SEED", me->seedstr); | ||
2233 | } | ||
2008 | if (me->desc) | 2234 | if (me->desc) |
2009 | wr("DESC", me->desc); | 2235 | wr("DESC", me->desc); |
2010 | if (me->privdesc) | 2236 | if (me->privdesc) |
@@ -2036,7 +2262,7 @@ void midend_serialise(midend *me, | |||
2036 | /* | 2262 | /* |
2037 | * Any required serialisation of the game_ui. | 2263 | * Any required serialisation of the game_ui. |
2038 | */ | 2264 | */ |
2039 | if (me->ui) { | 2265 | if (me->ui && me->ourgame->encode_ui) { |
2040 | char *s = me->ourgame->encode_ui(me->ui); | 2266 | char *s = me->ourgame->encode_ui(me->ui); |
2041 | if (s) { | 2267 | if (s) { |
2042 | wr("UI", s); | 2268 | wr("UI", s); |
@@ -2049,7 +2275,7 @@ void midend_serialise(midend *me, | |||
2049 | */ | 2275 | */ |
2050 | if (me->ourgame->is_timed) { | 2276 | if (me->ourgame->is_timed) { |
2051 | char buf[80]; | 2277 | char buf[80]; |
2052 | ftoa(buf, me->elapsed); | 2278 | sprintf(buf, "%g", me->elapsed); |
2053 | wr("TIME", buf); | 2279 | wr("TIME", buf); |
2054 | } | 2280 | } |
2055 | 2281 | ||
@@ -2060,6 +2286,7 @@ void midend_serialise(midend *me, | |||
2060 | char buf[80]; | 2286 | char buf[80]; |
2061 | sprintf(buf, "%d", me->nstates); | 2287 | sprintf(buf, "%d", me->nstates); |
2062 | wr("NSTATES", buf); | 2288 | wr("NSTATES", buf); |
2289 | assert(me->statepos >= 1 && me->statepos <= me->nstates); | ||
2063 | sprintf(buf, "%d", me->statepos); | 2290 | sprintf(buf, "%d", me->statepos); |
2064 | wr("STATEPOS", buf); | 2291 | wr("STATEPOS", buf); |
2065 | } | 2292 | } |
@@ -2159,7 +2386,7 @@ static const char *midend_deserialise_internal( | |||
2159 | 2386 | ||
2160 | if (c == ':') { | 2387 | if (c == ':') { |
2161 | break; | 2388 | break; |
2162 | } else if (c >= '0' && c <= '9') { | 2389 | } else if (c >= '0' && c <= '9' && len < (INT_MAX - 10) / 10) { |
2163 | len = (len * 10) + (c - '0'); | 2390 | len = (len * 10) + (c - '0'); |
2164 | } else { | 2391 | } else { |
2165 | if (started) | 2392 | if (started) |
@@ -2171,10 +2398,17 @@ static const char *midend_deserialise_internal( | |||
2171 | 2398 | ||
2172 | val = snewn(len+1, char); | 2399 | val = snewn(len+1, char); |
2173 | if (!read(rctx, val, len)) { | 2400 | if (!read(rctx, val, len)) { |
2174 | if (started) | 2401 | /* unexpected EOF */ |
2175 | goto cleanup; | 2402 | goto cleanup; |
2176 | } | 2403 | } |
2177 | val[len] = '\0'; | 2404 | val[len] = '\0'; |
2405 | /* Validate that all values (apart from SEED) are printable ASCII. */ | ||
2406 | if (strcmp(key, "SEED")) | ||
2407 | for (i = 0; val[i]; i++) | ||
2408 | if (val[i] < 32 || val[i] >= 127) { | ||
2409 | ret = "Forbidden characters in saved game file"; | ||
2410 | goto cleanup; | ||
2411 | } | ||
2178 | 2412 | ||
2179 | if (!started) { | 2413 | if (!started) { |
2180 | if (strcmp(key, "SAVEFILE") || strcmp(val, SERIALISE_MAGIC)) { | 2414 | if (strcmp(key, "SAVEFILE") || strcmp(val, SERIALISE_MAGIC)) { |
@@ -2204,6 +2438,15 @@ static const char *midend_deserialise_internal( | |||
2204 | sfree(data.cparstr); | 2438 | sfree(data.cparstr); |
2205 | data.cparstr = val; | 2439 | data.cparstr = val; |
2206 | val = NULL; | 2440 | val = NULL; |
2441 | } else if (!strcmp(key, "HEXSEED")) { | ||
2442 | unsigned char *tmp; | ||
2443 | int len = strlen(val) / 2; /* length in bytes */ | ||
2444 | tmp = hex2bin(val, len); | ||
2445 | sfree(data.seed); | ||
2446 | data.seed = snewn(len + 1, char); | ||
2447 | memcpy(data.seed, tmp, len); | ||
2448 | data.seed[len] = '\0'; | ||
2449 | sfree(tmp); | ||
2207 | } else if (!strcmp(key, "SEED")) { | 2450 | } else if (!strcmp(key, "SEED")) { |
2208 | sfree(data.seed); | 2451 | sfree(data.seed); |
2209 | data.seed = val; | 2452 | data.seed = val; |
@@ -2234,15 +2477,15 @@ static const char *midend_deserialise_internal( | |||
2234 | } else if (!strcmp(key, "TIME")) { | 2477 | } else if (!strcmp(key, "TIME")) { |
2235 | data.elapsed = (float)atof(val); | 2478 | data.elapsed = (float)atof(val); |
2236 | } else if (!strcmp(key, "NSTATES")) { | 2479 | } else if (!strcmp(key, "NSTATES")) { |
2480 | if (data.states) { | ||
2481 | ret = "Two state counts provided in save file"; | ||
2482 | goto cleanup; | ||
2483 | } | ||
2237 | data.nstates = atoi(val); | 2484 | data.nstates = atoi(val); |
2238 | if (data.nstates <= 0) { | 2485 | if (data.nstates <= 0) { |
2239 | ret = "Number of states in save file was negative"; | 2486 | ret = "Number of states in save file was negative"; |
2240 | goto cleanup; | 2487 | goto cleanup; |
2241 | } | 2488 | } |
2242 | if (data.states) { | ||
2243 | ret = "Two state counts provided in save file"; | ||
2244 | goto cleanup; | ||
2245 | } | ||
2246 | data.states = snewn(data.nstates, struct midend_state_entry); | 2489 | data.states = snewn(data.nstates, struct midend_state_entry); |
2247 | for (i = 0; i < data.nstates; i++) { | 2490 | for (i = 0; i < data.nstates; i++) { |
2248 | data.states[i].state = NULL; | 2491 | data.states[i].state = NULL; |
@@ -2251,19 +2494,25 @@ static const char *midend_deserialise_internal( | |||
2251 | } | 2494 | } |
2252 | } else if (!strcmp(key, "STATEPOS")) { | 2495 | } else if (!strcmp(key, "STATEPOS")) { |
2253 | data.statepos = atoi(val); | 2496 | data.statepos = atoi(val); |
2254 | } else if (!strcmp(key, "MOVE")) { | 2497 | } else if (!strcmp(key, "MOVE") || |
2255 | gotstates++; | 2498 | !strcmp(key, "SOLVE") || |
2256 | data.states[gotstates].movetype = MOVE; | 2499 | !strcmp(key, "RESTART")) { |
2257 | data.states[gotstates].movestr = val; | 2500 | if (!data.states) { |
2258 | val = NULL; | 2501 | ret = "No state count provided in save file"; |
2259 | } else if (!strcmp(key, "SOLVE")) { | 2502 | goto cleanup; |
2260 | gotstates++; | 2503 | } |
2261 | data.states[gotstates].movetype = SOLVE; | 2504 | if (data.statepos < 0) { |
2262 | data.states[gotstates].movestr = val; | 2505 | ret = "No game position provided in save file"; |
2263 | val = NULL; | 2506 | goto cleanup; |
2264 | } else if (!strcmp(key, "RESTART")) { | 2507 | } |
2265 | gotstates++; | 2508 | gotstates++; |
2266 | data.states[gotstates].movetype = RESTART; | 2509 | assert(gotstates < data.nstates); |
2510 | if (!strcmp(key, "MOVE")) | ||
2511 | data.states[gotstates].movetype = MOVE; | ||
2512 | else if (!strcmp(key, "SOLVE")) | ||
2513 | data.states[gotstates].movetype = SOLVE; | ||
2514 | else | ||
2515 | data.states[gotstates].movetype = RESTART; | ||
2267 | data.states[gotstates].movestr = val; | 2516 | data.states[gotstates].movestr = val; |
2268 | val = NULL; | 2517 | val = NULL; |
2269 | } | 2518 | } |
@@ -2274,12 +2523,20 @@ static const char *midend_deserialise_internal( | |||
2274 | } | 2523 | } |
2275 | 2524 | ||
2276 | data.params = me->ourgame->default_params(); | 2525 | data.params = me->ourgame->default_params(); |
2526 | if (!data.parstr) { | ||
2527 | ret = "Long-term parameters in save file are missing"; | ||
2528 | goto cleanup; | ||
2529 | } | ||
2277 | me->ourgame->decode_params(data.params, data.parstr); | 2530 | me->ourgame->decode_params(data.params, data.parstr); |
2278 | if (me->ourgame->validate_params(data.params, true)) { | 2531 | if (me->ourgame->validate_params(data.params, true)) { |
2279 | ret = "Long-term parameters in save file are invalid"; | 2532 | ret = "Long-term parameters in save file are invalid"; |
2280 | goto cleanup; | 2533 | goto cleanup; |
2281 | } | 2534 | } |
2282 | data.cparams = me->ourgame->default_params(); | 2535 | data.cparams = me->ourgame->default_params(); |
2536 | if (!data.cparstr) { | ||
2537 | ret = "Short-term parameters in save file are missing"; | ||
2538 | goto cleanup; | ||
2539 | } | ||
2283 | me->ourgame->decode_params(data.cparams, data.cparstr); | 2540 | me->ourgame->decode_params(data.cparams, data.cparstr); |
2284 | if (me->ourgame->validate_params(data.cparams, false)) { | 2541 | if (me->ourgame->validate_params(data.cparams, false)) { |
2285 | ret = "Short-term parameters in save file are invalid"; | 2542 | ret = "Short-term parameters in save file are invalid"; |
@@ -2305,12 +2562,18 @@ static const char *midend_deserialise_internal( | |||
2305 | ret = "Game private description in save file is invalid"; | 2562 | ret = "Game private description in save file is invalid"; |
2306 | goto cleanup; | 2563 | goto cleanup; |
2307 | } | 2564 | } |
2308 | if (data.statepos < 0 || data.statepos >= data.nstates) { | 2565 | if (data.statepos < 1 || data.statepos > data.nstates) { |
2309 | ret = "Game position in save file is out of range"; | 2566 | ret = "Game position in save file is out of range"; |
2567 | goto cleanup; | ||
2310 | } | 2568 | } |
2311 | 2569 | ||
2570 | if (!data.states) { | ||
2571 | ret = "No state count provided in save file"; | ||
2572 | goto cleanup; | ||
2573 | } | ||
2312 | data.states[0].state = me->ourgame->new_game( | 2574 | data.states[0].state = me->ourgame->new_game( |
2313 | me, data.cparams, data.privdesc ? data.privdesc : data.desc); | 2575 | me, data.cparams, data.privdesc ? data.privdesc : data.desc); |
2576 | |||
2314 | for (i = 1; i < data.nstates; i++) { | 2577 | for (i = 1; i < data.nstates; i++) { |
2315 | assert(data.states[i].movetype != NEWGAME); | 2578 | assert(data.states[i].movetype != NEWGAME); |
2316 | switch (data.states[i].movetype) { | 2579 | switch (data.states[i].movetype) { |
@@ -2336,7 +2599,10 @@ static const char *midend_deserialise_internal( | |||
2336 | } | 2599 | } |
2337 | 2600 | ||
2338 | data.ui = me->ourgame->new_ui(data.states[0].state); | 2601 | data.ui = me->ourgame->new_ui(data.states[0].state); |
2339 | me->ourgame->decode_ui(data.ui, data.uistr); | 2602 | midend_apply_prefs(me, data.ui); |
2603 | if (data.uistr && me->ourgame->decode_ui) | ||
2604 | me->ourgame->decode_ui(data.ui, data.uistr, | ||
2605 | data.states[data.statepos-1].state); | ||
2340 | 2606 | ||
2341 | /* | 2607 | /* |
2342 | * Run the externally provided check function, and abort if it | 2608 | * Run the externally provided check function, and abort if it |
@@ -2429,6 +2695,7 @@ static const char *midend_deserialise_internal( | |||
2429 | me->drawstate = | 2695 | me->drawstate = |
2430 | me->ourgame->new_drawstate(me->drawing, | 2696 | me->ourgame->new_drawstate(me->drawing, |
2431 | me->states[me->statepos-1].state); | 2697 | me->states[me->statepos-1].state); |
2698 | me->first_draw = true; | ||
2432 | midend_size_new_drawstate(me); | 2699 | midend_size_new_drawstate(me); |
2433 | if (me->game_id_change_notify_function) | 2700 | if (me->game_id_change_notify_function) |
2434 | me->game_id_change_notify_function(me->game_id_change_notify_ctx); | 2701 | me->game_id_change_notify_function(me->game_id_change_notify_ctx); |
@@ -2528,7 +2795,7 @@ const char *identify_game(char **name, | |||
2528 | 2795 | ||
2529 | if (c == ':') { | 2796 | if (c == ':') { |
2530 | break; | 2797 | break; |
2531 | } else if (c >= '0' && c <= '9') { | 2798 | } else if (c >= '0' && c <= '9' && len < (INT_MAX - 10) / 10) { |
2532 | len = (len * 10) + (c - '0'); | 2799 | len = (len * 10) + (c - '0'); |
2533 | } else { | 2800 | } else { |
2534 | if (started) | 2801 | if (started) |
@@ -2540,7 +2807,7 @@ const char *identify_game(char **name, | |||
2540 | 2807 | ||
2541 | val = snewn(len+1, char); | 2808 | val = snewn(len+1, char); |
2542 | if (!read(rctx, val, len)) { | 2809 | if (!read(rctx, val, len)) { |
2543 | if (started) | 2810 | /* unexpected EOF */ |
2544 | goto cleanup; | 2811 | goto cleanup; |
2545 | } | 2812 | } |
2546 | val[len] = '\0'; | 2813 | val[len] = '\0'; |
@@ -2605,14 +2872,340 @@ const char *midend_print_puzzle(midend *me, document *doc, bool with_soln) | |||
2605 | soln = NULL; | 2872 | soln = NULL; |
2606 | 2873 | ||
2607 | /* | 2874 | /* |
2608 | * This call passes over ownership of the two game_states and | 2875 | * This call passes over ownership of the two game_states, the |
2609 | * the game_params. Hence we duplicate the ones we want to | 2876 | * game_params and the game_ui. Hence we duplicate the ones we |
2610 | * keep, and we don't have to bother freeing soln if it was | 2877 | * want to keep, and we don't have to bother freeing soln if it |
2611 | * non-NULL. | 2878 | * was non-NULL. |
2612 | */ | 2879 | */ |
2880 | game_ui *ui = me->ourgame->new_ui(me->states[0].state); | ||
2881 | midend_apply_prefs(me, ui); | ||
2613 | document_add_puzzle(doc, me->ourgame, | 2882 | document_add_puzzle(doc, me->ourgame, |
2614 | me->ourgame->dup_params(me->curparams), | 2883 | me->ourgame->dup_params(me->curparams), ui, |
2615 | me->ourgame->dup_game(me->states[0].state), soln); | 2884 | me->ourgame->dup_game(me->states[0].state), soln); |
2616 | 2885 | ||
2617 | return NULL; | 2886 | return NULL; |
2618 | } | 2887 | } |
2888 | |||
2889 | static void midend_apply_prefs(midend *me, game_ui *ui) | ||
2890 | { | ||
2891 | struct midend_serialise_buf_read_ctx rctx[1]; | ||
2892 | rctx->ser = &me->be_prefs; | ||
2893 | rctx->len = me->be_prefs.len; | ||
2894 | rctx->pos = 0; | ||
2895 | const char *err = midend_deserialise_prefs( | ||
2896 | me, ui, midend_serialise_buf_read, rctx); | ||
2897 | /* This should have come from our own serialise function, so | ||
2898 | * it should never be invalid. */ | ||
2899 | assert(!err && "Bad internal serialisation of preferences"); | ||
2900 | } | ||
2901 | |||
2902 | static config_item *midend_get_prefs(midend *me, game_ui *ui) | ||
2903 | { | ||
2904 | int n_be_prefs, n_me_prefs, pos, i; | ||
2905 | config_item *all_prefs, *be_prefs; | ||
2906 | |||
2907 | be_prefs = NULL; | ||
2908 | n_be_prefs = 0; | ||
2909 | if (me->ourgame->get_prefs) { | ||
2910 | if (ui) { | ||
2911 | be_prefs = me->ourgame->get_prefs(ui); | ||
2912 | } else if (me->ui) { | ||
2913 | be_prefs = me->ourgame->get_prefs(me->ui); | ||
2914 | } else { | ||
2915 | game_ui *tmp_ui = me->ourgame->new_ui(NULL); | ||
2916 | be_prefs = me->ourgame->get_prefs(tmp_ui); | ||
2917 | me->ourgame->free_ui(tmp_ui); | ||
2918 | } | ||
2919 | while (be_prefs[n_be_prefs].type != C_END) | ||
2920 | n_be_prefs++; | ||
2921 | } | ||
2922 | |||
2923 | n_me_prefs = 1; | ||
2924 | all_prefs = snewn(n_me_prefs + n_be_prefs + 1, config_item); | ||
2925 | |||
2926 | pos = 0; | ||
2927 | |||
2928 | assert(pos < n_me_prefs); | ||
2929 | all_prefs[pos].name = "Keyboard shortcuts without Ctrl"; | ||
2930 | all_prefs[pos].kw = "one-key-shortcuts"; | ||
2931 | all_prefs[pos].type = C_BOOLEAN; | ||
2932 | all_prefs[pos].u.boolean.bval = me->one_key_shortcuts; | ||
2933 | pos++; | ||
2934 | |||
2935 | for (i = 0; i < n_be_prefs; i++) { | ||
2936 | all_prefs[pos] = be_prefs[i]; /* structure copy */ | ||
2937 | pos++; | ||
2938 | } | ||
2939 | |||
2940 | all_prefs[pos].name = NULL; | ||
2941 | all_prefs[pos].type = C_END; | ||
2942 | |||
2943 | if (be_prefs) | ||
2944 | /* We already copied each element, so don't free those with | ||
2945 | free_cfg(). */ | ||
2946 | sfree(be_prefs); | ||
2947 | |||
2948 | return all_prefs; | ||
2949 | } | ||
2950 | |||
2951 | static void midend_set_prefs(midend *me, game_ui *ui, config_item *all_prefs) | ||
2952 | { | ||
2953 | int pos = 0; | ||
2954 | game_ui *tmpui = NULL; | ||
2955 | |||
2956 | me->one_key_shortcuts = all_prefs[pos].u.boolean.bval; | ||
2957 | pos++; | ||
2958 | |||
2959 | if (me->ourgame->get_prefs) { | ||
2960 | if (!ui) | ||
2961 | ui = tmpui = me->ourgame->new_ui(NULL); | ||
2962 | me->ourgame->set_prefs(ui, all_prefs + pos); | ||
2963 | } | ||
2964 | |||
2965 | me->be_prefs.len = 0; | ||
2966 | midend_serialise_prefs(me, ui, midend_serialise_buf_write, &me->be_prefs); | ||
2967 | |||
2968 | if (tmpui) | ||
2969 | me->ourgame->free_ui(tmpui); | ||
2970 | } | ||
2971 | |||
2972 | static void midend_serialise_prefs( | ||
2973 | midend *me, game_ui *ui, | ||
2974 | void (*write)(void *ctx, const void *buf, int len), void *wctx) | ||
2975 | { | ||
2976 | config_item *cfg; | ||
2977 | int i; | ||
2978 | |||
2979 | cfg = midend_get_prefs(me, ui); | ||
2980 | |||
2981 | assert(cfg); | ||
2982 | |||
2983 | for (i = 0; cfg[i].type != C_END; i++) { | ||
2984 | config_item *it = &cfg[i]; | ||
2985 | |||
2986 | /* Expect keywords to be made up only of simple characters */ | ||
2987 | assert(it->kw[strspn(it->kw, "abcdefghijklmnopqrstuvwxyz-")] == '\0'); | ||
2988 | |||
2989 | write(wctx, it->kw, strlen(it->kw)); | ||
2990 | write(wctx, "=", 1); | ||
2991 | |||
2992 | switch (it->type) { | ||
2993 | case C_BOOLEAN: | ||
2994 | if (it->u.boolean.bval) | ||
2995 | write(wctx, "true", 4); | ||
2996 | else | ||
2997 | write(wctx, "false", 5); | ||
2998 | break; | ||
2999 | case C_STRING: { | ||
3000 | const char *p = it->u.string.sval; | ||
3001 | while (*p) { | ||
3002 | char c = *p++; | ||
3003 | write(wctx, &c, 1); | ||
3004 | if (c == '\n') | ||
3005 | write(wctx, " ", 1); | ||
3006 | } | ||
3007 | break; | ||
3008 | } | ||
3009 | case C_CHOICES: { | ||
3010 | int n = it->u.choices.selected; | ||
3011 | const char *p = it->u.choices.choicekws; | ||
3012 | char sepstr[2]; | ||
3013 | |||
3014 | sepstr[0] = *p++; | ||
3015 | sepstr[1] = '\0'; | ||
3016 | |||
3017 | while (n > 0) { | ||
3018 | const char *q = strchr(p, sepstr[0]); | ||
3019 | assert(q != NULL && "Value out of range in C_CHOICES"); | ||
3020 | p = q+1; | ||
3021 | n--; | ||
3022 | } | ||
3023 | |||
3024 | write(wctx, p, strcspn(p, sepstr)); | ||
3025 | break; | ||
3026 | } | ||
3027 | } | ||
3028 | |||
3029 | write(wctx, "\n", 1); | ||
3030 | } | ||
3031 | |||
3032 | free_cfg(cfg); | ||
3033 | } | ||
3034 | |||
3035 | struct buffer { | ||
3036 | char *data; | ||
3037 | size_t len, size; | ||
3038 | }; | ||
3039 | |||
3040 | static void buffer_append(struct buffer *buf, char c) | ||
3041 | { | ||
3042 | if (buf->len + 2 > buf->size) { | ||
3043 | size_t new_size = buf->size + buf->size / 4 + 128; | ||
3044 | assert(new_size > buf->size); | ||
3045 | buf->data = sresize(buf->data, new_size, char); | ||
3046 | buf->size = new_size; | ||
3047 | assert(buf->len < buf->size); | ||
3048 | } | ||
3049 | buf->data[buf->len++] = c; | ||
3050 | assert(buf->len < buf->size); | ||
3051 | buf->data[buf->len] = '\0'; | ||
3052 | } | ||
3053 | |||
3054 | static const char *midend_deserialise_prefs( | ||
3055 | midend *me, game_ui *ui, | ||
3056 | bool (*read)(void *ctx, void *buf, int len), void *rctx) | ||
3057 | { | ||
3058 | config_item *cfg, *it; | ||
3059 | int i; | ||
3060 | struct buffer buf[1] = {{ NULL, 0, 0 }}; | ||
3061 | const char *errmsg = NULL; | ||
3062 | char read_char; | ||
3063 | char ungot_char = '\0'; | ||
3064 | bool have_ungot_a_char = false, eof = false; | ||
3065 | |||
3066 | cfg = midend_get_prefs(me, ui); | ||
3067 | |||
3068 | while (!eof) { | ||
3069 | if (have_ungot_a_char) { | ||
3070 | read_char = ungot_char; | ||
3071 | have_ungot_a_char = false; | ||
3072 | } else { | ||
3073 | if (!read(rctx, &read_char, 1)) | ||
3074 | goto out; /* EOF at line start == success */ | ||
3075 | } | ||
3076 | |||
3077 | if (read_char == '#' || read_char == '\n') { | ||
3078 | /* Skip comment or blank line */ | ||
3079 | while (read_char != '\n') { | ||
3080 | if (!read(rctx, &read_char, 1)) | ||
3081 | goto out; /* EOF during boring line == success */ | ||
3082 | } | ||
3083 | continue; | ||
3084 | } | ||
3085 | |||
3086 | buf->len = 0; | ||
3087 | while (true) { | ||
3088 | buffer_append(buf, read_char); | ||
3089 | if (!read(rctx, &read_char, 1)) { | ||
3090 | errmsg = "Partial line at end of preferences file"; | ||
3091 | goto out; | ||
3092 | } | ||
3093 | if (read_char == '\n') { | ||
3094 | errmsg = "Expected '=' after keyword"; | ||
3095 | goto out; | ||
3096 | } | ||
3097 | if (read_char == '=') | ||
3098 | break; | ||
3099 | } | ||
3100 | |||
3101 | it = NULL; | ||
3102 | for (i = 0; cfg[i].type != C_END; i++) | ||
3103 | if (!strcmp(buf->data, cfg[i].kw)) | ||
3104 | it = &cfg[i]; | ||
3105 | |||
3106 | buf->len = 0; | ||
3107 | while (true) { | ||
3108 | if (!read(rctx, &read_char, 1)) { | ||
3109 | /* We tolerate missing \n at the end of the file, so | ||
3110 | * this is taken to mean we've got a complete config | ||
3111 | * directive. But set the eof flag so that we stop | ||
3112 | * after processing it. */ | ||
3113 | eof = true; | ||
3114 | break; | ||
3115 | } else if (read_char == '\n') { | ||
3116 | /* Newline _might_ be the end of this config | ||
3117 | * directive, unless it's followed by a space, in | ||
3118 | * which case it's a space-stuffed line | ||
3119 | * continuation. */ | ||
3120 | if (read(rctx, &read_char, 1)) { | ||
3121 | if (read_char == ' ') { | ||
3122 | buffer_append(buf, '\n'); | ||
3123 | continue; | ||
3124 | } else { | ||
3125 | /* But if the next character wasn't a space, | ||
3126 | * then we must unget it so that it'll be | ||
3127 | * available to the next iteration of our | ||
3128 | * outer loop as the first character of the | ||
3129 | * next keyword. */ | ||
3130 | ungot_char = read_char; | ||
3131 | have_ungot_a_char = true; | ||
3132 | break; | ||
3133 | } | ||
3134 | } else { | ||
3135 | /* And if the newline was followed by EOF, then we | ||
3136 | * should finish this iteration of the outer | ||
3137 | * loop normally, and then not go round again. */ | ||
3138 | eof = true; | ||
3139 | break; | ||
3140 | } | ||
3141 | } else { | ||
3142 | /* Any other character is just added to the buffer. */ | ||
3143 | buffer_append(buf, read_char); | ||
3144 | } | ||
3145 | } | ||
3146 | |||
3147 | if (!it) { | ||
3148 | /* | ||
3149 | * Tolerate unknown keywords in a preferences file, on the | ||
3150 | * assumption that they're from a different (probably | ||
3151 | * later) version of the game. | ||
3152 | */ | ||
3153 | continue; | ||
3154 | } | ||
3155 | |||
3156 | switch (it->type) { | ||
3157 | case C_BOOLEAN: | ||
3158 | if (!strcmp(buf->data, "true")) | ||
3159 | it->u.boolean.bval = true; | ||
3160 | else if (!strcmp(buf->data, "false")) | ||
3161 | it->u.boolean.bval = false; | ||
3162 | else { | ||
3163 | errmsg = "Value for boolean was not 'true' or 'false'"; | ||
3164 | goto out; | ||
3165 | } | ||
3166 | break; | ||
3167 | case C_STRING: | ||
3168 | sfree(it->u.string.sval); | ||
3169 | it->u.string.sval = buf->data; | ||
3170 | buf->data = NULL; | ||
3171 | buf->len = buf->size = 0; | ||
3172 | break; | ||
3173 | case C_CHOICES: { | ||
3174 | int n = 0; | ||
3175 | bool found = false; | ||
3176 | const char *p = it->u.choices.choicekws; | ||
3177 | char sepstr[2]; | ||
3178 | |||
3179 | sepstr[0] = *p; | ||
3180 | sepstr[1] = '\0'; | ||
3181 | |||
3182 | while (*p++) { | ||
3183 | int len = strcspn(p, sepstr); | ||
3184 | if (buf->len == len && !memcmp(p, buf->data, len)) { | ||
3185 | it->u.choices.selected = n; | ||
3186 | found = true; | ||
3187 | break; | ||
3188 | } | ||
3189 | p += len; | ||
3190 | n++; | ||
3191 | } | ||
3192 | |||
3193 | if (!found) { | ||
3194 | errmsg = "Invalid value for enumeration"; | ||
3195 | goto out; | ||
3196 | } | ||
3197 | |||
3198 | break; | ||
3199 | } | ||
3200 | } | ||
3201 | } | ||
3202 | |||
3203 | out: | ||
3204 | |||
3205 | if (!errmsg) | ||
3206 | midend_set_prefs(me, ui, cfg); | ||
3207 | |||
3208 | free_cfg(cfg); | ||
3209 | sfree(buf->data); | ||
3210 | return errmsg; | ||
3211 | } | ||