shithub: puzzles

Download patch

ref: bb1ab36108942ed9b0c84bf68e22869994467a2a
parent: ea6be8f0af766ed15b19260ae17fa793d3d6d4d8
author: Simon Tatham <anakin@pobox.com>
date: Sat Apr 22 08:54:11 EDT 2023

Keep a set of preferences in the midend.

This commit introduces a serialisation format for the user preferences
stored in game_ui, using the keyword identifiers that get_prefs is
required to write into its list of config_item. As a result, the
serialisation format looks enough like an ordinary config file that a
user could write one by hand.

The preferences for the game backend are kept in serialised form in
me->be_prefs. The typical use of this is to apply it to a just-created
game_ui by calling midend_apply_prefs(), which deserialises the prefs
buffer into a list of config_item and passes it to the backend's
set_prefs function, overwriting the preference fields (but no others)
of the game_ui.

This is duly done when creating a new game, when loading a game from a
save file, and also when printing a puzzle. To make the latter work,
document_add_puzzle now takes a game_ui (and keeps ownership of it
afterwards), and passes that to the backend's compute_size and print
functions.

The backend's own get_prefs and set_prefs functions are wrapped by
midend_get_prefs and midend_set_prefs. This is partly as a convenience
(it deals with optionally constructing a game_ui specially to call the
backend with), but mostly so that there will be a convenient place in
the midend to add standard preferences applying across all puzzles.
No cross-puzzle preferences are provided yet.

There are two external interfaces to all this, and in this commit,
neither one is yet called by any frontend:

A new pair of midend functions is exposed to the front end, called
midend_load_prefs and midend_save_prefs. These have a similar API to
midend_serialise and midend_deserialise, taking a read/write function
pointer and a context. So front ends that can already load/save a game
to a file on disk should find it easy to add a similar set of
functions loading/saving user preferences.

Secondly, a new value CFG_PREFS is added to the enumeration of
configuration dialog types, alongside the ones for the Custom game
type, entering a game description and entering a random seed. This
should make it easy for frontends to offer a Preferences dialog,
because it will operate almost exactly like three dialogs they already
handle.

--- a/devel.but
+++ b/devel.but
@@ -3542,7 +3542,11 @@
 description itself. This should be used when the user selects
 \q{Random Seed} from the game menu (or equivalent).
 
-(A fourth value \cw{CFG_FRONTEND_SPECIFIC} is provided in this
+\dt \cw{CFG_PREFS}
+
+\dd Requests a box suitable for configuring user preferences.
+
+(An additional value \cw{CFG_FRONTEND_SPECIFIC} is provided in this
 enumeration, so that frontends can extend it for their own internal
 use. For example, you might wrap this function with a
 \cw{frontend_get_config} which handles some values of \c{which} itself
@@ -3795,6 +3799,32 @@
 \k{identify-game} for a helper function which will allow you to
 identify a save file before you instantiate your mid-end in the first
 place.
+
+\H{midend-save-prefs} \cw{midend_save_prefs()}
+
+\c void midend_save_prefs(
+\c     midend *me, void (*write)(void *ctx, const void *buf, int len),
+\c     void *wctx);
+
+Calling this function causes the mid-end to write out the states of
+all user-settable preference options, including its own cross-platform
+preferences and ones exported by a particular game via
+\cw{get_prefs()} and \cw{set_prefs()} (\k{backend-get-prefs},
+\k{backend-set-prefs}). The output is a textual format suitable for
+writing into a configuration file on disk.
+
+The \c{write} and \c{wctx} parameters have the same semantics as for
+\cw{midend_serialise()} (\k{midend-serialise}).
+
+\H{midend-load-prefs} \cw{midend_load_prefs()}
+
+\c const char *midend_load_prefs(
+\c     midend *me, bool (*read)(void *ctx, void *buf, int len),
+\c     void *rctx);
+
+This function is used to load a configuration file in the same format
+emitted by \cw{midend_save_prefs()}, and import all the preferences
+described in the file into the current mid-end.
 
 \H{identify-game} \cw{identify_game()}
 
--- a/midend.c
+++ b/midend.c
@@ -94,6 +94,8 @@
 
     int pressed_mouse_button;
 
+    struct midend_serialise_buf be_prefs;
+
     int preferred_tilesize, preferred_tilesize_dpr, tilesize;
     int winwidth, winheight;
 
@@ -126,12 +128,21 @@
 };
 
 /*
- * Forward reference.
+ * Forward references.
  */
 static const char *midend_deserialise_internal(
     midend *me, bool (*read)(void *ctx, void *buf, int len), void *rctx,
     const char *(*check)(void *ctx, midend *, const struct deserialise_data *),
     void *cctx);
+static void midend_serialise_prefs(
+    midend *me, game_ui *ui,
+    void (*write)(void *ctx, const void *buf, int len), void *wctx);
+static const char *midend_deserialise_prefs(
+    midend *me, game_ui *ui,
+    bool (*read)(void *ctx, void *buf, int len), void *rctx);
+static config_item *midend_get_prefs(midend *me, game_ui *ui);
+static void midend_set_prefs(midend *me, game_ui *ui, config_item *all_prefs);
+static void midend_apply_prefs(midend *me, game_ui *ui);
 
 void midend_reset_tilesize(midend *me)
 {
@@ -223,6 +234,9 @@
     else
 	me->drawing = NULL;
 
+    me->be_prefs.buf = NULL;
+    me->be_prefs.size = me->be_prefs.len = 0;
+
     midend_reset_tilesize(me);
 
     sfree(randseed);
@@ -638,6 +652,7 @@
     if (me->ui)
         me->ourgame->free_ui(me->ui);
     me->ui = me->ourgame->new_ui(me->states[0].state);
+    midend_apply_prefs(me, me->ui);
     midend_set_timer(me);
     me->pressed_mouse_button = 0;
 
@@ -647,6 +662,20 @@
     me->newgame_can_store_undo = true;
 }
 
+const char *midend_load_prefs(
+    midend *me, bool (*read)(void *ctx, void *buf, int len), void *rctx)
+{
+    const char *err = midend_deserialise_prefs(me, NULL, read, rctx);
+    return err;
+}
+
+void midend_save_prefs(midend *me,
+                       void (*write)(void *ctx, const void *buf, int len),
+                       void *wctx)
+{
+    midend_serialise_prefs(me, NULL, write, wctx);
+}
+
 bool midend_can_undo(midend *me)
 {
     return (me->statepos > 1 || me->newgame_undo.len);
@@ -1711,6 +1740,10 @@
 	ret[1].name = NULL;
 
 	return ret;
+      case CFG_PREFS:
+	sprintf(titlebuf, "%s preferences", me->ourgame->name);
+	*wintitle = titlebuf;
+	return midend_get_prefs(me, NULL);
     }
 
     assert(!"We shouldn't be here");
@@ -1959,6 +1992,10 @@
 	if (error)
 	    return error;
 	break;
+
+      case CFG_PREFS:
+        midend_set_prefs(me, NULL, cfg);
+        break;
     }
 
     return NULL;
@@ -2543,6 +2580,7 @@
     }
 
     data.ui = me->ourgame->new_ui(data.states[0].state);
+    midend_apply_prefs(me, data.ui);
     if (data.uistr && me->ourgame->decode_ui)
         me->ourgame->decode_ui(data.ui, data.uistr,
                                data.states[data.statepos-1].state);
@@ -2815,14 +2853,326 @@
 	soln = NULL;
 
     /*
-     * This call passes over ownership of the two game_states and
-     * the game_params. Hence we duplicate the ones we want to
-     * keep, and we don't have to bother freeing soln if it was
-     * non-NULL.
+     * This call passes over ownership of the two game_states, the
+     * game_params and the game_ui. Hence we duplicate the ones we
+     * want to keep, and we don't have to bother freeing soln if it
+     * was non-NULL.
      */
+    game_ui *ui = me->ourgame->new_ui(me->states[0].state);
+    midend_apply_prefs(me, ui);
     document_add_puzzle(doc, me->ourgame,
-			me->ourgame->dup_params(me->curparams),
+			me->ourgame->dup_params(me->curparams), ui,
 			me->ourgame->dup_game(me->states[0].state), soln);
 
     return NULL;
+}
+
+static void midend_apply_prefs(midend *me, game_ui *ui)
+{
+    struct midend_serialise_buf_read_ctx rctx[1];
+    rctx->ser = &me->be_prefs;
+    rctx->len = me->be_prefs.len;
+    rctx->pos = 0;
+    const char *err = midend_deserialise_prefs(
+        me, me->ui, midend_serialise_buf_read, rctx);
+    /* This should have come from our own serialise function, so
+     * it should never be invalid. */
+    assert(!err && "Bad internal serialisation of preferences");
+}
+
+static config_item *midend_get_prefs(midend *me, game_ui *ui)
+{
+    int n_be_prefs, n_me_prefs, pos, i;
+    config_item *all_prefs, *be_prefs;
+
+    be_prefs = NULL;
+    n_be_prefs = 0;
+    if (me->ourgame->get_prefs) {
+        if (ui) {
+            be_prefs = me->ourgame->get_prefs(ui);
+        } else if (me->ui) {
+            be_prefs = me->ourgame->get_prefs(me->ui);
+        } else {
+            game_ui *tmp_ui = me->ourgame->new_ui(NULL);
+            be_prefs = me->ourgame->get_prefs(tmp_ui);
+            me->ourgame->free_ui(tmp_ui);
+        }
+        while (be_prefs[n_be_prefs].type != C_END)
+            n_be_prefs++;
+    }
+
+    n_me_prefs = 0;
+    all_prefs = snewn(n_me_prefs + n_be_prefs + 1, config_item);
+
+    pos = 0;
+
+    for (i = 0; i < n_be_prefs; i++) {
+        all_prefs[pos] = be_prefs[i];  /* structure copy */
+        pos++;
+    }
+
+    all_prefs[pos].name = NULL;
+    all_prefs[pos].type = C_END;
+
+    if (be_prefs)
+        free_cfg(be_prefs);
+
+    return all_prefs;
+}
+
+static void midend_set_prefs(midend *me, game_ui *ui, config_item *all_prefs)
+{
+    int pos = 0;
+    game_ui *tmpui = NULL;
+
+    if (me->ourgame->get_prefs) {
+        if (!ui)
+            ui = tmpui = me->ourgame->new_ui(NULL);
+        me->ourgame->set_prefs(ui, all_prefs + pos);
+    }
+
+    me->be_prefs.len = 0;
+    midend_serialise_prefs(me, ui, midend_serialise_buf_write, &me->be_prefs);
+
+    if (tmpui)
+        me->ourgame->free_ui(tmpui);
+}
+
+static void midend_serialise_prefs(
+    midend *me, game_ui *ui,
+    void (*write)(void *ctx, const void *buf, int len), void *wctx)
+{
+    config_item *cfg;
+    int i;
+
+    cfg = midend_get_prefs(me, ui);
+
+    assert(cfg);
+
+    for (i = 0; cfg[i].type != C_END; i++) {
+        config_item *it = &cfg[i];
+
+        /* Expect keywords to be made up only of simple characters */
+        assert(it->kw[strspn(it->kw, "abcdefghijklmnopqrstuvwxyz-")] == '\0');
+
+        write(wctx, it->kw, strlen(it->kw));
+        write(wctx, "=", 1);
+
+        switch (it->type) {
+          case C_BOOLEAN:
+            if (it->u.boolean.bval)
+                write(wctx, "true", 4);
+            else
+                write(wctx, "false", 5);
+            break;
+          case C_STRING: {
+            const char *p = it->u.string.sval;
+            while (*p) {
+                char c = *p++;
+                write(wctx, &c, 1);
+                if (c == '\n')
+                write(wctx, " ", 1);
+            }
+            break;
+          }
+          case C_CHOICES: {
+            int n = it->u.choices.selected;
+            const char *p = it->u.choices.choicekws;
+            char sepstr[2];
+
+            sepstr[0] = *p++;
+            sepstr[1] = '\0';
+
+            while (n > 0) {
+                const char *q = strchr(p, sepstr[0]);
+                assert(q != NULL && "Value out of range in C_CHOICES");
+                p = q+1;
+                n--;
+            }
+
+            write(wctx, p, strcspn(p, sepstr));
+            break;
+          }
+        }
+
+        write(wctx, "\n", 1);
+    }
+}
+
+struct buffer {
+    char *data;
+    size_t len, size;
+};
+
+static void buffer_append(struct buffer *buf, char c)
+{
+    if (buf->len + 1 > buf->size) {
+        size_t new_size = buf->size + buf->size / 4 + 128;
+        assert(new_size > buf->size);
+        buf->data = sresize(buf->data, new_size, char);
+        buf->size = new_size;
+        assert(buf->len < buf->size);
+    }
+    buf->data[buf->len++] = c;
+    assert(buf->len < buf->size);
+    buf->data[buf->len] = '\0';
+}
+
+static const char *midend_deserialise_prefs(
+    midend *me, game_ui *ui,
+    bool (*read)(void *ctx, void *buf, int len), void *rctx)
+{
+    config_item *cfg, *it;
+    int i;
+    struct buffer buf[1] = {{ NULL, 0, 0 }};
+    const char *errmsg = NULL;
+    char read_char;
+    char ungot_char = '\0';
+    bool have_ungot_a_char = false, eof = false;
+
+    cfg = midend_get_prefs(me, ui);
+
+    while (!eof) {
+        if (have_ungot_a_char) {
+            read_char = ungot_char;
+            have_ungot_a_char = false;
+        } else {
+            if (!read(rctx, &read_char, 1))
+                goto out;           /* EOF at line start == success */
+        }
+
+        if (read_char == '#' || read_char == '\n') {
+            /* Skip comment or blank line */
+            while (read_char != '\n') {
+                if (!read(rctx, &read_char, 1))
+                    goto out;  /* EOF during boring line == success */
+            }
+            continue;
+        }
+
+        buf->len = 0;
+        while (true) {
+            buffer_append(buf, read_char);
+            if (!read(rctx, &read_char, 1)) {
+                errmsg = "Partial line at end of preferences file";
+                goto out;
+            }
+            if (read_char == '\n') {
+                errmsg = "Expected '=' after keyword";
+                goto out;
+            }
+            if (read_char == '=')
+                break;
+        }
+
+        it = NULL;
+        for (i = 0; cfg[i].type != C_END; i++)
+            if (!strcmp(buf->data, cfg[i].kw))
+                it = &cfg[i];
+
+        buf->len = 0;
+        while (true) {
+            if (!read(rctx, &read_char, 1)) {
+                /* We tolerate missing \n at the end of the file, so
+                 * this is taken to mean we've got a complete config
+                 * directive. But set the eof flag so that we stop
+                 * after processing it. */
+                eof = true;
+                break;
+            } else if (read_char == '\n') {
+                /* Newline _might_ be the end of this config
+                 * directive, unless it's followed by a space, in
+                 * which case it's a space-stuffed line
+                 * continuation. */
+                if (read(rctx, &read_char, 1)) {
+                    if (read_char == ' ') {
+                        buffer_append(buf, '\n');
+                        continue;
+                    } else {
+                        /* But if the next character wasn't a space,
+                         * then we must unget it so that it'll be
+                         * available to the next iteration of our
+                         * outer loop as the first character of the
+                         * next keyword. */
+                        ungot_char = read_char;
+                        have_ungot_a_char = true;
+                        break;
+                    }
+                } else {
+                    /* And if the newline was followed by EOF, then we
+                     * should finish this iteration of the outer
+                     * loop normally, and then not go round again. */
+                    eof = true;
+                    break;
+                }
+            } else {
+                /* Any other character is just added to the buffer. */
+                buffer_append(buf, read_char);
+            }
+        }
+
+        if (!it) {
+            /*
+             * Tolerate unknown keywords in a preferences file, on the
+             * assumption that they're from a different (probably
+             * later) version of the game.
+             */
+            continue;
+        }
+
+        switch (it->type) {
+          case C_BOOLEAN:
+            if (!strcmp(buf->data, "true"))
+                it->u.boolean.bval = true;
+            else if (!strcmp(buf->data, "false"))
+                it->u.boolean.bval = false;
+            else {
+                errmsg = "Value for boolean was not 'true' or 'false'";
+                goto out;
+            }
+            break;
+          case C_STRING:
+            sfree(it->u.string.sval);
+            it->u.string.sval = buf->data;
+            buf->data = NULL;
+            buf->len = buf->size = 0;
+            break;
+          case C_CHOICES: {
+            int n = 0;
+            bool found = false;
+            const char *p = it->u.choices.choicekws;
+            char sepstr[2];
+
+            sepstr[0] = *p;
+            sepstr[1] = '\0';
+
+            while (*p++) {
+                int len = strcspn(p, sepstr);
+                if (buf->len == len && !memcmp(p, buf->data, len)) {
+                    it->u.choices.selected = n;
+                    found = true;
+                    break;
+                }
+                p += len;
+                n++;
+            }
+
+            if (!found) {
+                errmsg = "Invalid value for enumeration";
+                goto out;
+            }
+
+            break;
+          }
+        }
+    }
+
+  out:
+
+    if (!errmsg)
+        midend_set_prefs(me, ui, cfg);
+
+    free_cfg(cfg);
+    sfree(buf->data);
+    return errmsg;
 }
--- a/nullfe.c
+++ b/nullfe.c
@@ -52,7 +52,7 @@
 void print_line_dotted(drawing *dr, bool dotted) {}
 void status_bar(drawing *dr, const char *text) {}
 void document_add_puzzle(document *doc, const game *game, game_params *par,
-			 game_state *st, game_state *st2) {}
+			 game_ui *ui, game_state *st, game_state *st2) {}
 
 void fatal(const char *fmt, ...)
 {
--- a/osx.m
+++ b/osx.m
@@ -174,7 +174,7 @@
  * this stub to satisfy the reference in midend_print_puzzle().
  */
 void document_add_puzzle(document *doc, const game *game, game_params *par,
-			 game_state *st, game_state *st2)
+                         game_ui *ui, game_state *st, game_state *st2)
 {
 }
 
--- a/printing.c
+++ b/printing.c
@@ -10,6 +10,7 @@
 struct puzzle {
     const game *game;
     game_params *par;
+    game_ui *ui;
     game_state *st;
     game_state *st2;
 };
@@ -56,6 +57,7 @@
 
     for (i = 0; i < doc->npuzzles; i++) {
 	doc->puzzles[i].game->free_params(doc->puzzles[i].par);
+	doc->puzzles[i].game->free_ui(doc->puzzles[i].ui);
 	doc->puzzles[i].game->free_game(doc->puzzles[i].st);
 	if (doc->puzzles[i].st2)
 	    doc->puzzles[i].game->free_game(doc->puzzles[i].st2);
@@ -75,7 +77,7 @@
  * another sheet (typically the solution to the first game_state).
  */
 void document_add_puzzle(document *doc, const game *game, game_params *par,
-			 game_state *st, game_state *st2)
+			 game_ui *ui, game_state *st, game_state *st2)
 {
     if (doc->npuzzles >= doc->puzzlesize) {
 	doc->puzzlesize += 32;
@@ -83,6 +85,7 @@
     }
     doc->puzzles[doc->npuzzles].game = game;
     doc->puzzles[doc->npuzzles].par = par;
+    doc->puzzles[doc->npuzzles].ui = ui;
     doc->puzzles[doc->npuzzles].st = st;
     doc->puzzles[doc->npuzzles].st2 = st2;
     doc->npuzzles++;
@@ -274,14 +277,9 @@
          * permit each game to choose its own?)
          */
         tilesize = 512;
-        {
-            game_ui *ui = pz->game->new_ui(pz->st);
-            pz->game->compute_size(pz->par, tilesize, ui,
-                                   &pixw, &pixh);
-            print_begin_puzzle(dr, xm, xc, ym, yc, pixw, pixh, w, scale);
-            pz->game->print(dr, pass == 0 ? pz->st : pz->st2, ui, tilesize);
-            pz->game->free_ui(ui);
-        }
+        pz->game->compute_size(pz->par, tilesize, pz->ui, &pixw, &pixh);
+        print_begin_puzzle(dr, xm, xc, ym, yc, pixw, pixh, w, scale);
+        pz->game->print(dr, pass == 0 ? pz->st : pz->st2, pz->ui, tilesize);
         print_end_puzzle(dr);
     }
 
--- a/puzzles.h
+++ b/puzzles.h
@@ -331,7 +331,7 @@
 struct preset_menu *midend_get_presets(midend *me, int *id_limit);
 int midend_which_preset(midend *me);
 bool midend_wants_statusbar(midend *me);
-enum { CFG_SETTINGS, CFG_SEED, CFG_DESC, CFG_FRONTEND_SPECIFIC };
+enum { CFG_SETTINGS, CFG_SEED, CFG_DESC, CFG_PREFS, CFG_FRONTEND_SPECIFIC };
 config_item *midend_get_config(midend *me, int which, char **wintitle);
 const char *midend_set_config(midend *me, int which, config_item *cfg);
 const char *midend_game_id(midend *me, const char *id);
@@ -352,6 +352,11 @@
 const char *midend_deserialise(midend *me,
                                bool (*read)(void *ctx, void *buf, int len),
                                void *rctx);
+const char *midend_load_prefs(
+    midend *me, bool (*read)(void *ctx, void *buf, int len), void *rctx);
+void midend_save_prefs(midend *me,
+                       void (*write)(void *ctx, const void *buf, int len),
+                       void *wctx);
 const char *identify_game(char **name,
                           bool (*read)(void *ctx, void *buf, int len),
                           void *rctx);
@@ -557,7 +562,7 @@
 document *document_new(int pw, int ph, float userscale);
 void document_free(document *doc);
 void document_add_puzzle(document *doc, const game *game, game_params *par,
-			 game_state *st, game_state *st2);
+			 game_ui *ui, game_state *st, game_state *st2);
 int document_npages(const document *doc);
 void document_begin(const document *doc, drawing *dr);
 void document_end(const document *doc, drawing *dr);