summaryrefslogtreecommitdiff
path: root/apps/plugins/puzzles/src/midend.c
diff options
context:
space:
mode:
Diffstat (limited to 'apps/plugins/puzzles/src/midend.c')
-rw-r--r--apps/plugins/puzzles/src/midend.c797
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
33struct midend_serialise_buf_read_ctx {
34 struct midend_serialise_buf *ser;
35 int len, pos;
36};
37
33struct midend { 38struct 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 */
124static const char *midend_deserialise_internal( 135static 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);
139static void midend_serialise_prefs(
140 midend *me, game_ui *ui,
141 void (*write)(void *ctx, const void *buf, int len), void *wctx);
142static const char *midend_deserialise_prefs(
143 midend *me, game_ui *ui,
144 bool (*read)(void *ctx, void *buf, int len), void *rctx);
145static config_item *midend_get_prefs(midend *me, game_ui *ui);
146static void midend_set_prefs(midend *me, game_ui *ui, config_item *all_prefs);
147static void midend_apply_prefs(midend *me, game_ui *ui);
128 148
129void midend_reset_tilesize(midend *me) 149void 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
307void 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 */
344static 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
378void 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
460static 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
473static 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
383static void midend_set_timer(midend *me) 482static 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
404static void newgame_serialise_write(void *ctx, const void *buf, int len) 504static 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
519static 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
419void midend_new_game(midend *me) 531void 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
555bool midend_can_undo(midend *me) 670const 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
560bool midend_can_redo(midend *me) 677void 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
565struct newgame_undo_deserialise_read_ctx { 684bool midend_can_undo(midend *me)
566 struct midend_serialise_buf *ser;
567 int len, pos;
568};
569
570static 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); 689bool 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
582struct newgame_undo_deserialise_check_ctx { 694struct 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
862static bool midend_really_process_key(midend *me, int x, int y, int button) 976static 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
978bool midend_process_key(midend *me, int x, int y, int button) 1104int 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. */
1293const 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
1138void midend_redraw(midend *me) 1301void 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
1439int midend_which_preset(midend *me) 1632int 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,
1496void midend_supersede_game_desc(midend *me, const char *desc, 1689void 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
2889static 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
2902static 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
2951static 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
2972static 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
3035struct buffer {
3036 char *data;
3037 size_t len, size;
3038};
3039
3040static 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
3054static 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}