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

View File

@ -3542,7 +3542,11 @@ viewing the existing one). The mid-end generates this dialog box
description itself. This should be used when the user selects description itself. This should be used when the user selects
\q{Random Seed} from the game menu (or equivalent). \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 enumeration, so that frontends can extend it for their own internal
use. For example, you might wrap this function with a use. For example, you might wrap this function with a
\cw{frontend_get_config} which handles some values of \c{which} itself \cw{frontend_get_config} which handles some values of \c{which} itself
@ -3796,6 +3800,32 @@ application is a monolithic one containing all the puzzles. See
identify a save file before you instantiate your mid-end in the first identify a save file before you instantiate your mid-end in the first
place. 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()} \H{identify-game} \cw{identify_game()}
\c const char *identify_game(char **name, \c const char *identify_game(char **name,

362
midend.c
View File

@ -94,6 +94,8 @@ struct midend {
int pressed_mouse_button; int pressed_mouse_button;
struct midend_serialise_buf be_prefs;
int preferred_tilesize, preferred_tilesize_dpr, tilesize; int preferred_tilesize, preferred_tilesize_dpr, tilesize;
int winwidth, winheight; int winwidth, winheight;
@ -126,12 +128,21 @@ struct deserialise_data {
}; };
/* /*
* Forward reference. * Forward references.
*/ */
static const char *midend_deserialise_internal( static const char *midend_deserialise_internal(
midend *me, bool (*read)(void *ctx, void *buf, int len), void *rctx, midend *me, bool (*read)(void *ctx, void *buf, int len), void *rctx,
const char *(*check)(void *ctx, midend *, const struct deserialise_data *), const char *(*check)(void *ctx, midend *, const struct deserialise_data *),
void *cctx); 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) void midend_reset_tilesize(midend *me)
{ {
@ -223,6 +234,9 @@ midend *midend_new(frontend *fe, const game *ourgame,
else else
me->drawing = NULL; me->drawing = NULL;
me->be_prefs.buf = NULL;
me->be_prefs.size = me->be_prefs.len = 0;
midend_reset_tilesize(me); midend_reset_tilesize(me);
sfree(randseed); sfree(randseed);
@ -638,6 +652,7 @@ void midend_new_game(midend *me)
if (me->ui) if (me->ui)
me->ourgame->free_ui(me->ui); me->ourgame->free_ui(me->ui);
me->ui = me->ourgame->new_ui(me->states[0].state); me->ui = me->ourgame->new_ui(me->states[0].state);
midend_apply_prefs(me, me->ui);
midend_set_timer(me); midend_set_timer(me);
me->pressed_mouse_button = 0; me->pressed_mouse_button = 0;
@ -647,6 +662,20 @@ void midend_new_game(midend *me)
me->newgame_can_store_undo = true; 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) bool midend_can_undo(midend *me)
{ {
return (me->statepos > 1 || me->newgame_undo.len); 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; ret[1].name = NULL;
return ret; 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"); assert(!"We shouldn't be here");
@ -1959,6 +1992,10 @@ const char *midend_set_config(midend *me, int which, config_item *cfg)
if (error) if (error)
return error; return error;
break; break;
case CFG_PREFS:
midend_set_prefs(me, NULL, cfg);
break;
} }
return NULL; return NULL;
@ -2543,6 +2580,7 @@ static const char *midend_deserialise_internal(
} }
data.ui = me->ourgame->new_ui(data.states[0].state); data.ui = me->ourgame->new_ui(data.states[0].state);
midend_apply_prefs(me, data.ui);
if (data.uistr && me->ourgame->decode_ui) if (data.uistr && me->ourgame->decode_ui)
me->ourgame->decode_ui(data.ui, data.uistr, me->ourgame->decode_ui(data.ui, data.uistr,
data.states[data.statepos-1].state); data.states[data.statepos-1].state);
@ -2815,14 +2853,326 @@ const char *midend_print_puzzle(midend *me, document *doc, bool with_soln)
soln = NULL; soln = NULL;
/* /*
* This call passes over ownership of the two game_states and * This call passes over ownership of the two game_states, the
* the game_params. Hence we duplicate the ones we want to * game_params and the game_ui. Hence we duplicate the ones we
* keep, and we don't have to bother freeing soln if it was * want to keep, and we don't have to bother freeing soln if it
* non-NULL. * 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, 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); me->ourgame->dup_game(me->states[0].state), soln);
return NULL; 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;
}

View File

@ -52,7 +52,7 @@ void print_line_width(drawing *dr, int width) {}
void print_line_dotted(drawing *dr, bool dotted) {} void print_line_dotted(drawing *dr, bool dotted) {}
void status_bar(drawing *dr, const char *text) {} void status_bar(drawing *dr, const char *text) {}
void document_add_puzzle(document *doc, const game *game, game_params *par, 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, ...) void fatal(const char *fmt, ...)
{ {

2
osx.m
View File

@ -174,7 +174,7 @@ static bool savefile_read(void *wctx, void *buf, int len)
* this stub to satisfy the reference in midend_print_puzzle(). * this stub to satisfy the reference in midend_print_puzzle().
*/ */
void document_add_puzzle(document *doc, const game *game, game_params *par, 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)
{ {
} }

View File

@ -10,6 +10,7 @@
struct puzzle { struct puzzle {
const game *game; const game *game;
game_params *par; game_params *par;
game_ui *ui;
game_state *st; game_state *st;
game_state *st2; game_state *st2;
}; };
@ -56,6 +57,7 @@ void document_free(document *doc)
for (i = 0; i < doc->npuzzles; i++) { for (i = 0; i < doc->npuzzles; i++) {
doc->puzzles[i].game->free_params(doc->puzzles[i].par); 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); doc->puzzles[i].game->free_game(doc->puzzles[i].st);
if (doc->puzzles[i].st2) if (doc->puzzles[i].st2)
doc->puzzles[i].game->free_game(doc->puzzles[i].st2); doc->puzzles[i].game->free_game(doc->puzzles[i].st2);
@ -75,7 +77,7 @@ void document_free(document *doc)
* another sheet (typically the solution to the first game_state). * another sheet (typically the solution to the first game_state).
*/ */
void document_add_puzzle(document *doc, const game *game, game_params *par, 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) { if (doc->npuzzles >= doc->puzzlesize) {
doc->puzzlesize += 32; doc->puzzlesize += 32;
@ -83,6 +85,7 @@ void document_add_puzzle(document *doc, const game *game, game_params *par,
} }
doc->puzzles[doc->npuzzles].game = game; doc->puzzles[doc->npuzzles].game = game;
doc->puzzles[doc->npuzzles].par = par; doc->puzzles[doc->npuzzles].par = par;
doc->puzzles[doc->npuzzles].ui = ui;
doc->puzzles[doc->npuzzles].st = st; doc->puzzles[doc->npuzzles].st = st;
doc->puzzles[doc->npuzzles].st2 = st2; doc->puzzles[doc->npuzzles].st2 = st2;
doc->npuzzles++; doc->npuzzles++;
@ -274,14 +277,9 @@ void document_print_page(const document *doc, drawing *dr, int page_nr)
* permit each game to choose its own?) * permit each game to choose its own?)
*/ */
tilesize = 512; tilesize = 512;
{ pz->game->compute_size(pz->par, tilesize, pz->ui, &pixw, &pixh);
game_ui *ui = pz->game->new_ui(pz->st); print_begin_puzzle(dr, xm, xc, ym, yc, pixw, pixh, w, scale);
pz->game->compute_size(pz->par, tilesize, ui, pz->game->print(dr, pass == 0 ? pz->st : pz->st2, pz->ui, tilesize);
&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);
}
print_end_puzzle(dr); print_end_puzzle(dr);
} }

View File

@ -331,7 +331,7 @@ void midend_timer(midend *me, float tplus);
struct preset_menu *midend_get_presets(midend *me, int *id_limit); struct preset_menu *midend_get_presets(midend *me, int *id_limit);
int midend_which_preset(midend *me); int midend_which_preset(midend *me);
bool midend_wants_statusbar(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); 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_set_config(midend *me, int which, config_item *cfg);
const char *midend_game_id(midend *me, const char *id); const char *midend_game_id(midend *me, const char *id);
@ -352,6 +352,11 @@ void midend_serialise(midend *me,
const char *midend_deserialise(midend *me, const char *midend_deserialise(midend *me,
bool (*read)(void *ctx, void *buf, int len), bool (*read)(void *ctx, void *buf, int len),
void *rctx); 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, const char *identify_game(char **name,
bool (*read)(void *ctx, void *buf, int len), bool (*read)(void *ctx, void *buf, int len),
void *rctx); void *rctx);
@ -557,7 +562,7 @@ void SHA_Simple(const void *p, int len, unsigned char *output);
document *document_new(int pw, int ph, float userscale); document *document_new(int pw, int ph, float userscale);
void document_free(document *doc); void document_free(document *doc);
void document_add_puzzle(document *doc, const game *game, game_params *par, 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); int document_npages(const document *doc);
void document_begin(const document *doc, drawing *dr); void document_begin(const document *doc, drawing *dr);
void document_end(const document *doc, drawing *dr); void document_end(const document *doc, drawing *dr);