shithub: puzzles

Download patch

ref: 0058331aeb027f7441a04d99cc7c1e445bd896d9
parent: 0d1a1f08bac25a4641c38a8e42dfa6e2bb9981d7
author: Simon Tatham <anakin@pobox.com>
date: Fri Apr 21 11:50:05 EDT 2023

New backend functions: get_prefs and set_prefs.

These are similar to the existing pair configure() and custom_params()
in that get_prefs() returns an array of config_item describing a set
of dialog-box controls to present to the user, and set_prefs()
receives the same array with answers filled in and implements the
answers. But where configure() and custom_params() operate on a
game_params structure, the new pair operate on a game_ui, and are
intended to permit GUI configuration of all the settings I just moved
into that structure.

However, nothing actually _calls_ these routines yet. All I've done in
this commit is to add them to 'struct game' and implement them for the
functions that need them.

Also, config_item has new fields, permitting each config option to
define a machine-readable identifying keyword as well as the
user-facing description. For options of type C_CHOICES, each choice
also has a keyword. These keyword fields are only defined at all by
the new get_prefs() function - they're left uninitialised in existing
uses of the dialog system. The idea is to use them when writing out
the user's preferences into a configuration file on disk, although I
haven't actually done any of that work in this commit.

--- a/blackbox.c
+++ b/blackbox.c
@@ -1556,6 +1556,7 @@
     free_game,
     true, solve_game,
     false, NULL, NULL, /* can_format_as_text_now, text_format */
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     encode_ui,
--- a/bridges.c
+++ b/bridges.c
@@ -3289,6 +3289,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/cube.c
+++ b/cube.c
@@ -1760,6 +1760,7 @@
     free_game,
     false, NULL, /* solve */
     false, NULL, NULL, /* can_format_as_text_now, text_format */
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/devel.but
+++ b/devel.but
@@ -332,24 +332,50 @@
 and begins another one (or closes the window); in particular,
 \q{Restart Game} does \e{not} destroy the \c{game_ui}.
 
-\c{game_ui} is useful for implementing user-interface state which is
-not part of \c{game_state}. Common examples are keyboard control
-(you wouldn't want to have to separately Undo through every cursor
-motion) and mouse dragging. See \k{writing-keyboard-cursor} and
-\k{writing-howto-dragging}, respectively, for more details.
+There are various things that you might store in \c{game_ui}, which
+are conceptually different from each other, but I haven't yet found a
+need to split them out into smaller sub-structures for different
+purposes:
 
-Another use for \c{game_ui} is to store highly persistent data such
-as the Mines death counter. This is conceptually rather different:
-where the Net cursor position was \e{not important enough} to
-preserve for the player to restore by Undo, the Mines death counter
-is \e{too important} to permit the player to revert by Undo!
+\dt Transient UI state:
 
-A final use for \c{game_ui} is to pass information to the redraw
-function about recent changes to the game state. This is used in
-Mines, for example, to indicate whether a requested \q{flash} should
-be a white flash for victory or a red flash for defeat; see
-\k{writing-flash-types}.
+\dd Storing a piece of UI state in \c{game_state} means that you can
+only update it by appending a move to the undo chain. Some UI state
+shouldn't really be treated this way. For example, if your puzzle has
+a keyboard-controlled cursor, you probably don't want every cursor
+movement to be an undoable action, because the history of where the
+cursor went just isn't interesting. More likely the cursor should just
+move freely, and the only undoable actions are the ones where you
+modify the element under the cursor. So you'd store the cursor
+position in \c{game_ui} rather than \c{game_state}. See
+\k{writing-keyboard-cursor} for more details.
 
+\lcont{ Another example of this is the state of an ongoing mouse drag.
+If there's an undoable action involved, it will probably occur when
+the drag is released. In between, you still need to store state that
+the redraw function will use to update the display \dash and that can
+live in \c{game_ui}. See \k{writing-howto-dragging} for more details
+of this. }
+
+\dt Persistent UI state:
+
+\dd An example of this is the counter of deaths in Mines or Inertia.
+This shouldn't be reverted by pressing Undo, for the opposite reason
+to the cursor position: the cursor position is too boring to store the
+history of, but the deaths counter is too \e{important}!
+
+\dt Information about recent changes to the game state:
+
+\dd This is used in Mines, for example, to indicate whether a
+requested \q{flash} should be a white flash for victory or a red flash
+for defeat; see \k{writing-flash-types}.
+
+\dt User preferences:
+
+\dd Any user preference about display or UI handled by
+\cw{get_prefs()} and \cw{set_prefs()} will need to live in
+\c{game_ui}, because that's the structure that those functions access.
+
 \H{backend-simple} Simple data in the back end
 
 In this section I begin to discuss each individual element in the
@@ -579,7 +605,8 @@
 return the updated array to \cw{custom_params()} (see
 \k{backend-custom-params}).
 
-The \cw{config_item} structure contains the following elements:
+The \cw{config_item} structure contains the following elements used by
+this function:
 
 \c const char *name;
 \c int type;
@@ -687,6 +714,57 @@
 
 If the game's \c{can_configure} flag is set to \cw{false}, this
 function is never called and can be \cw{NULL}.
+
+\S{backend-get-prefs} \cw{get_prefs()}
+
+\c config_item *(*get_prefs)(game_ui *ui);
+
+This function works very like \cw{configure()}, but instead of
+receiving a \c{game_params} and returning GUI elements describing the
+data in it, this function receives a \c{game_ui} and returns GUI
+elements describing any user preferences stored in that.
+
+This function should only deal with fields of \c{game_ui} that are
+user-settable preferences. In-game state like cursor position and
+mouse drags, or per-game state like death counters, are nothing to do
+with this function.
+
+If there are no user preferences, you can set both this function
+pointer and \c{set_prefs} to \cw{NULL}.
+
+In every \c{config_item} returned from this function, you must set an
+additional field beyond the ones described in \k{backend-configure}:
+
+\c const char *kw;
+
+This should be an identifying keyword for the user preference in
+question, suitable for use in configuration files. That means it
+should remain stable, even if the user-facing wording in the \c{name}
+field is reworded for clarity. If it doesn't stay stable, old
+configuration files will not be read correctly.
+
+For \c{config_item}s of type \cw{C_CHOICES}, you must also set an
+extra field in \c{u.choices}:
+
+\c const char *choicekws;
+
+This has the same structure as the \c{choicenames} field (a list of
+values delimited by the first character in the whole string), and it
+provides an identifying keyword for each individual choice in the
+list, in the same order as the entries of \c{choicenames}.
+
+\S{backend-set-prefs} \cw{set_prefs()}
+
+\c void (*set_prefs)(game_ui *ui, const config_item *cfg);
+
+This function is the counterpart to \cw{set_prefs()}, as
+\cw{custom_params()} is to \cw{configure()}. It receives an array of
+\c{config_item}s which was originally created by \cw{get_prefs()},
+with the controls' values updated from user input, and it should
+transcribe the new settings into the provided \c{game_ui}.
+
+If there are no user preferences, you can set both this function
+pointer and \c{get_prefs} to \cw{NULL}.
 
 \S{backend-validate-params} \cw{validate_params()}
 
--- a/dominosa.c
+++ b/dominosa.c
@@ -3442,6 +3442,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/fifteen.c
+++ b/fifteen.c
@@ -484,6 +484,30 @@
     return ui;
 }
 
+static config_item *get_prefs(game_ui *ui)
+{
+    config_item *ret;
+
+    ret = snewn(2, config_item);
+
+    ret[0].name = "Sense of arrow keys";
+    ret[0].kw = "arrow-semantics";
+    ret[0].type = C_CHOICES;
+    ret[0].u.choices.choicenames = ":Move the tile:Move the gap";
+    ret[0].u.choices.choicekws = ":tile:gap";
+    ret[0].u.choices.selected = ui->invert_cursor;
+
+    ret[1].name = NULL;
+    ret[1].type = C_END;
+
+    return ret;
+}
+
+static void set_prefs(game_ui *ui, const config_item *cfg)
+{
+    ui->invert_cursor = cfg[0].u.choices.selected;
+}
+
 static void free_ui(game_ui *ui)
 {
     sfree(ui);
@@ -1122,6 +1146,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    get_prefs, set_prefs,
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/filling.c
+++ b/filling.c
@@ -2174,6 +2174,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/flip.c
+++ b/flip.c
@@ -1337,6 +1337,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/flood.c
+++ b/flood.c
@@ -1365,6 +1365,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/galaxies.c
+++ b/galaxies.c
@@ -4129,6 +4129,7 @@
     true, solve_game,
 #endif
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/guess.c
+++ b/guess.c
@@ -1518,6 +1518,7 @@
     free_game,
     true, solve_game,
     false, NULL, NULL, /* can_format_as_text_now, text_format */
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     encode_ui,
--- a/inertia.c
+++ b/inertia.c
@@ -2231,6 +2231,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     encode_ui,
--- a/keen.c
+++ b/keen.c
@@ -2479,6 +2479,7 @@
     free_game,
     true, solve_game,
     false, NULL, NULL, /* can_format_as_text_now, text_format */
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/lightup.c
+++ b/lightup.c
@@ -1867,6 +1867,28 @@
     return ui;
 }
 
+static config_item *get_prefs(game_ui *ui)
+{
+    config_item *ret;
+
+    ret = snewn(2, config_item);
+
+    ret[0].name = "Draw non-light marks even when lit";
+    ret[0].kw = "show-lit-blobs";
+    ret[0].type = C_BOOLEAN;
+    ret[0].u.boolean.bval = ui->draw_blobs_when_lit;
+
+    ret[1].name = NULL;
+    ret[1].type = C_END;
+
+    return ret;
+}
+
+static void set_prefs(game_ui *ui, const config_item *cfg)
+{
+    ui->draw_blobs_when_lit = cfg[0].u.boolean.bval;
+}
+
 static void free_ui(game_ui *ui)
 {
     sfree(ui);
@@ -2353,6 +2375,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    get_prefs, set_prefs,
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/loopy.c
+++ b/loopy.c
@@ -931,6 +931,37 @@
     sfree(ui);
 }
 
+static config_item *get_prefs(game_ui *ui)
+{
+    config_item *ret;
+
+    ret = snewn(3, config_item);
+
+    ret[0].name = "Draw excluded grid lines faintly";
+    ret[0].kw = "draw-faint-lines";
+    ret[0].type = C_BOOLEAN;
+    ret[0].u.boolean.bval = ui->draw_faint_lines;
+
+    ret[1].name = "Auto-follow unique paths of edges";
+    ret[1].kw = "auto-follow";
+    ret[1].type = C_CHOICES;
+    ret[1].u.choices.choicenames =
+        ":No:Based on grid only:Based on grid and game state";
+    ret[1].u.choices.choicekws = ":off:fixed:adaptive";
+    ret[1].u.choices.selected = ui->autofollow;
+
+    ret[2].name = NULL;
+    ret[2].type = C_END;
+
+    return ret;
+}
+
+static void set_prefs(game_ui *ui, const config_item *cfg)
+{
+    ui->draw_faint_lines = cfg[0].u.boolean.bval;
+    ui->autofollow = cfg[1].u.choices.selected;
+}
+
 static void game_changed_state(game_ui *ui, const game_state *oldstate,
                                const game_state *newstate)
 {
@@ -3710,6 +3741,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    get_prefs, set_prefs,
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/magnets.c
+++ b/magnets.c
@@ -2459,6 +2459,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/map.c
+++ b/map.c
@@ -2333,6 +2333,30 @@
     return ui;
 }
 
+static config_item *get_prefs(game_ui *ui)
+{
+    config_item *ret;
+
+    ret = snewn(2, config_item);
+
+    ret[0].name = "Victory flash effect";
+    ret[0].kw = "flash-type";
+    ret[0].type = C_CHOICES;
+    ret[0].u.choices.choicenames = ":Cyclic:Each to white:All to white";
+    ret[0].u.choices.choicekws = ":cyclic:each-white:all-white";
+    ret[0].u.choices.selected = ui->flash_type;
+
+    ret[1].name = NULL;
+    ret[1].type = C_END;
+
+    return ret;
+}
+
+static void set_prefs(game_ui *ui, const config_item *cfg)
+{
+    ui->flash_type = cfg[0].u.choices.selected;
+}
+
 static void free_ui(game_ui *ui)
 {
     sfree(ui);
@@ -3291,6 +3315,7 @@
     free_game,
     true, solve_game,
     false, NULL, NULL, /* can_format_as_text_now, text_format */
+    get_prefs, set_prefs,
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/mines.c
+++ b/mines.c
@@ -3193,6 +3193,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     encode_ui,
--- a/mosaic.c
+++ b/mosaic.c
@@ -1609,6 +1609,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/net.c
+++ b/net.c
@@ -3270,6 +3270,7 @@
     free_game,
     true, solve_game,
     false, NULL, NULL, /* can_format_as_text_now, text_format */
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     encode_ui,
--- a/netslide.c
+++ b/netslide.c
@@ -1858,6 +1858,7 @@
     free_game,
     true, solve_game,
     false, NULL, NULL, /* can_format_as_text_now, text_format */
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/nullgame.c
+++ b/nullgame.c
@@ -237,6 +237,7 @@
     free_game,
     false, NULL, /* solve */
     false, NULL, NULL, /* can_format_as_text_now, text_format */
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/palisade.c
+++ b/palisade.c
@@ -1380,6 +1380,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/pattern.c
+++ b/pattern.c
@@ -2078,6 +2078,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/pearl.c
+++ b/pearl.c
@@ -1916,6 +1916,30 @@
     sfree(ui);
 }
 
+static config_item *get_prefs(game_ui *ui)
+{
+    config_item *ret;
+
+    ret = snewn(2, config_item);
+
+    ret[0].name = "Puzzle appearance";
+    ret[0].kw = "appearance";
+    ret[0].type = C_CHOICES;
+    ret[0].u.choices.choicenames = ":Traditional:Loopy-style";
+    ret[0].u.choices.choicekws = ":traditional:loopy";
+    ret[0].u.choices.selected = ui->gui_style;
+
+    ret[1].name = NULL;
+    ret[1].type = C_END;
+
+    return ret;
+}
+
+static void set_prefs(game_ui *ui, const config_item *cfg)
+{
+    ui->gui_style = cfg[0].u.choices.selected;
+}
+
 static void game_changed_state(game_ui *ui, const game_state *oldstate,
                                const game_state *newstate)
 {
@@ -2744,6 +2768,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    get_prefs, set_prefs,
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/pegs.c
+++ b/pegs.c
@@ -1361,6 +1361,7 @@
     free_game,
     false, NULL, /* solve */
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/puzzles.h
+++ b/puzzles.h
@@ -127,8 +127,13 @@
  */
 enum { C_STRING, C_CHOICES, C_BOOLEAN, C_END };
 struct config_item {
-    /* Not dynamically allocated */
+    /* Not dynamically allocated: the GUI display name for the option */
     const char *name;
+    /* Not dynamically allocated: the keyword identifier for the
+     * option. Only examined in the case where this structure is being
+     * used for options that appear in config files, i.e. the
+     * get_prefs method fills this in but configure does not. */
+    const char *kw;
     /* Value from the above C_* enum */
     int type;
     union {
@@ -146,6 +151,13 @@
              */
             const char *choicenames;
             /*
+             * choicekws is non-NULL, not dynamically allocated, and
+             * contains a parallel list of keyword strings used to
+             * represent the enumeration in config files. As with 'kw'
+             * above, this is only expected to be set by get_prefs.
+             */
+            const char *choicekws;
+            /*
              * Indicates the chosen index from the options in
              * choicenames. In the above example, 0==Foo, 1==Bar and
              * 2==Baz.
@@ -676,6 +688,8 @@
     bool can_format_as_text_ever;
     bool (*can_format_as_text_now)(const game_params *params);
     char *(*text_format)(const game_state *state);
+    config_item *(*get_prefs)(game_ui *ui);
+    void (*set_prefs)(game_ui *ui, const config_item *cfg);
     game_ui *(*new_ui)(const game_state *state);
     void (*free_ui)(game_ui *ui);
     char *(*encode_ui)(const game_ui *ui);
--- a/range.c
+++ b/range.c
@@ -1271,6 +1271,31 @@
     return ui;
 }
 
+static config_item *get_prefs(game_ui *ui)
+{
+    config_item *ret;
+
+    ret = snewn(2, config_item);
+
+    ret[0].name = "Mouse button order";
+    ret[0].kw = "left-mouse-button";
+    ret[0].type = C_CHOICES;
+    ret[0].u.choices.choicenames =
+        ":Left to fill, right to dot:Left to dot, right to fill";
+    ret[0].u.choices.choicekws = ":fill:dot";
+    ret[0].u.choices.selected = ui->swap_buttons;
+
+    ret[1].name = NULL;
+    ret[1].type = C_END;
+
+    return ret;
+}
+
+static void set_prefs(game_ui *ui, const config_item *cfg)
+{
+    ui->swap_buttons = cfg[0].u.choices.selected;
+}
+
 static void free_ui(game_ui *ui)
 {
     sfree(ui);
@@ -1834,6 +1859,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    get_prefs, set_prefs,
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/rect.c
+++ b/rect.c
@@ -2995,6 +2995,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/samegame.c
+++ b/samegame.c
@@ -1666,6 +1666,7 @@
     free_game,
     false, NULL, /* solve */
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/signpost.c
+++ b/signpost.c
@@ -1437,6 +1437,30 @@
     sfree(ui);
 }
 
+static config_item *get_prefs(game_ui *ui)
+{
+    config_item *ret;
+
+    ret = snewn(2, config_item);
+
+    ret[0].name = "Victory rotation effect";
+    ret[0].kw = "flash-type";
+    ret[0].type = C_CHOICES;
+    ret[0].u.choices.choicenames = ":Unidirectional:Meshing gears";
+    ret[0].u.choices.choicekws = ":unidirectional:gears";
+    ret[0].u.choices.selected = ui->gear_mode;
+
+    ret[1].name = NULL;
+    ret[1].type = C_END;
+
+    return ret;
+}
+
+static void set_prefs(game_ui *ui, const config_item *cfg)
+{
+    ui->gear_mode = cfg[0].u.choices.selected;
+}
+
 static void game_changed_state(game_ui *ui, const game_state *oldstate,
                                const game_state *newstate)
 {
@@ -2292,6 +2316,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    get_prefs, set_prefs,
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/singles.c
+++ b/singles.c
@@ -1852,6 +1852,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/sixteen.c
+++ b/sixteen.c
@@ -1191,6 +1191,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/slant.c
+++ b/slant.c
@@ -1617,6 +1617,30 @@
     return ui;
 }
 
+static config_item *get_prefs(game_ui *ui)
+{
+    config_item *ret;
+
+    ret = snewn(2, config_item);
+
+    ret[0].name = "Mouse button order";
+    ret[0].kw = "left-button";
+    ret[0].type = C_CHOICES;
+    ret[0].u.choices.choicenames = ":Left \\, right /:Left /, right \\";
+    ret[0].u.choices.choicekws = ":\\:/";
+    ret[0].u.choices.selected = ui->swap_buttons;
+
+    ret[1].name = NULL;
+    ret[1].type = C_END;
+
+    return ret;
+}
+
+static void set_prefs(game_ui *ui, const config_item *cfg)
+{
+    ui->swap_buttons = cfg[0].u.choices.selected;
+}
+
 static void free_ui(game_ui *ui)
 {
     sfree(ui);
@@ -2204,6 +2228,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    get_prefs, set_prefs,
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/solo.c
+++ b/solo.c
@@ -5630,6 +5630,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/tents.c
+++ b/tents.c
@@ -2649,6 +2649,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/towers.c
+++ b/towers.c
@@ -1209,6 +1209,30 @@
     sfree(ui);
 }
 
+static config_item *get_prefs(game_ui *ui)
+{
+    config_item *ret;
+
+    ret = snewn(2, config_item);
+
+    ret[0].name = "Puzzle appearance";
+    ret[0].kw = "appearance";
+    ret[0].type = C_CHOICES;
+    ret[0].u.choices.choicenames = ":2D:3D";
+    ret[0].u.choices.choicekws = ":2d:3d";
+    ret[0].u.choices.selected = ui->three_d;
+
+    ret[1].name = NULL;
+    ret[1].type = C_END;
+
+    return ret;
+}
+
+static void set_prefs(game_ui *ui, const config_item *cfg)
+{
+    ui->three_d = cfg[0].u.choices.selected;
+}
+
 static void game_changed_state(game_ui *ui, const game_state *oldstate,
                                const game_state *newstate)
 {
@@ -2084,6 +2108,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    get_prefs, set_prefs,
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/tracks.c
+++ b/tracks.c
@@ -3047,6 +3047,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/twiddle.c
+++ b/twiddle.c
@@ -1305,6 +1305,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/undead.c
+++ b/undead.c
@@ -2784,6 +2784,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/unequal.c
+++ b/unequal.c
@@ -2154,6 +2154,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/unfinished/group.c
+++ b/unfinished/group.c
@@ -2323,6 +2323,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/unfinished/separate.c
+++ b/unfinished/separate.c
@@ -835,6 +835,7 @@
     free_game,
     false, solve_game,
     false, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/unfinished/slide.c
+++ b/unfinished/slide.c
@@ -2328,6 +2328,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/unfinished/sokoban.c
+++ b/unfinished/sokoban.c
@@ -1450,6 +1450,7 @@
     free_game,
     false, solve_game,
     false, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/unruly.c
+++ b/unruly.c
@@ -2030,6 +2030,7 @@
     free_game,
     true, solve_game,
     true, game_can_format_as_text_now, game_text_format,
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */
--- a/untangle.c
+++ b/untangle.c
@@ -1454,6 +1454,7 @@
     free_game,
     true, solve_game,
     false, NULL, NULL, /* can_format_as_text_now, text_format */
+    NULL, NULL, /* get_prefs, set_prefs */
     new_ui,
     free_ui,
     NULL, /* encode_ui */