X-Git-Url: https://git.stderr.nl/gitweb?a=blobdiff_plain;f=interpreters%2Fnitfol%2Fundo.c;fp=interpreters%2Fnitfol%2Fundo.c;h=3f2cb225a92069f1ef6ca8b723ce386685407d31;hb=c98ccb87aa2581cbcd0458682727274b6e9a8cf7;hp=0000000000000000000000000000000000000000;hpb=a1689380dcfe59c99e70da8c311ace071d113ce5;p=rodin%2Fchimara.git diff --git a/interpreters/nitfol/undo.c b/interpreters/nitfol/undo.c new file mode 100644 index 0000000..3f2cb22 --- /dev/null +++ b/interpreters/nitfol/undo.c @@ -0,0 +1,386 @@ +/* Nitfol - z-machine interpreter using Glk for output. + Copyright (C) 1999 Evin Robertson + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA. + + The author can be reached at nitfol@deja.com +*/ +#include "nitfol.h" + + +/* Early versions just used quetzal format for the undo slot, which let + me just call the same function, but made it impossible to add undo + capability to games which don't natively support it, and was too expensive + to provide infinite undo/redo. + + Now I use a linked list containing a memory delta and stack stored in + quetzal format. We take the delta between consecutive turns. This makes + things smaller as there should be fewer differences between turns than + from beginning to end of a story. + + Since there is very little difference between turns, I use quetzal + format for the delta with a twist. If there are runs of more than + 127 unchanged bytes, I store the length in a 15 bit number, ala UTF-8. + This saves over a hundred bytes each turn (depending on game size and + arrangement). + + Here is some data for advent (note that the usages are mostly for the + the previous turn, since the save_undo is called shortly after each input): + +Command: Overhead Dynamic Stack Total + Memory +enter 28 + 20 + 172 = 220 +take all 28 + 435 + 172 = 635 +x stream 28 + 266 + 172 = 466 +exit 28 + 132 + 172 = 332 +s 28 + 120 + 172 = 320 +verbose 28 + 114 + 172 = 314 +d 28 + 64 + 172 = 264 +s 28 + 90 + 172 = 290 +unlock grate with keys 28 + 127 + 172 = 327 +open grate. down. 28 + 202 + 172 = 402 +help 28 + 199 + 172 = 399 + + Overhead doesn't count alignment and malloc overhead. Assume another 20 + bytes there. + + At approx 380 bytes per turn, I can store over 2500 turns in a meg of + memory, enough for a longish game. + + Note that stack size (and contents mostly) stay the same between calls + since save_undo is almost always called from the same place. I should take + advantage of this somehow. (another hundred bytes to save, another 1000 + turns per meg...) + +*/ + + +static zbyte *prevstate = NULL; + +typedef struct move_difference move_difference; + +struct move_difference { + move_difference *next; + + zbyte *delta; /* Encoded like quetzal mixed with UTF-8 */ + glui32 deltalength; + + offset PC; + offset oldPC; + BOOL PC_in_instruction; + + zbyte *stackchunk; /* Quetzal encoded */ + glui32 stacklength; +}; + +static move_difference *movelist = NULL; +static int move_index; + + +void init_undo(void) +{ + kill_undo(); + + prevstate = (zbyte *) n_malloc(dynamic_size); + n_memcpy(prevstate, z_memory, dynamic_size); +} + +/* Frees old undo slots if possible in order to reduce memory consumption. + Will never free the most recent @save_undo */ +BOOL free_undo(void) +{ + move_difference *p, *g = NULL; + for(p = movelist; p; p=p->next) + if(p->next) + g = p; + if(g == NULL) + return FALSE; + + n_free(g->next->delta); + n_free(g->next->stackchunk); + n_free(g->next); + g->next = NULL; + return TRUE; +} + + +BOOL saveundo(BOOL in_instruction) +{ + move_difference newdiff; + strid_t stack; + stream_result_t poo; + + if(!allow_saveundo) + return TRUE; + + /* In games which provide @save_undo, we will have already issued a faked + saveundo before the first @save_undo hits, since there hadn't been any + @save_undo before the first read line. So when this happens, wipe the + fake saveundo in favor of the real one */ + if(in_instruction && movelist && !movelist->next + && !movelist->PC_in_instruction) + init_undo(); + + + if(!quetzal_diff(z_memory, prevstate, dynamic_size, &newdiff.delta, + &newdiff.deltalength, TRUE)) + return FALSE; + +#ifdef PARANOID + { + char *newmem = (char *) n_malloc(dynamic_size); + n_memcpy(newmem, prevstate, dynamic_size); + quetzal_undiff(newmem, dynamic_size, newdiff.delta, + newdiff.deltalength, TRUE); + if(n_memcmp(z_memory, newmem, dynamic_size)) { + n_show_error(E_SAVE, "save doesn't match itself", 0); + } + n_free(newmem); + } +#endif + + newdiff.PC = PC; + newdiff.oldPC = oldPC; + + newdiff.PC_in_instruction = in_instruction; + newdiff.stacklength = get_quetzal_stack_size(); + newdiff.stackchunk = (zbyte *) n_malloc(newdiff.stacklength); + stack = glk_stream_open_memory((char *) newdiff.stackchunk, + newdiff.stacklength, filemode_Write, 0); + if(!stack) { + n_free(newdiff.delta); + n_free(newdiff.stackchunk); + return FALSE; + } + if(!quetzal_stack_save(stack)) { + glk_stream_close(stack, NULL); + n_free(newdiff.delta); + n_free(newdiff.stackchunk); + return FALSE; + } + glk_stream_close(stack, &poo); + if(poo.writecount != newdiff.stacklength) { + n_show_error(E_SAVE, "incorrect stack size assessment", poo.writecount); + n_free(newdiff.delta); + n_free(newdiff.stackchunk); + return FALSE; + } + + while(move_index-- > 0) { + n_free(movelist->delta); + n_free(movelist->stackchunk); + LEremove(movelist); + } + LEadd(movelist, newdiff); + move_index++; + n_memcpy(prevstate, z_memory, dynamic_size); + + has_done_save_undo = TRUE; + return TRUE; +} + + +BOOL restoreundo(void) +{ + strid_t stack; + int i; + glui32 wid, hei; + move_difference *p = movelist; + + if(!p || move_index < 0) + return FALSE; + + for(i = 0; i < move_index; i++) { + p=p->next; + if(!p) + return FALSE; + } + move_index++; + + n_memcpy(z_memory, prevstate, dynamic_size); + + quetzal_undiff(prevstate, dynamic_size, p->delta, p->deltalength, TRUE); + + stack = glk_stream_open_memory((char *) p->stackchunk, p->stacklength, + filemode_Read, 0); + + quetzal_stack_restore(stack, p->stacklength); + glk_stream_close(stack, NULL); + + if(p->PC_in_instruction) { + PC = p->PC; + mop_store_result(2); + false_undo = FALSE; + } else { + PC = p->oldPC; + false_undo = TRUE; + } + has_done_save_undo = TRUE; + + z_find_size(&wid, &hei); + set_header(wid, hei); + return TRUE; +} + + +/* Just like restoreundo, but the opposite ;) + The idea is to go to the @save_undo location, but return 0 instead of 2 + so the game thinks it just successfully saved the game. For games which + don't contain @save_undo, jumps to right after the @read instruction. */ +BOOL restoreredo(void) +{ + strid_t stack; + int i; + glui32 wid, hei; + stream_result_t poo; + move_difference *p = movelist; + + if(!p || move_index <= 0) + return FALSE; + + move_index--; + for(i = 0; i < move_index; i++) { + p=p->next; + if(!p) + return FALSE; + } + + quetzal_undiff(prevstate, dynamic_size, p->delta, p->deltalength, TRUE); + + n_memcpy(z_memory, prevstate, dynamic_size); + + stack = glk_stream_open_memory((char *) p->stackchunk, p->stacklength, + filemode_Read, 0); + + quetzal_stack_restore(stack, p->stacklength); + glk_stream_close(stack, &poo); + + if(poo.readcount != p->stacklength) { + n_show_error(E_SAVE, "incorrect stack size assessment", poo.readcount); + return FALSE; + } + + if(p->PC_in_instruction) { + PC = p->PC; + mop_store_result(0); + false_undo = FALSE; + } else { + PC = p->PC; + false_undo = FALSE; + } + has_done_save_undo = TRUE; + + z_find_size(&wid, &hei); + set_header(wid, hei); + return TRUE; +} + + +#ifdef DEBUGGING + +/* For automapping, we want the saveundo/restoreundo to be as fast as possible + and we also don't want it clobbering the redo capabilities, so give it + separate fast_saveundo and fast_restoreundo. Doesn't need multiple undo + or redo +*/ + +struct fast_undoslot { + zbyte *z_mem; + glui32 z_memsize; + offset PC; + zbyte *stackchunk; /* Quetzal encoded */ + glui32 stackchunksize; + glui32 stacklength; +}; + + +static struct fast_undoslot automap_undoslot = { NULL, 0, 0, NULL, 0, 0 }; + +BOOL fast_saveundo(void) +{ + strid_t stack; + glui32 stack_size; + /* Avoids realloc() in hopes that'll make it a teensy bit faster */ + if(automap_undoslot.z_memsize < dynamic_size) { + n_free(automap_undoslot.z_mem); + automap_undoslot.z_mem = n_malloc(dynamic_size); + automap_undoslot.z_memsize = dynamic_size; + } + n_memcpy(automap_undoslot.z_mem, z_memory, dynamic_size); + + automap_undoslot.PC = oldPC; + + automap_undoslot.stacklength = stack_size = get_quetzal_stack_size(); + if(automap_undoslot.stackchunksize < stack_size) { + free(automap_undoslot.stackchunk); + automap_undoslot.stackchunk = (zbyte *) n_malloc(stack_size); + automap_undoslot.stackchunksize = stack_size; + } + + stack = glk_stream_open_memory((char *) automap_undoslot.stackchunk, + automap_undoslot.stacklength, + filemode_Write, 0); + if(!stack) + return FALSE; + if(!quetzal_stack_save(stack)) { + glk_stream_close(stack, NULL); + return FALSE; + } + glk_stream_close(stack, NULL); + return TRUE; +} + + +BOOL fast_restoreundo(void) +{ + strid_t stack; + n_memcpy(z_memory, automap_undoslot.z_mem, dynamic_size); + PC = automap_undoslot.PC; + + stack = glk_stream_open_memory((char *) automap_undoslot.stackchunk, + automap_undoslot.stacklength, + filemode_Read, 0); + + quetzal_stack_restore(stack, automap_undoslot.stacklength); + glk_stream_close(stack, NULL); + return TRUE; +} + +#endif + + +void kill_undo(void) +{ + n_free(prevstate); + prevstate = 0; + + while(movelist) { + n_free(movelist->delta); + n_free(movelist->stackchunk); + LEremove(movelist); + } + move_index = 0; + +#ifdef DEBUGGING + n_free(automap_undoslot.z_mem); + n_free(automap_undoslot.stackchunk); + automap_undoslot.z_mem = NULL; + automap_undoslot.z_memsize = 0; + automap_undoslot.PC = 0; + automap_undoslot.stackchunk = NULL; + automap_undoslot.stackchunksize = 0; + automap_undoslot.stacklength = 0; +#endif +}