7f28c94eda
The interpreter more or less passes all the tests in the z-machine test suite. It should build for every target except Archos (for which it is disabled). git-svn-id: svn://svn.rockbox.org/rockbox/trunk@24267 a1c6a512-1295-4272-9138-f99709370657
541 lines
16 KiB
C
541 lines
16 KiB
C
/* quetzal.c - Saving and restoring of Quetzal files.
|
|
* Written by Martin Frost <mdf@doc.ic.ac.uk>
|
|
*
|
|
* Changes for Rockbox copyright 2009 Torne Wuff
|
|
*
|
|
* This file is part of Frotz.
|
|
*
|
|
* Frotz 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.
|
|
*
|
|
* Frotz 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-1307, USA
|
|
*/
|
|
|
|
#include "frotz.h"
|
|
|
|
#define far
|
|
|
|
#define get_c fgetc
|
|
#define put_c fputc
|
|
|
|
typedef unsigned long zlong;
|
|
|
|
/*
|
|
* This is used only by save_quetzal. It probably should be allocated
|
|
* dynamically rather than statically.
|
|
*/
|
|
|
|
static zword frames[STACK_SIZE/4+1];
|
|
|
|
/*
|
|
* ID types.
|
|
*/
|
|
|
|
#define makeid(a,b,c,d) ((zlong) (((a)<<24) | ((b)<<16) | ((c)<<8) | (d)))
|
|
|
|
#define ID_FORM makeid ('F','O','R','M')
|
|
#define ID_IFZS makeid ('I','F','Z','S')
|
|
#define ID_IFhd makeid ('I','F','h','d')
|
|
#define ID_UMem makeid ('U','M','e','m')
|
|
#define ID_CMem makeid ('C','M','e','m')
|
|
#define ID_Stks makeid ('S','t','k','s')
|
|
#define ID_ANNO makeid ('A','N','N','O')
|
|
|
|
/*
|
|
* Various parsing states within restoration.
|
|
*/
|
|
|
|
#define GOT_HEADER 0x01
|
|
#define GOT_STACK 0x02
|
|
#define GOT_MEMORY 0x04
|
|
#define GOT_NONE 0x00
|
|
#define GOT_ALL 0x07
|
|
#define GOT_ERROR 0x80
|
|
|
|
/*
|
|
* Macros used to write the files.
|
|
*/
|
|
|
|
#define write_byte(fp,b) (put_c (b, fp) != EOF)
|
|
#define write_bytx(fp,b) write_byte (fp, (b) & 0xFF)
|
|
#define write_word(fp,w) \
|
|
(write_bytx (fp, (w) >> 8) && write_bytx (fp, (w)))
|
|
#define write_long(fp,l) \
|
|
(write_bytx (fp, (l) >> 24) && write_bytx (fp, (l) >> 16) && \
|
|
write_bytx (fp, (l) >> 8) && write_bytx (fp, (l)))
|
|
#define write_chnk(fp,id,len) \
|
|
(write_long (fp, (id)) && write_long (fp, (len)))
|
|
#define write_run(fp,run) \
|
|
(write_byte (fp, 0) && write_byte (fp, (run)))
|
|
|
|
/* Read one word from file; return TRUE if OK. */
|
|
static bool read_word (int f, zword *result)
|
|
{
|
|
int a, b;
|
|
|
|
if ((a = get_c (f)) == EOF) return FALSE;
|
|
if ((b = get_c (f)) == EOF) return FALSE;
|
|
|
|
*result = ((zword) a << 8) | (zword) b;
|
|
return TRUE;
|
|
}
|
|
|
|
/* Read one long from file; return TRUE if OK. */
|
|
static bool read_long (int f, zlong *result)
|
|
{
|
|
int a, b, c, d;
|
|
|
|
if ((a = get_c (f)) == EOF) return FALSE;
|
|
if ((b = get_c (f)) == EOF) return FALSE;
|
|
if ((c = get_c (f)) == EOF) return FALSE;
|
|
if ((d = get_c (f)) == EOF) return FALSE;
|
|
|
|
*result = ((zlong) a << 24) | ((zlong) b << 16) |
|
|
((zlong) c << 8) | (zlong) d;
|
|
return TRUE;
|
|
}
|
|
|
|
/*
|
|
* Restore a saved game using Quetzal format. Return 2 if OK, 0 if an error
|
|
* occurred before any damage was done, -1 on a fatal error.
|
|
*/
|
|
|
|
zword restore_quetzal (int svf, int stf)
|
|
{
|
|
zlong ifzslen, currlen, tmpl;
|
|
zlong pc;
|
|
zword i, tmpw;
|
|
zword fatal = 0; /* Set to -1 when errors must be fatal. */
|
|
zbyte skip, progress = GOT_NONE;
|
|
int x, y;
|
|
|
|
/* Check it's really an `IFZS' file. */
|
|
if (!read_long (svf, &tmpl)
|
|
|| !read_long (svf, &ifzslen)
|
|
|| !read_long (svf, &currlen)) return 0;
|
|
if (tmpl != ID_FORM || currlen != ID_IFZS)
|
|
{
|
|
print_string ("This is not a saved game file!\n");
|
|
return 0;
|
|
}
|
|
if ((ifzslen & 1) || ifzslen<4) /* Sanity checks. */ return 0;
|
|
ifzslen -= 4;
|
|
|
|
/* Read each chunk and process it. */
|
|
while (ifzslen > 0)
|
|
{
|
|
/* Read chunk header. */
|
|
if (ifzslen < 8) /* Couldn't contain a chunk. */ return 0;
|
|
if (!read_long (svf, &tmpl)
|
|
|| !read_long (svf, &currlen)) return 0;
|
|
ifzslen -= 8; /* Reduce remaining by size of header. */
|
|
|
|
/* Handle chunk body. */
|
|
if (ifzslen < currlen) /* Chunk goes past EOF?! */ return 0;
|
|
skip = currlen & 1;
|
|
ifzslen -= currlen + (zlong) skip;
|
|
|
|
switch (tmpl)
|
|
{
|
|
/* `IFhd' header chunk; must be first in file. */
|
|
case ID_IFhd:
|
|
if (progress & GOT_HEADER)
|
|
{
|
|
print_string ("Save file has two IFZS chunks!\n");
|
|
return fatal;
|
|
}
|
|
progress |= GOT_HEADER;
|
|
if (currlen < 13
|
|
|| !read_word (svf, &tmpw)) return fatal;
|
|
if (tmpw != h_release)
|
|
progress = GOT_ERROR;
|
|
|
|
for (i=H_SERIAL; i<H_SERIAL+6; ++i)
|
|
{
|
|
if ((x = get_c (svf)) == EOF) return fatal;
|
|
if (x != zmp[i])
|
|
progress = GOT_ERROR;
|
|
}
|
|
|
|
if (!read_word (svf, &tmpw)) return fatal;
|
|
if (tmpw != h_checksum)
|
|
progress = GOT_ERROR;
|
|
|
|
if (progress & GOT_ERROR)
|
|
{
|
|
print_string ("File was not saved from this story!\n");
|
|
return fatal;
|
|
}
|
|
if ((x = get_c (svf)) == EOF) return fatal;
|
|
pc = (zlong) x << 16;
|
|
if ((x = get_c (svf)) == EOF) return fatal;
|
|
pc |= (zlong) x << 8;
|
|
if ((x = get_c (svf)) == EOF) return fatal;
|
|
pc |= (zlong) x;
|
|
fatal = -1; /* Setting PC means errors must be fatal. */
|
|
SET_PC (pc);
|
|
|
|
for (i=13; i<currlen; ++i)
|
|
(void) get_c (svf); /* Skip rest of chunk. */
|
|
break;
|
|
/* `Stks' stacks chunk; restoring this is quite complex. ;) */
|
|
case ID_Stks:
|
|
if (progress & GOT_STACK)
|
|
{
|
|
print_string ("File contains two stack chunks!\n");
|
|
break;
|
|
}
|
|
progress |= GOT_STACK;
|
|
|
|
fatal = -1; /* Setting SP means errors must be fatal. */
|
|
sp = stack + STACK_SIZE;
|
|
|
|
/*
|
|
* All versions other than V6 may use evaluation stack outside
|
|
* any function context. As a result a faked function context
|
|
* will be present in the file here. We skip this context, but
|
|
* load the associated stack onto the stack proper...
|
|
*/
|
|
if (h_version != V6)
|
|
{
|
|
if (currlen < 8) return fatal;
|
|
for (i=0; i<6; ++i)
|
|
if (get_c (svf) != 0) return fatal;
|
|
if (!read_word (svf, &tmpw)) return fatal;
|
|
if (tmpw > STACK_SIZE)
|
|
{
|
|
print_string ("Save-file has too much stack (and I can't cope).\n");
|
|
return fatal;
|
|
}
|
|
currlen -= 8;
|
|
if ((signed)currlen < tmpw*2) return fatal;
|
|
for (i=0; i<tmpw; ++i)
|
|
if (!read_word (svf, --sp)) return fatal;
|
|
currlen -= tmpw*2;
|
|
}
|
|
|
|
/* We now proceed to load the main block of stack frames. */
|
|
for (fp = stack+STACK_SIZE, frame_count = 0;
|
|
currlen > 0;
|
|
currlen -= 8, ++frame_count)
|
|
{
|
|
if (currlen < 8) return fatal;
|
|
if (sp - stack < 4) /* No space for frame. */
|
|
{
|
|
print_string ("Save-file has too much stack (and I can't cope).\n");
|
|
return fatal;
|
|
}
|
|
|
|
/* Read PC, procedure flag and formal param count. */
|
|
if (!read_long (svf, &tmpl)) return fatal;
|
|
y = (int) (tmpl & 0x0F); /* Number of formals. */
|
|
tmpw = y << 8;
|
|
|
|
/* Read result variable. */
|
|
if ((x = get_c (svf)) == EOF) return fatal;
|
|
|
|
/* Check the procedure flag... */
|
|
if (tmpl & 0x10)
|
|
{
|
|
tmpw |= 0x1000; /* It's a procedure. */
|
|
tmpl >>= 8; /* Shift to get PC value. */
|
|
}
|
|
else
|
|
{
|
|
/* Functions have type 0, so no need to or anything. */
|
|
tmpl >>= 8; /* Shift to get PC value. */
|
|
--tmpl; /* Point at result byte. */
|
|
/* Sanity check on result variable... */
|
|
if (zmp[tmpl] != (zbyte) x)
|
|
{
|
|
print_string ("Save-file has wrong variable number on stack (possibly wrong game version?)\n");
|
|
return fatal;
|
|
}
|
|
}
|
|
*--sp = (zword) (tmpl >> 9); /* High part of PC */
|
|
*--sp = (zword) (tmpl & 0x1FF); /* Low part of PC */
|
|
*--sp = (zword) (fp - stack - 1); /* FP */
|
|
|
|
/* Read and process argument mask. */
|
|
if ((x = get_c (svf)) == EOF) return fatal;
|
|
++x; /* Should now be a power of 2 */
|
|
for (i=0; i<8; ++i)
|
|
if (x & (1<<i))
|
|
break;
|
|
if (x ^ (1<<i)) /* Not a power of 2 */
|
|
{
|
|
print_string ("Save-file uses incomplete argument lists (which I can't handle)\n");
|
|
return fatal;
|
|
}
|
|
*--sp = tmpw | i;
|
|
fp = sp; /* FP for next frame. */
|
|
|
|
/* Read amount of eval stack used. */
|
|
if (!read_word (svf, &tmpw)) return fatal;
|
|
|
|
tmpw += y; /* Amount of stack + number of locals. */
|
|
if (sp - stack <= tmpw)
|
|
{
|
|
print_string ("Save-file has too much stack (and I can't cope).\n");
|
|
return fatal;
|
|
}
|
|
if ((signed)currlen < tmpw*2) return fatal;
|
|
for (i=0; i<tmpw; ++i)
|
|
if (!read_word (svf, --sp)) return fatal;
|
|
currlen -= tmpw*2;
|
|
}
|
|
/* End of `Stks' processing... */
|
|
break;
|
|
/* Any more special chunk types must go in HERE or ABOVE. */
|
|
/* `CMem' compressed memory chunk; uncompress it. */
|
|
case ID_CMem:
|
|
if (!(progress & GOT_MEMORY)) /* Don't complain if two. */
|
|
{
|
|
(void) fseek (stf, 0, SEEK_SET);
|
|
i=0; /* Bytes written to data area. */
|
|
for (; currlen > 0; --currlen)
|
|
{
|
|
if ((x = get_c (svf)) == EOF) return fatal;
|
|
if (x == 0) /* Start run. */
|
|
{
|
|
/* Check for bogus run. */
|
|
if (currlen < 2)
|
|
{
|
|
print_string ("File contains bogus `CMem' chunk.\n");
|
|
for (; currlen > 0; --currlen)
|
|
(void) get_c (svf); /* Skip rest. */
|
|
currlen = 1;
|
|
i = 0xFFFF;
|
|
break; /* Keep going; may be a `UMem' too. */
|
|
}
|
|
/* Copy story file to memory during the run. */
|
|
--currlen;
|
|
if ((x = get_c (svf)) == EOF) return fatal;
|
|
for (; x >= 0 && i<h_dynamic_size; --x, ++i)
|
|
if ((y = get_c (stf)) == EOF) return fatal;
|
|
else
|
|
zmp[i] = (zbyte) y;
|
|
}
|
|
else /* Not a run. */
|
|
{
|
|
if ((y = get_c (stf)) == EOF) return fatal;
|
|
zmp[i] = (zbyte) (x ^ y);
|
|
++i;
|
|
}
|
|
/* Make sure we don't load too much. */
|
|
if (i > h_dynamic_size)
|
|
{
|
|
print_string ("warning: `CMem' chunk too long!\n");
|
|
for (; currlen > 1; --currlen)
|
|
(void) get_c (svf); /* Skip rest. */
|
|
break; /* Keep going; there may be a `UMem' too. */
|
|
}
|
|
}
|
|
/* If chunk is short, assume a run. */
|
|
for (; i<h_dynamic_size; ++i)
|
|
if ((y = get_c (stf)) == EOF) return fatal;
|
|
else
|
|
zmp[i] = (zbyte) y;
|
|
if (currlen == 0)
|
|
progress |= GOT_MEMORY; /* Only if succeeded. */
|
|
break;
|
|
}
|
|
/* Fall right thru (to default) if already GOT_MEMORY */
|
|
/* `UMem' uncompressed memory chunk; load it. */
|
|
case ID_UMem:
|
|
if (!(progress & GOT_MEMORY)) /* Don't complain if two. */
|
|
{
|
|
/* Must be exactly the right size. */
|
|
if (currlen == h_dynamic_size)
|
|
{
|
|
if (fread (zmp, currlen, 1, svf) == 1)
|
|
{
|
|
progress |= GOT_MEMORY; /* Only on success. */
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
print_string ("`UMem' chunk wrong size!\n");
|
|
/* Fall into default action (skip chunk) on errors. */
|
|
}
|
|
/* Fall thru (to default) if already GOT_MEMORY */
|
|
/* Unrecognised chunk type; skip it. */
|
|
default:
|
|
(void) fseek (svf, currlen, SEEK_CUR); /* Skip chunk. */
|
|
break;
|
|
}
|
|
if (skip)
|
|
(void) get_c (svf); /* Skip pad byte. */
|
|
}
|
|
|
|
/*
|
|
* We've reached the end of the file. For the restoration to have been a
|
|
* success, we must have had one of each of the required chunks.
|
|
*/
|
|
if (!(progress & GOT_HEADER))
|
|
print_string ("error: no valid header (`IFhd') chunk in file.\n");
|
|
if (!(progress & GOT_STACK))
|
|
print_string ("error: no valid stack (`Stks') chunk in file.\n");
|
|
if (!(progress & GOT_MEMORY))
|
|
print_string ("error: no valid memory (`CMem' or `UMem') chunk in file.\n");
|
|
|
|
return (progress == GOT_ALL ? 2 : fatal);
|
|
}
|
|
|
|
/*
|
|
* Save a game using Quetzal format. Return 1 if OK, 0 if failed.
|
|
*/
|
|
|
|
zword save_quetzal (int svf, int stf)
|
|
{
|
|
zlong ifzslen = 0, cmemlen = 0, stkslen = 0;
|
|
zlong pc;
|
|
zword i, j, n;
|
|
zword nvars, nargs, nstk, *p;
|
|
zbyte var;
|
|
long cmempos, stkspos;
|
|
int c;
|
|
|
|
/* Write `IFZS' header. */
|
|
if (!write_chnk (svf, ID_FORM, 0)) return 0;
|
|
if (!write_long (svf, ID_IFZS)) return 0;
|
|
|
|
/* Write `IFhd' chunk. */
|
|
GET_PC (pc);
|
|
if (!write_chnk (svf, ID_IFhd, 13)) return 0;
|
|
if (!write_word (svf, h_release)) return 0;
|
|
for (i=H_SERIAL; i<H_SERIAL+6; ++i)
|
|
if (!write_byte (svf, zmp[i])) return 0;
|
|
if (!write_word (svf, h_checksum)) return 0;
|
|
if (!write_long (svf, pc << 8)) /* Includes pad. */ return 0;
|
|
|
|
/* Write `CMem' chunk. */
|
|
if ((cmempos = ftell (svf)) < 0) return 0;
|
|
if (!write_chnk (svf, ID_CMem, 0)) return 0;
|
|
(void) fseek (stf, 0, SEEK_SET);
|
|
/* j holds current run length. */
|
|
for (i=0, j=0, cmemlen=0; i < h_dynamic_size; ++i)
|
|
{
|
|
if ((c = get_c (stf)) == EOF) return 0;
|
|
c ^= (int) zmp[i];
|
|
if (c == 0)
|
|
++j; /* It's a run of equal bytes. */
|
|
else
|
|
{
|
|
/* Write out any run there may be. */
|
|
if (j > 0)
|
|
{
|
|
for (; j > 0x100; j -= 0x100)
|
|
{
|
|
if (!write_run (svf, 0xFF)) return 0;
|
|
cmemlen += 2;
|
|
}
|
|
if (!write_run (svf, j-1)) return 0;
|
|
cmemlen += 2;
|
|
j = 0;
|
|
}
|
|
/* Any runs are now written. Write this (nonzero) byte. */
|
|
if (!write_byte (svf, (zbyte) c)) return 0;
|
|
++cmemlen;
|
|
}
|
|
}
|
|
/*
|
|
* Reached end of dynamic memory. We ignore any unwritten run there may be
|
|
* at this point.
|
|
*/
|
|
if (cmemlen & 1) /* Chunk length must be even. */
|
|
if (!write_byte (svf, 0)) return 0;
|
|
|
|
/* Write `Stks' chunk. You are not expected to understand this. ;) */
|
|
if ((stkspos = ftell (svf)) < 0) return 0;
|
|
if (!write_chnk (svf, ID_Stks, 0)) return 0;
|
|
|
|
/*
|
|
* We construct a list of frame indices, most recent first, in `frames'.
|
|
* These indices are the offsets into the `stack' array of the word before
|
|
* the first word pushed in each frame.
|
|
*/
|
|
frames[0] = sp - stack; /* The frame we'd get by doing a call now. */
|
|
for (i = fp - stack + 4, n=0; i < STACK_SIZE+4; i = stack[i-3] + 5)
|
|
frames[++n] = i;
|
|
|
|
/*
|
|
* All versions other than V6 can use evaluation stack outside a function
|
|
* context. We write a faked stack frame (most fields zero) to cater for
|
|
* this.
|
|
*/
|
|
if (h_version != V6)
|
|
{
|
|
for (i=0; i<6; ++i)
|
|
if (!write_byte (svf, 0)) return 0;
|
|
nstk = STACK_SIZE - frames[n];
|
|
if (!write_word (svf, nstk)) return 0;
|
|
for (j=STACK_SIZE-1; j >= frames[n]; --j)
|
|
if (!write_word (svf, stack[j])) return 0;
|
|
stkslen = 8 + 2*nstk;
|
|
}
|
|
|
|
/* Write out the rest of the stack frames. */
|
|
for (i=n; i>0; --i)
|
|
{
|
|
p = stack + frames[i] - 4; /* Points to call frame. */
|
|
nvars = (p[0] & 0x0F00) >> 8;
|
|
nargs = p[0] & 0x00FF;
|
|
nstk = frames[i] - frames[i-1] - nvars - 4;
|
|
pc = ((zlong) p[3] << 9) | p[2];
|
|
|
|
switch (p[0] & 0xF000) /* Check type of call. */
|
|
{
|
|
case 0x0000: /* Function. */
|
|
var = zmp[pc];
|
|
pc = ((pc + 1) << 8) | nvars;
|
|
break;
|
|
case 0x1000: /* Procedure. */
|
|
var = 0;
|
|
pc = (pc << 8) | 0x10 | nvars; /* Set procedure flag. */
|
|
break;
|
|
/* case 0x2000: */
|
|
default:
|
|
runtime_error (ERR_SAVE_IN_INTER);
|
|
return 0;
|
|
}
|
|
if (nargs != 0)
|
|
nargs = (1 << nargs) - 1; /* Make args into bitmap. */
|
|
|
|
/* Write the main part of the frame... */
|
|
if (!write_long (svf, pc)
|
|
|| !write_byte (svf, var)
|
|
|| !write_byte (svf, nargs)
|
|
|| !write_word (svf, nstk)) return 0;
|
|
|
|
/* Write the variables and eval stack. */
|
|
for (j=0, ++p; j<nvars+nstk; ++j, --p)
|
|
if (!write_word (svf, *p)) return 0;
|
|
|
|
/* Calculate length written thus far. */
|
|
stkslen += 8 + 2 * (nvars + nstk);
|
|
}
|
|
|
|
/* Fill in variable chunk lengths. */
|
|
ifzslen = 3*8 + 4 + 14 + cmemlen + stkslen;
|
|
if (cmemlen & 1)
|
|
++ifzslen;
|
|
(void) fseek (svf, 4, SEEK_SET);
|
|
if (!write_long (svf, ifzslen)) return 0;
|
|
(void) fseek (svf, cmempos+4, SEEK_SET);
|
|
if (!write_long (svf, cmemlen)) return 0;
|
|
(void) fseek (svf, stkspos+4, SEEK_SET);
|
|
if (!write_long (svf, stkslen)) return 0;
|
|
|
|
/* After all that, still nothing went wrong! */
|
|
return 1;
|
|
}
|