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.
This commit is contained in:
Simon Tatham
2023-04-24 10:17:33 +01:00
parent 2b6d34adbd
commit 43db4aa38e
6 changed files with 124 additions and 6 deletions

View File

@ -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)

84
emcc.c
View File

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

View File

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

View File

@ -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 <ul> object implementing the game-type drop-down, and a list of
// the sub-lists inside it. Used by js_add_preset().
var gametypelist = document.getElementById("gametype");
@ -161,6 +166,7 @@ var permalink_desc = document.getElementById("permalink-desc");
// The various buttons. Undo and redo are used by js_enable_undo_redo().
var specific_button = document.getElementById("specific");
var random_button = document.getElementById("random");
var prefs_button = document.getElementById("prefs");
var new_button = document.getElementById("new");
var restart_button = document.getElementById("restart");
var undo_button = document.getElementById("undo");
@ -425,6 +431,10 @@ function initPuzzle() {
if (dlg_dimmer === null)
command(9);
};
if (prefs_button) prefs_button.onclick = function(event) {
if (dlg_dimmer === null)
command(10);
};
// 'number' is used for C pointers
var get_save_file = Module.cwrap('get_save_file', 'number', []);
@ -682,6 +692,8 @@ function initPuzzle() {
dlg_return_ival = Module.cwrap('dlg_return_ival', 'void',
['number','number']);
timer_callback = Module.cwrap('timer_callback', 'void', ['number']);
prefs_load_callback = Module.cwrap('prefs_load_callback', 'void',
['number','string']);
if (resizable_div !== null) {
var resize_handle = document.getElementById("resizehandle");

View File

@ -340,6 +340,7 @@ ${unfinishedpara}
<li><button type="button" id="random">Enter random seed...</button></li>
<li><button type="button" id="save">Download save file...</button></li>
<li><button type="button" id="load">Upload save file...</button></li>
<li><button type="button" id="prefs">Preferences...</button></li>
</ul></div></li>
<li><div tabindex="0">Type<ul role="menu" id="gametype"></ul></div></li>
<li role="separator"></li>

View File

@ -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{