diff --git a/midend.c b/midend.c index fde2833..ca0ec02 100644 --- a/midend.c +++ b/midend.c @@ -68,7 +68,7 @@ struct midend { int nstates, statesize, statepos; struct midend_state_entry *states; - struct midend_serialise_buf newgame_undo; + struct midend_serialise_buf newgame_undo, newgame_redo; game_params *params, *curparams; game_drawstate *drawstate; @@ -164,6 +164,8 @@ midend *midend_new(frontend *fe, const game *ourgame, me->states = NULL; me->newgame_undo.buf = NULL; me->newgame_undo.size = me->newgame_undo.len = 0; + me->newgame_redo.buf = NULL; + me->newgame_redo.size = me->newgame_redo.len = 0; me->params = ourgame->default_params(); me->game_id_change_notify_function = NULL; me->game_id_change_notify_ctx = NULL; @@ -225,6 +227,7 @@ static void midend_purge_states(midend *me) if (me->states[me->nstates].movestr) sfree(me->states[me->nstates].movestr); } + me->newgame_redo.len = 0; } static void midend_free_game(midend *me) @@ -262,6 +265,7 @@ void midend_free(midend *me) drawing_free(me->drawing); random_free(me->random); sfree(me->newgame_undo.buf); + sfree(me->newgame_redo.buf); sfree(me->states); sfree(me->desc); sfree(me->privdesc); @@ -418,6 +422,7 @@ void midend_new_game(midend *me) * and just confuse us into thinking we had something to undo * to. */ + midend_purge_states(me); midend_serialise(me, newgame_serialise_write, &me->newgame_undo); } @@ -540,7 +545,7 @@ int midend_can_undo(midend *me) int midend_can_redo(midend *me) { - return (me->statepos < me->nstates); + return (me->statepos < me->nstates || me->newgame_redo.len); } struct newgame_undo_deserialise_read_ctx { @@ -627,9 +632,19 @@ static int midend_undo(midend *me) me->dir = -1; return 1; } 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; + struct midend_serialise_buf serbuf; + + /* + * Serialise the current game so that you can later redo past + * this undo. Once we're committed to the undo actually + * happening, we'll copy this data into place. + */ + serbuf.buf = NULL; + serbuf.len = serbuf.size = 0; + midend_serialise(me, newgame_serialise_write, &serbuf); + rctx.ser = &me->newgame_undo; rctx.len = me->newgame_undo.len; /* copy for reentrancy safety */ rctx.pos = 0; @@ -644,6 +659,7 @@ static int midend_undo(midend *me) * contain the dummy error message generated by our check * function, which we ignore.) */ + sfree(serbuf.buf); return 0; } else { /* @@ -654,6 +670,22 @@ static int midend_undo(midend *me) * replaced by the wrong file, etc., by user error. */ assert(!deserialise_error); + + /* + * Clear the old newgame_undo serialisation, so that we + * don't try to undo past the beginning of the game we've + * just gone back to and end up at the front of it again. + */ + me->newgame_undo.len = 0; + + /* + * Copy the serialisation of the game we've just left into + * the midend so that we can redo back into it later. + */ + me->newgame_redo.len = 0; + newgame_serialise_write(&me->newgame_redo, serbuf.buf, serbuf.len); + + sfree(serbuf.buf); return 1; } } else @@ -662,6 +694,8 @@ static int midend_undo(midend *me) static int midend_redo(midend *me) { + const char *deserialise_error; + if (me->statepos < me->nstates) { if (me->ui) me->ourgame->changed_state(me->ui, @@ -670,6 +704,63 @@ static int midend_redo(midend *me) me->statepos++; me->dir = +1; return 1; + } else if (me->newgame_redo.len) { + struct newgame_undo_deserialise_read_ctx rctx; + struct newgame_undo_deserialise_check_ctx cctx; + struct midend_serialise_buf serbuf; + + /* + * Serialise the current game so that you can later undo past + * this redo. Once we're committed to the undo actually + * happening, we'll copy this data into place. + */ + serbuf.buf = NULL; + serbuf.len = serbuf.size = 0; + midend_serialise(me, newgame_serialise_write, &serbuf); + + rctx.ser = &me->newgame_redo; + rctx.len = me->newgame_redo.len; /* copy for reentrancy safety */ + rctx.pos = 0; + 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.) + */ + sfree(serbuf.buf); + 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); + + /* + * Clear the old newgame_redo serialisation, so that we + * don't try to redo past the end of the game we've just + * come into and end up at the back of it again. + */ + me->newgame_redo.len = 0; + + /* + * Copy the serialisation of the game we've just left into + * the midend so that we can undo back into it later. + */ + me->newgame_undo.len = 0; + newgame_serialise_write(&me->newgame_undo, serbuf.buf, serbuf.len); + + sfree(serbuf.buf); + return 1; + } } else return 0; } @@ -2226,13 +2317,14 @@ static const char *midend_deserialise_internal( me->statepos = data.statepos; /* - * Don't save the "new game undo" state. So "new game" twice or + * Don't save the "new game undo/redo" state. So "new game" twice or * (in some environments) switching away and back, will make a * "new game" irreversible. Maybe in the future we will have a * more sophisticated way to decide when to discard the previous * game state. */ me->newgame_undo.len = 0; + me->newgame_redo.len = 0; { game_params *tmp;