ref: 43db4aa38e83595dc6df245cb952795f9f306ed0
parent: 2b6d34adbd03a6110d4c2a0a0959eb6e30d3936d
author: Simon Tatham <anakin@pobox.com>
date: Mon Apr 24 06:17:33 EDT 2023
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.
--- a/cmake/platforms/emscripten.cmake
+++ b/cmake/platforms/emscripten.cmake
@@ -47,6 +47,8 @@
_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)
--- a/emcc.c
+++ b/emcc.c
@@ -101,6 +101,9 @@
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.
@@ -122,6 +125,11 @@
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 @@
* 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 @@
post_move();
js_focus_canvas();
break;
+ case 10: /* user preferences */
+ cfg_start(CFG_PREFS);
+ break;
}
}
@@ -926,6 +947,64 @@
}
/* ----------------------------------------------------------------------
+ * 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()
* in the usual sense of bounding the program's entire execution.
@@ -948,6 +1027,7 @@
* 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
--- a/emcclib.js
+++ b/emcclib.js
@@ -792,5 +792,28 @@
*/
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);
+ }
}
});
--- a/emccpre.js
+++ b/emccpre.js
@@ -141,6 +141,11 @@
// 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 @@
// 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 @@
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 @@
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");
--- a/html/jspage.pl
+++ b/html/jspage.pl
@@ -340,6 +340,7 @@
<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>
--- a/puzzles.but
+++ b/puzzles.but
@@ -179,10 +179,10 @@
\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{