ref: db313b3948d27244dd7c34c2609c66d6204d8931
parent: 9f6114e27236105d64e23e063a17db84389059ba
author: Simon Tatham <anakin@pobox.com>
date: Sun Oct 1 06:22:35 EDT 2017
Forbid undo of new-game if it would change the params. 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.)
--- a/midend.c
+++ b/midend.c
@@ -542,6 +542,62 @@
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 @@
} 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;
}