From 43db4aa38e83595dc6df245cb952795f9f306ed0 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Mon, 24 Apr 2023 10:17:33 +0100 Subject: [PATCH] Support user preferences in the Emscripten frontend. Here, user preferences are stored in localStorage, so that they can persist when you come back to the same puzzle page later. localStorage is global across a whole web server, which means we need to take care to put our uses of it in a namespace reasonably unlikely to collide with unrelated web pages on the same server. Ben suggested that a good way to do this would be to store things in localStorage under keys derived from location.pathname. In this case I've appended a fragment id "#preferences" to that, so that space alongside it remains for storing other things we might want in future (such as serialised saved-game files used as quick-save slots). When loading preferences, I've chosen to pass the whole serialised preferences buffer from Javascript to C as a single C string argument to a callback, rather than reusing the existing system for C to read the save file a chunk at a time. Partly that's because preferences data is bounded in size whereas saved games can keep growing; also it's because the way I'm storing preferences data means it will be a UTF-8 string, and I didn't fancy trying to figure out byte offsets in the data on the JS side. I think at this point I should stop keeping a list in the docs of which frontends support preferences. Most of the in-tree ones do now, and that means the remaining interesting frontends are ones I don't have a full list of. At this moment I guess no out-of-tree frontends support preferences (unless someone is _very_ quick off the mark), but as and when that changes, I won't necessarily know, and don't want to have to keep updating the docs when I find out. --- cmake/platforms/emscripten.cmake | 2 + emcc.c | 84 +++++++++++++++++++++++++++++++- emcclib.js | 23 +++++++++ emccpre.js | 12 +++++ html/jspage.pl | 1 + puzzles.but | 8 +-- 6 files changed, 124 insertions(+), 6 deletions(-) diff --git a/cmake/platforms/emscripten.cmake b/cmake/platforms/emscripten.cmake index c84a3c7..d74217f 100644 --- a/cmake/platforms/emscripten.cmake +++ b/cmake/platforms/emscripten.cmake @@ -47,6 +47,8 @@ set(emcc_export_list _restore_puzzle_size # Callback when device pixel ratio changes _rescale_puzzle + # Callback for loading user preferences + _prefs_load_callback # Main program, run at initialisation time _main) diff --git a/emcc.c b/emcc.c index 5338dd4..60ef8c8 100644 --- a/emcc.c +++ b/emcc.c @@ -101,6 +101,9 @@ extern void js_focus_canvas(void); extern bool js_savefile_read(void *buf, int len); +extern void js_save_prefs(const char *); +extern void js_load_prefs(midend *); + /* * These functions are called from JavaScript, so their prototypes * need to be kept in sync with emccpre.js. @@ -121,6 +124,11 @@ void resize_puzzle(int w, int h); void restore_puzzle_size(int w, int h); void rescale_puzzle(void); +/* + * Internal forward references. + */ +static void save_prefs(midend *me); + /* * Call JS to get the date, and use that to initialise our random * number generator to invent the first game seed. @@ -743,10 +751,20 @@ static void cfg_end(bool use_results) * open for the user to adjust them and try again. */ js_error_box(err); + } else if (cfg_which == CFG_PREFS) { + /* + * Acceptable settings for user preferences: enact them + * without blowing away the current game. + */ + resize(); + midend_redraw(me); + free_cfg(cfg); + js_dialog_cleanup(); + save_prefs(me); } else { /* - * New settings are fine; start a new game and close the - * dialog. + * Acceptable settings for the remaining configuration + * types: start a new game and close the dialog. */ select_appropriate_preset(); midend_new_game(me); @@ -849,6 +867,9 @@ void command(int n) post_move(); js_focus_canvas(); break; + case 10: /* user preferences */ + cfg_start(CFG_PREFS); + break; } } @@ -925,6 +946,64 @@ void load_game(void) } } +/* ---------------------------------------------------------------------- + * Functions to load and save preferences, calling out to JS to access + * the appropriate localStorage slot. + */ + +static void save_prefs(midend *me) +{ + struct savefile_write_ctx ctx; + size_t size; + + /* First pass, to count up the size */ + ctx.buffer = NULL; + ctx.pos = 0; + midend_save_prefs(me, savefile_write, &ctx); + size = ctx.pos; + + /* Second pass, to actually write out the data. As with + * get_save_file, we append a terminating \0. */ + ctx.buffer = snewn(size+1, char); + ctx.pos = 0; + midend_save_prefs(me, savefile_write, &ctx); + assert(ctx.pos == size); + ctx.buffer[ctx.pos] = '\0'; + + js_save_prefs(ctx.buffer); + + sfree(ctx.buffer); +} + +struct prefs_read_ctx { + const char *buffer; + size_t pos, len; +}; + +static bool prefs_read(void *vctx, void *buf, int len) +{ + struct prefs_read_ctx *ctx = (struct prefs_read_ctx *)vctx; + + if (len < 0) + return false; + if (ctx->len - ctx->pos < len) + return false; + memcpy(buf, ctx->buffer + ctx->pos, len); + ctx->pos += len; + return true; +} + +void prefs_load_callback(midend *me, const char *prefs) +{ + struct prefs_read_ctx ctx; + + ctx.buffer = prefs; + ctx.len = strlen(prefs); + ctx.pos = 0; + + midend_load_prefs(me, prefs_read, &ctx); +} + /* ---------------------------------------------------------------------- * Setup function called at page load time. It's called main() because * that's the most convenient thing in Emscripten, but it's not main() @@ -948,6 +1027,7 @@ int main(int argc, char **argv) * Instantiate a midend. */ me = midend_new(NULL, &thegame, &js_drawing, NULL); + js_load_prefs(me); /* * Chuck in the HTML fragment ID if we have one (trimming the diff --git a/emcclib.js b/emcclib.js index 80d17b9..d161c49 100644 --- a/emcclib.js +++ b/emcclib.js @@ -792,5 +792,28 @@ mergeInto(LibraryManager.library, { */ js_savefile_read: function(buf, len) { return savefile_read_callback(buf, len); + }, + + /* + * void js_save_prefs(const char *); + * + * Write a buffer of serialised preferences data into localStorage. + */ + js_save_prefs: function(buf) { + var prefsdata = UTF8ToString(buf); + localStorage.setItem(location.pathname + "#preferences", prefsdata); + }, + + /* + * void js_load_prefs(midend *); + * + * Retrieve preferences data from localStorage. If there is any, + * pass it back in as a string, via prefs_load_callback. + */ + js_load_prefs: function(me) { + var prefsdata = localStorage.getItem(location.pathname+"#preferences"); + if (prefsdata !== undefined && prefsdata !== null) { + prefs_load_callback(me, prefsdata); + } } }); diff --git a/emccpre.js b/emccpre.js index 7c68445..a114ce1 100644 --- a/emccpre.js +++ b/emccpre.js @@ -141,6 +141,11 @@ var dlg_return_sval, dlg_return_ival; // process of loading at a time. var savefile_read_callback; +// void prefs_load_callback(midend *me, const char *prefs); +// +// Callback for passing in preferences data retrieved from localStorage. +var prefs_load_callback; + // The
  • Type
  • diff --git a/puzzles.but b/puzzles.but index be55f8e..1e0ebb7 100644 --- a/puzzles.but +++ b/puzzles.but @@ -179,10 +179,10 @@ solving it yourself after seeing the answer, you can just press Undo. \dt \i\e{Preferences} -\dd Where supported (currently on Windows, Unix and MacOS), brings up -a dialog allowing you to configure personal preferences about a -particular game. Some of these preferences will be specific to a -particular game; others will be common to all games. +\dd Where supported, brings up a dialog allowing you to configure +personal preferences about a particular game. Some of these +preferences will be specific to a particular game; others will be +common to all games. \lcont{