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.
This commit is contained in:
Simon Tatham
2023-04-22 12:54:11 +01:00
parent ea6be8f0af
commit bb1ab36108
6 changed files with 403 additions and 20 deletions

362
midend.c
View File

@ -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;
}