chiark / gitweb /
Forbid undo of new-game if it would change the params.
authorSimon Tatham <anakin@pobox.com>
Sun, 1 Oct 2017 09:22:35 +0000 (10:22 +0100)
committerSimon Tatham <anakin@pobox.com>
Sun, 1 Oct 2017 09:38:30 +0000 (10:38 +0100)
The newgame_undo data was being saved on every call to
midend_new_game, including the one just after midend_set_params when a
new puzzle preset was selected. So you could select a new preset from
the menu, and then press 'u', and the midend would _try_ to undo that
operation and restore the previous game with a different set of
parameters.

This would do the wrong thing in the front end, because front ends in
general will not be expecting that a change of game parameters might
result from an arbitrary keyboard event - they won't be expecting to
have to call the function that moves the highlight in the game-type
menu, for example, and they _certainly_ won't be expecting that a
window resize might be necessary in response to a random keystroke.

One possible response would be to fix all the front ends so that they
_are_ prepared for either of those consequences of a keystroke event,
and then it would be possible to undo not only the New Game menu
option and the 'n' key but also undo any selection of a preset from
the game-type menu, or even a full re-customisation of the game
settings. But that would be quite an upheaval even in _my_ front end
collection, and also probably be awkward for downstream front ends, so
until I'm convinced of the value of going to all the effort, the
simpler approach is just to disallow undoing a new game in those
situations.

(This does mean that re-selecting the _already active_ game preset
from the type menu will be treated as an undoable new-game event,
which I think is an acceptable UI oddity.)

midend.c

index 5dd896f5b66fb9a162c186660e1248754eb69133..2f09bb528c7a9a1fb38e4262f21867b5f782326d 100644 (file)
--- a/midend.c
+++ b/midend.c
@@ -542,6 +542,62 @@ static int newgame_undo_deserialise_read(void *ctx, void *buf, int len)
     return use;
 }
 
+struct newgame_undo_deserialise_check_ctx {
+    int refused;
+};
+
+static char *newgame_undo_deserialise_check(
+    void *vctx, midend *me, const struct deserialise_data *data)
+{
+    struct newgame_undo_deserialise_check_ctx *ctx =
+        (struct newgame_undo_deserialise_check_ctx *)vctx;
+    char *old, *new;
+
+    /*
+     * Undoing a New Game operation is only permitted if it doesn't
+     * change the game parameters. The point of having the ability at
+     * all is to recover from the momentary finger error of having hit
+     * the 'n' key (perhaps in place of some other nearby key), or hit
+     * the New Game menu item by mistake when aiming for the adjacent
+     * Restart; in both those situations, the game params are the same
+     * before and after the new-game operation.
+     *
+     * In principle, we could generalise this so that _any_ call to
+     * midend_new_game could be undone, but that would need all front
+     * ends to be alert to the possibility that any keystroke passed
+     * to midend_process_key might (if it turns out to have been one
+     * of the synonyms for undo, which the frontend doesn't
+     * necessarily check for) have various knock-on effects like
+     * needing to select a different preset in the game type menu, or
+     * even resizing the window. At least for the moment, it's easier
+     * not to do that, and to simply disallow any newgame-undo that is
+     * disruptive in either of those ways.
+     *
+     * We check both params and cparams, to be as safe as possible.
+     */
+
+    old = me->ourgame->encode_params(me->params, TRUE);
+    new = me->ourgame->encode_params(data->params, TRUE);
+    if (strcmp(old, new)) {
+        /* Set a flag to distinguish this deserialise failure
+         * from one due to faulty decoding */
+        ctx->refused = TRUE;
+        return "Undoing this new-game operation would change params";
+    }
+
+    old = me->ourgame->encode_params(me->curparams, TRUE);
+    new = me->ourgame->encode_params(data->cparams, TRUE);
+    if (strcmp(old, new)) {
+        ctx->refused = TRUE;
+        return "Undoing this new-game operation would change params";
+    }
+
+    /*
+     * Otherwise, fine, go ahead.
+     */
+    return NULL;
+}
+
 static int midend_undo(midend *me)
 {
     char *deserialise_error;
@@ -557,13 +613,33 @@ static int midend_undo(midend *me)
     } else if (me->newgame_undo_len) {
        /* This undo cannot be undone with redo */
        struct newgame_undo_deserialise_read_ctx rctx;
+       struct newgame_undo_deserialise_check_ctx cctx;
        rctx.me = me;
        rctx.len = me->newgame_undo_len; /* copy for reentrancy safety */
        rctx.pos = 0;
-        deserialise_error =
-           midend_deserialise(me, newgame_undo_deserialise_read, &rctx);
-       assert(!deserialise_error);
-       return 1;
+        cctx.refused = FALSE;
+        deserialise_error = midend_deserialise_internal(
+            me, newgame_undo_deserialise_read, &rctx,
+            newgame_undo_deserialise_check, &cctx);
+        if (cctx.refused) {
+            /*
+             * Our post-deserialisation check shows that we can't use
+             * this saved game after all. (deserialise_error will
+             * contain the dummy error message generated by our check
+             * function, which we ignore.)
+             */
+            return 0;
+        } else {
+            /*
+             * There should never be any _other_ deserialisation
+             * error, because this serialised data has been held in
+             * our memory since it was created, and hasn't had any
+             * opportunity to be corrupted on disk, accidentally
+             * replaced by the wrong file, etc., by user error.
+             */
+            assert(!deserialise_error);
+            return 1;
+        }
     } else
         return 0;
 }