1 /* Nitfol - z-machine interpreter using Glk for output.
2 Copyright (C) 1999 Evin Robertson
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
18 The author can be reached at nitfol@deja.com
23 /* Early versions just used quetzal format for the undo slot, which let
24 me just call the same function, but made it impossible to add undo
25 capability to games which don't natively support it, and was too expensive
26 to provide infinite undo/redo.
28 Now I use a linked list containing a memory delta and stack stored in
29 quetzal format. We take the delta between consecutive turns. This makes
30 things smaller as there should be fewer differences between turns than
31 from beginning to end of a story.
33 Since there is very little difference between turns, I use quetzal
34 format for the delta with a twist. If there are runs of more than
35 127 unchanged bytes, I store the length in a 15 bit number, ala UTF-8.
36 This saves over a hundred bytes each turn (depending on game size and
39 Here is some data for advent (note that the usages are mostly for the
40 the previous turn, since the save_undo is called shortly after each input):
42 Command: Overhead Dynamic Stack Total
44 enter 28 + 20 + 172 = 220
45 take all 28 + 435 + 172 = 635
46 x stream 28 + 266 + 172 = 466
47 exit 28 + 132 + 172 = 332
48 s 28 + 120 + 172 = 320
49 verbose 28 + 114 + 172 = 314
52 unlock grate with keys 28 + 127 + 172 = 327
53 open grate. down. 28 + 202 + 172 = 402
54 help 28 + 199 + 172 = 399
56 Overhead doesn't count alignment and malloc overhead. Assume another 20
59 At approx 380 bytes per turn, I can store over 2500 turns in a meg of
60 memory, enough for a longish game.
62 Note that stack size (and contents mostly) stay the same between calls
63 since save_undo is almost always called from the same place. I should take
64 advantage of this somehow. (another hundred bytes to save, another 1000
70 static zbyte *prevstate = NULL;
72 typedef struct move_difference move_difference;
74 struct move_difference {
75 move_difference *next;
77 zbyte *delta; /* Encoded like quetzal mixed with UTF-8 */
82 BOOL PC_in_instruction;
84 zbyte *stackchunk; /* Quetzal encoded */
88 static move_difference *movelist = NULL;
89 static int move_index;
96 prevstate = (zbyte *) n_malloc(dynamic_size);
97 n_memcpy(prevstate, z_memory, dynamic_size);
100 /* Frees old undo slots if possible in order to reduce memory consumption.
101 Will never free the most recent @save_undo */
104 move_difference *p, *g = NULL;
105 for(p = movelist; p; p=p->next)
111 n_free(g->next->delta);
112 n_free(g->next->stackchunk);
119 BOOL saveundo(BOOL in_instruction)
121 move_difference newdiff;
128 /* In games which provide @save_undo, we will have already issued a faked
129 saveundo before the first @save_undo hits, since there hadn't been any
130 @save_undo before the first read line. So when this happens, wipe the
131 fake saveundo in favor of the real one */
132 if(in_instruction && movelist && !movelist->next
133 && !movelist->PC_in_instruction)
137 if(!quetzal_diff(z_memory, prevstate, dynamic_size, &newdiff.delta,
138 &newdiff.deltalength, TRUE))
143 char *newmem = (char *) n_malloc(dynamic_size);
144 n_memcpy(newmem, prevstate, dynamic_size);
145 quetzal_undiff(newmem, dynamic_size, newdiff.delta,
146 newdiff.deltalength, TRUE);
147 if(n_memcmp(z_memory, newmem, dynamic_size)) {
148 n_show_error(E_SAVE, "save doesn't match itself", 0);
155 newdiff.oldPC = oldPC;
157 newdiff.PC_in_instruction = in_instruction;
158 newdiff.stacklength = get_quetzal_stack_size();
159 newdiff.stackchunk = (zbyte *) n_malloc(newdiff.stacklength);
160 stack = glk_stream_open_memory((char *) newdiff.stackchunk,
161 newdiff.stacklength, filemode_Write, 0);
163 n_free(newdiff.delta);
164 n_free(newdiff.stackchunk);
167 if(!quetzal_stack_save(stack)) {
168 glk_stream_close(stack, NULL);
169 n_free(newdiff.delta);
170 n_free(newdiff.stackchunk);
173 glk_stream_close(stack, &poo);
174 if(poo.writecount != newdiff.stacklength) {
175 n_show_error(E_SAVE, "incorrect stack size assessment", poo.writecount);
176 n_free(newdiff.delta);
177 n_free(newdiff.stackchunk);
181 while(move_index-- > 0) {
182 n_free(movelist->delta);
183 n_free(movelist->stackchunk);
186 LEadd(movelist, newdiff);
188 n_memcpy(prevstate, z_memory, dynamic_size);
190 has_done_save_undo = TRUE;
195 BOOL restoreundo(void)
200 move_difference *p = movelist;
202 if(!p || move_index < 0)
205 for(i = 0; i < move_index; i++) {
212 n_memcpy(z_memory, prevstate, dynamic_size);
214 quetzal_undiff(prevstate, dynamic_size, p->delta, p->deltalength, TRUE);
216 stack = glk_stream_open_memory((char *) p->stackchunk, p->stacklength,
219 quetzal_stack_restore(stack, p->stacklength);
220 glk_stream_close(stack, NULL);
222 if(p->PC_in_instruction) {
230 has_done_save_undo = TRUE;
232 z_find_size(&wid, &hei);
233 set_header(wid, hei);
238 /* Just like restoreundo, but the opposite ;)
239 The idea is to go to the @save_undo location, but return 0 instead of 2
240 so the game thinks it just successfully saved the game. For games which
241 don't contain @save_undo, jumps to right after the @read instruction. */
242 BOOL restoreredo(void)
248 move_difference *p = movelist;
250 if(!p || move_index <= 0)
254 for(i = 0; i < move_index; i++) {
260 quetzal_undiff(prevstate, dynamic_size, p->delta, p->deltalength, TRUE);
262 n_memcpy(z_memory, prevstate, dynamic_size);
264 stack = glk_stream_open_memory((char *) p->stackchunk, p->stacklength,
267 quetzal_stack_restore(stack, p->stacklength);
268 glk_stream_close(stack, &poo);
270 if(poo.readcount != p->stacklength) {
271 n_show_error(E_SAVE, "incorrect stack size assessment", poo.readcount);
275 if(p->PC_in_instruction) {
283 has_done_save_undo = TRUE;
285 z_find_size(&wid, &hei);
286 set_header(wid, hei);
293 /* For automapping, we want the saveundo/restoreundo to be as fast as possible
294 and we also don't want it clobbering the redo capabilities, so give it
295 separate fast_saveundo and fast_restoreundo. Doesn't need multiple undo
299 struct fast_undoslot {
303 zbyte *stackchunk; /* Quetzal encoded */
304 glui32 stackchunksize;
309 static struct fast_undoslot automap_undoslot = { NULL, 0, 0, NULL, 0, 0 };
311 BOOL fast_saveundo(void)
315 /* Avoids realloc() in hopes that'll make it a teensy bit faster */
316 if(automap_undoslot.z_memsize < dynamic_size) {
317 n_free(automap_undoslot.z_mem);
318 automap_undoslot.z_mem = n_malloc(dynamic_size);
319 automap_undoslot.z_memsize = dynamic_size;
321 n_memcpy(automap_undoslot.z_mem, z_memory, dynamic_size);
323 automap_undoslot.PC = oldPC;
325 automap_undoslot.stacklength = stack_size = get_quetzal_stack_size();
326 if(automap_undoslot.stackchunksize < stack_size) {
327 free(automap_undoslot.stackchunk);
328 automap_undoslot.stackchunk = (zbyte *) n_malloc(stack_size);
329 automap_undoslot.stackchunksize = stack_size;
332 stack = glk_stream_open_memory((char *) automap_undoslot.stackchunk,
333 automap_undoslot.stacklength,
337 if(!quetzal_stack_save(stack)) {
338 glk_stream_close(stack, NULL);
341 glk_stream_close(stack, NULL);
346 BOOL fast_restoreundo(void)
349 n_memcpy(z_memory, automap_undoslot.z_mem, dynamic_size);
350 PC = automap_undoslot.PC;
352 stack = glk_stream_open_memory((char *) automap_undoslot.stackchunk,
353 automap_undoslot.stacklength,
356 quetzal_stack_restore(stack, automap_undoslot.stacklength);
357 glk_stream_close(stack, NULL);
370 n_free(movelist->delta);
371 n_free(movelist->stackchunk);
377 n_free(automap_undoslot.z_mem);
378 n_free(automap_undoslot.stackchunk);
379 automap_undoslot.z_mem = NULL;
380 automap_undoslot.z_memsize = 0;
381 automap_undoslot.PC = 0;
382 automap_undoslot.stackchunk = NULL;
383 automap_undoslot.stackchunksize = 0;
384 automap_undoslot.stacklength = 0;