diff --git a/devel.but b/devel.but index 2459863..bbf98a4 100644 --- a/devel.but +++ b/devel.but @@ -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, diff --git a/midend.c b/midend.c index 87b3c94..0038609 100644 --- a/midend.c +++ b/midend.c @@ -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; +} diff --git a/nullfe.c b/nullfe.c index 971250e..9a57832 100644 --- a/nullfe.c +++ b/nullfe.c @@ -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, ...) { diff --git a/osx.m b/osx.m index d18aeec..db68d2f 100644 --- a/osx.m +++ b/osx.m @@ -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) { } diff --git a/printing.c b/printing.c index 7301ba0..0a91194 100644 --- 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 @@ 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); } diff --git a/puzzles.h b/puzzles.h index 7e192c0..e429c26 100644 --- a/puzzles.h +++ b/puzzles.h @@ -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);