diff options
| author | Simon Tatham <anakin@pobox.com> | 2023-04-22 12:54:11 +0100 |
|---|---|---|
| committer | Simon Tatham <anakin@pobox.com> | 2023-04-23 13:25:57 +0100 |
| commit | bb1ab36108942ed9b0c84bf68e22869994467a2a (patch) | |
| tree | 85d446d041629683a8536ef8fc046e482cfde15c /midend.c | |
| parent | ea6be8f0af766ed15b19260ae17fa793d3d6d4d8 (diff) | |
| download | puzzles-bb1ab36108942ed9b0c84bf68e22869994467a2a.zip puzzles-bb1ab36108942ed9b0c84bf68e22869994467a2a.tar.gz puzzles-bb1ab36108942ed9b0c84bf68e22869994467a2a.tar.bz2 puzzles-bb1ab36108942ed9b0c84bf68e22869994467a2a.tar.xz | |
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.
Diffstat (limited to 'midend.c')
| -rw-r--r-- | midend.c | 362 |
1 files changed, 356 insertions, 6 deletions
@@ -94,6 +94,8 @@ struct midend { int pressed_mouse_button; + struct midend_serialise_buf be_prefs; + int preferred_tilesize, preferred_tilesize_dpr, tilesize; int winwidth, winheight; @@ -126,12 +128,21 @@ struct deserialise_data { }; /* - * 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 @@ midend *midend_new(frontend *fe, const game *ourgame, 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 @@ void midend_new_game(midend *me) 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 @@ void midend_new_game(midend *me) 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 @@ config_item *midend_get_config(midend *me, int which, char **wintitle) 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 @@ const char *midend_set_config(midend *me, int which, config_item *cfg) if (error) return error; break; + + case CFG_PREFS: + midend_set_prefs(me, NULL, cfg); + break; } return NULL; @@ -2543,6 +2580,7 @@ static const char *midend_deserialise_internal( } 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 @@ const char *midend_print_puzzle(midend *me, document *doc, bool with_soln) 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; +} |