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
\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
@ -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
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()}
\c const char *identify_game(char **name,

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

View File

@ -52,7 +52,7 @@ void print_line_width(drawing *dr, int width) {}
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, ...)
{

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().
*/
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 {
const game *game;
game_params *par;
game_ui *ui;
game_state *st;
game_state *st2;
};
@ -56,6 +57,7 @@ void document_free(document *doc)
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 @@ void document_free(document *doc)
* 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 @@ void document_add_puzzle(document *doc, const game *game, game_params *par,
}
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 @@ void document_print_page(const document *doc, drawing *dr, int page_nr)
* 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);
}

View File

@ -331,7 +331,7 @@ void midend_timer(midend *me, float tplus);
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 @@ void midend_serialise(midend *me,
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 @@ void SHA_Simple(const void *p, int len, unsigned char *output);
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);