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);