aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--engines/gargoyle/frotz/err.h2
-rw-r--r--engines/gargoyle/frotz/mem.cpp5
-rw-r--r--engines/gargoyle/frotz/processor.h63
-rw-r--r--engines/gargoyle/frotz/processor_streams.cpp5
-rw-r--r--engines/gargoyle/frotz/quetzal.cpp485
-rw-r--r--engines/gargoyle/frotz/quetzal.h97
-rw-r--r--engines/gargoyle/gargoyle.h7
-rw-r--r--engines/gargoyle/module.mk1
-rw-r--r--engines/gargoyle/streams.cpp2
9 files changed, 648 insertions, 19 deletions
diff --git a/engines/gargoyle/frotz/err.h b/engines/gargoyle/frotz/err.h
index e71c9881e8..090ce3f7df 100644
--- a/engines/gargoyle/frotz/err.h
+++ b/engines/gargoyle/frotz/err.h
@@ -96,7 +96,7 @@ protected:
/**
* Get the PC. Is implemented by the Processor class, which derives from Errors
*/
- virtual zword getPC() const = 0;
+ virtual uint getPC() const = 0;
public:
/**
* Constructor
diff --git a/engines/gargoyle/frotz/mem.cpp b/engines/gargoyle/frotz/mem.cpp
index ad60b89bbb..e327fbaecf 100644
--- a/engines/gargoyle/frotz/mem.cpp
+++ b/engines/gargoyle/frotz/mem.cpp
@@ -129,14 +129,11 @@ void Mem::initialize() {
// Load story file in chunks of 32KB
uint n = 0x8000;
-
for (uint size = 64; size < story_size; size += n) {
if (story_size - size < 0x8000)
n = story_size - size;
- SET_PC(size);
-
- if (story_fp->read(pcp, n) != n)
+ if (story_fp->read(zmp + size, n) != n)
error("Story file read error");
}
diff --git a/engines/gargoyle/frotz/processor.h b/engines/gargoyle/frotz/processor.h
index 6a28121512..0f7ecc06b4 100644
--- a/engines/gargoyle/frotz/processor.h
+++ b/engines/gargoyle/frotz/processor.h
@@ -32,25 +32,27 @@
namespace Gargoyle {
namespace Frotz {
-#define CODE_BYTE(v) v = *pcp++
-#define CODE_WORD(v) v = READ_BE_UINT16(pcp += 2)
-#define CODE_IDX_WORD(v,i) v = READ_BE_UINT16(pcp + i)
-#define GET_PC(v) v = pcp - zmp
-#define SET_PC(v) pcp = zmp + v
-
#define TEXT_BUFFER_SIZE 200
+#define CODE_BYTE(v) v = codeByte()
+#define CODE_WORD(v) v = codeWord()
+#define CODE_IDX_WORD(v,i) v = codeWordIdx(i)
+#define GET_PC(v) v = getPC()
+#define SET_PC(v) setPC(v)
+
enum string_type {
LOW_STRING, ABBREVIATION, HIGH_STRING, EMBEDDED_STRING, VOCABULARY
};
class Processor;
+class Quetzal;
typedef void (Processor::*Opcode)();
/**
* Zcode processor
*/
class Processor : public Errors, public GlkInterface, public virtual Mem {
+ friend class Quetzal;
private:
int _finished;
zword zargs[8];
@@ -1510,11 +1512,6 @@ protected:
void z_store();
/**@}*/
-protected:
- /**
- * Get the PC. Is implemented by the Processor class, which derives from Errors
- */
- virtual zword getPC() const { return pcp - zmp; }
public:
/**
* Constructor
@@ -1530,6 +1527,50 @@ public:
* Z-code interpreter main loop
*/
void interpret();
+
+ /**
+ * \defgroup Memory access methods
+ * @{
+ */
+
+ /**
+ * Square brackets operator
+ */
+ zbyte &operator[](uint addr) { return zmp[addr]; }
+
+ /**
+ * Read a code byte
+ */
+ zbyte codeByte() { return *pcp++; }
+
+ /**
+ * Read a code word
+ */
+ zword codeWord() {
+ zword v = READ_BE_UINT16(pcp);
+ pcp += 2;
+ return v;
+ }
+
+ /**
+ * Return a code word at a given address
+ */
+ zword codeWordIdx(uint addr) const {
+ return READ_BE_UINT16(pcp + addr);
+ }
+
+ /**
+ * Return the current program execution offset
+ * @remarks This virtual as a convenient way for the ancestor Err class to access
+ */
+ virtual uint getPC() const override { return pcp - zmp; }
+
+ /**
+ * Set the program execution offset
+ */
+ void setPC(uint addr) { pcp = zmp + addr; }
+
+ /**@}*/
};
} // End of namespace Frotz
diff --git a/engines/gargoyle/frotz/processor_streams.cpp b/engines/gargoyle/frotz/processor_streams.cpp
index d75819e661..50d642585d 100644
--- a/engines/gargoyle/frotz/processor_streams.cpp
+++ b/engines/gargoyle/frotz/processor_streams.cpp
@@ -21,6 +21,7 @@
*/
#include "gargoyle/frotz/processor.h"
+#include "gargoyle/frotz/quetzal.h"
namespace Gargoyle {
namespace Frotz {
@@ -574,7 +575,7 @@ void Processor::z_save() {
if ((gfp = frotzopenprompt (FILE_SAVE)) == nullptr)
goto finished;
- if (option_save_quetzal) {
+ if (_save_quetzal) {
success = save_quetzal (gfp, story_fp, blorb_ofs);
} else {
/* Write game file */
@@ -670,7 +671,7 @@ void Processor::z_restore() {
if ((gfp = frotzopenprompt(FILE_RESTORE)) == nullptr)
goto finished;
- if (option_save_quetzal) {
+ if (_save_quetzal) {
success = restore_quetzal (gfp, story_fp, blorb_ofs);
} else {
diff --git a/engines/gargoyle/frotz/quetzal.cpp b/engines/gargoyle/frotz/quetzal.cpp
new file mode 100644
index 0000000000..183217a390
--- /dev/null
+++ b/engines/gargoyle/frotz/quetzal.cpp
@@ -0,0 +1,485 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#include "gargoyle/frotz/quetzal.h"
+#include "gargoyle/frotz/processor.h"
+#include "common/memstream.h"
+
+namespace Gargoyle {
+namespace Frotz {
+
+/**
+ * Various parsing states within restoration.
+ */
+enum ParseState {
+ GOT_HEADER = 0x01,
+ GOT_STACK = 0x02,
+ GOT_MEMORY = 0x04,
+ GOT_NONE = 0x00,
+ GOT_ALL = 0x07,
+ GOT_ERROR = 0x80
+};
+
+
+bool Quetzal::read_word(Common::ReadStream *f, zword *result) {
+ *result = f->readUint16BE();
+ return true;
+}
+
+bool Quetzal::read_long(Common::ReadStream *f, uint *result) {
+ *result = f->readUint32BE();
+ return true;
+}
+
+bool Quetzal::save(Common::WriteStream *svf, Processor *proc) {
+ Processor &p = *proc;
+ uint ifzslen = 0, cmemlen = 0, stkslen = 0;
+ uint pc;
+ zword i, j, n;
+ zword nvars, nargs, nstk;
+ zbyte var;
+ long cmempos, stkspos;
+ int c;
+
+ // Set a temporary memory stream for writing out the data. This is needed, since we need to
+ // do some seeking within it at the end to fill out totals before properly writing it all out
+ Common::MemoryWriteStreamDynamic saveData(DisposeAfterUse::YES);
+ _out = &saveData;
+
+ // Write `IFZS' header.
+ write_chnk(ID_FORM, 0);
+ write_long(ID_IFZS);
+
+ // Write `IFhd' chunk
+ pc = p.getPC();
+ write_chnk(ID_IFhd, 13);
+ write_word(p.h_release);
+ for (i = H_SERIAL; i<H_SERIAL + 6; ++i)
+ write_byte(p[i]);
+
+ write_word(p.h_checksum);
+ write_long(pc << 8); // Includes pad
+
+ // Write `CMem' chunk.
+ cmempos = svf->pos();
+ write_chnk(ID_CMem, 0);
+ _storyFile->seek(_blorbOffset);
+
+ // j holds current run length.
+ for (i = 0, j = 0, cmemlen = 0; i < p.h_dynamic_size; ++i) {
+ c = _storyFile->readByte();
+ c ^= p[i];
+
+ if (c == 0) {
+ // It's a run of equal bytes
+ ++j;
+ } else {
+ // Write out any run there may be.
+ if (j > 0) {
+ for (; j > 0x100; j -= 0x100) {
+ write_run(0xFF);
+ cmemlen += 2;
+ }
+ write_run(j - 1);
+ cmemlen += 2;
+ j = 0;
+ }
+
+ // Any runs are now written. Write this (nonzero) byte
+ write_byte((zbyte)c);
+ ++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.
+ write_byte(0);
+
+ // Write `Stks' chunk. You are not expected to understand this. ;)
+ stkspos = _storyFile->pos();
+ write_chnk(ID_Stks, 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] = p._sp - p._stack; // The frame we'd get by doing a call now.
+ for (i = p._fp - p._stack + 4, n = 0; i < STACK_SIZE + 4; i = p._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 (p.h_version != V6) {
+ for (i = 0; i < 6; ++i)
+ write_byte(0);
+ nstk = STACK_SIZE - frames[n];
+ write_word(nstk);
+ for (j = STACK_SIZE - 1; j >= frames[n]; --j)
+ write_word(p._stack[j]);
+ stkslen = 8 + 2 * nstk;
+ }
+
+ // Write out the rest of the stack frames.
+ for (i = n; i > 0; --i) {
+ zword *pf = p._stack + frames[i] - 4; // Points to call frame
+ nvars = (pf[0] & 0x0F00) >> 8;
+ nargs = pf[0] & 0x00FF;
+ nstk = frames[i] - frames[i - 1] - nvars - 4;
+ pc = ((uint)pf[3] << 9) | pf[2];
+
+ // Check type of call
+ switch (pf[0] & 0xF000) {
+ case 0x0000:
+ // Function
+ var = p[pc];
+ pc = ((pc + 1) << 8) | nvars;
+ break;
+
+ case 0x1000:
+ // Procedure
+ var = 0;
+ pc = (pc << 8) | 0x10 | nvars; // Set procedure flag
+ break;
+
+ default:
+ p.runtimeError(ERR_SAVE_IN_INTER);
+ return 0;
+ }
+ if (nargs != 0)
+ nargs = (1 << nargs) - 1; // Make args into bitmap
+
+ // Write the main part of the frame...
+ write_long(pc);
+ write_byte(var);
+ write_byte(nargs);
+ write_word(nstk);
+
+ // Write the variables and eval stack
+ for (j = 0, --pf; j<nvars + nstk; ++j, --pf)
+ write_word(*pf);
+
+ // 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;
+
+ saveData.seek(4);
+ saveData.writeUint32BE(ifzslen);
+ saveData.seek(cmempos + 4);
+ saveData.writeUint32BE(cmemlen);
+ saveData.seek(stkspos + 4);
+ saveData.writeUint32BE(stkslen);
+
+ // Write the save data out
+ svf->write(saveData.getData(), saveData.size());
+
+ // After all that, still nothing went wrong!
+ return true;
+}
+
+
+int Quetzal::restore(Common::SeekableReadStream *svf, Processor *proc) {
+ Processor &p = *proc;
+ uint ifzslen, currlen, tmpl;
+ uint pc;
+ zword i, tmpw;
+ int 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.
+ tmpl = svf->readUint32BE();
+ ifzslen = svf->readUint32BE();
+ currlen = svf->readUint32BE();
+ if (tmpl != ID_FORM || currlen != ID_IFZS) {
+ p.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;
+
+ tmpl = svf->readUint32BE();
+ currlen = svf->readUint32BE();
+ 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 + (uint)skip;
+
+ switch (tmpl) {
+ // `IFhd' header chunk; must be first in file
+ case ID_IFhd:
+ if (progress & GOT_HEADER) {
+ p.print_string("Save file has two IFZS chunks!\n");
+ return fatal;
+ }
+ progress |= GOT_HEADER;
+ if (currlen < 13)
+ return fatal;
+
+ tmpw = svf->readUint16BE();
+ if (tmpw != p.h_release)
+ progress = GOT_ERROR;
+
+ for (i = H_SERIAL; i < H_SERIAL + 6; ++i) {
+ x = svf->readByte();
+ if (x != p[i])
+ progress = GOT_ERROR;
+ }
+
+ tmpw = svf->readUint16BE();
+ if (tmpw != p.h_checksum)
+ progress = GOT_ERROR;
+
+ if (progress & GOT_ERROR) {
+ p.print_string("File was not saved from this story!\n");
+ return fatal;
+ }
+
+ x = svf->readByte();
+ pc = (uint)x << 16;
+ x = svf->readByte();
+ pc |= (uint)x << 8;
+ x = svf->readByte();
+ pc |= (uint)x;
+
+ fatal = -1; // Setting PC means errors must be fatal
+ p.setPC(pc);
+
+ svf->skip(13); // Skip rest of chunk
+ break;
+
+ // `Stks' stacks chunk; restoring this is quite complex. ;)
+ case ID_Stks:
+ if (progress & GOT_STACK) {
+ p.print_string("File contains two stack chunks!\n");
+ break;
+ }
+ progress |= GOT_STACK;
+
+ fatal = -1; // Setting SP means errors must be fatal
+ p._sp = p._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 (p.h_version != V6) {
+ if (currlen < 8)
+ return fatal;
+
+ svf->skip(6);
+ tmpw = svf->readUint16BE();
+
+ if (tmpw > STACK_SIZE) {
+ p.print_string("Save-file has too much stack (and I can't cope).\n");
+ return fatal;
+ }
+
+ currlen -= 8;
+ if (currlen < (uint)tmpw * 2)
+ return fatal;
+ for (i = 0; i < tmpw; ++i)
+ *--p._sp = svf->readUint16BE();
+ currlen -= tmpw * 2;
+ }
+
+ // We now proceed to load the main block of stack frames
+ for (p._fp = p._stack + STACK_SIZE, p._frameCount = 0;
+ currlen > 0; currlen -= 8, ++p._frameCount) {
+ if (currlen < 8) return fatal;
+ if (p._sp - p._stack < 4) {
+ // No space for frame
+ p.print_string("Save-file has too much stack (and I can't cope).\n");
+ return fatal;
+ }
+
+ // Read PC, procedure flag and formal param count
+ tmpl = svf->readUint32BE();
+ y = (int)(tmpl & 0x0F); // Number of formals
+ tmpw = y << 8;
+
+ // Read result variable
+ x = svf->readByte();
+
+ // 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 (p[tmpl] != (zbyte)x) {
+ p.print_string("Save-file has wrong variable number on stack (possibly wrong game version?)\n");
+ return fatal;
+ }
+ }
+
+ *--p._sp = (zword)(tmpl >> 9); // High part of PC
+ *--p._sp = (zword)(tmpl & 0x1FF); // Low part of PC
+ *--p._sp = (zword)(p._fp - p._stack - 1); // FP
+
+ // Read and process argument mask
+ x = svf->readByte();
+ ++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
+ p.print_string("Save-file uses incomplete argument lists (which I can't handle)\n");
+ return fatal;
+ }
+
+ *--p._sp = tmpw | i;
+ p._fp = p._sp; // FP for next frame
+
+ // Read amount of eval stack used
+ tmpw = svf->readUint16BE();
+
+ tmpw += y; // Amount of stack + number of locals
+ if (p._sp - p._stack <= tmpw) {
+ p.print_string("Save-file has too much stack (and I can't cope).\n");
+ return fatal;
+ }
+ if (currlen < (uint)tmpw * 2)
+ return fatal;
+
+ for (i = 0; i < tmpw; ++i)
+ --*p._sp = svf->readUint16BE();
+ 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
+ _storyFile->seek(_blorbOffset);
+
+ i = 0; // Bytes written to data area
+ for (; currlen > 0; --currlen) {
+ x = svf->readByte();
+ if (x == 0) {
+ // Start of run
+ // Check for bogus run
+ if (currlen < 2) {
+ p.print_string("File contains bogus `CMem' chunk.\n");
+ svf->skip(currlen);
+
+ currlen = 1;
+ i = 0xFFFF;
+ break; // Keep going; may be a `UMem' too
+ }
+
+ // Copy story file to memory during the run
+ --currlen;
+ x = svf->readByte();
+ for (; x >= 0 && i < p.h_dynamic_size; --x, ++i)
+ p[i] = svf->readByte();
+ } else {
+ // Not a run
+ y = svf->readByte();
+ p[i] = (zbyte)(x ^ y);
+ ++i;
+ }
+
+ // Make sure we don't load too much
+ if (i > p.h_dynamic_size) {
+ p.print_string("warning: `CMem' chunk too long!\n");
+ svf->skip(currlen);
+ break; // Keep going; there may be a `UMem' too
+ }
+ }
+
+ // If chunk is short, assume a run
+ for (; i < p.h_dynamic_size; ++i)
+ p[i] = svf->readByte();
+
+ if (currlen == 0)
+ progress |= GOT_MEMORY; // Only if succeeded
+ break;
+ }
+
+ // Intentional fall-through
+
+ case ID_UMem:
+ if (!(progress & GOT_MEMORY)) {
+ // Must be exactly the right size
+ if (currlen == p.h_dynamic_size) {
+ if (svf->read(p.zmp, currlen) == currlen) {
+ progress |= GOT_MEMORY; // Only on success
+ break;
+ }
+ } else {
+ p.print_string("`UMem' chunk wrong size!\n");
+ }
+
+ // Fall into default action (skip chunk) on errors
+ }
+
+ // Intentional fall-through
+
+ default:
+ svf->seek(currlen, SEEK_CUR); // Skip chunk
+ break;
+ }
+
+ if (skip)
+ svf->skip(1); // 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))
+ p.print_string("error: no valid header (`IFhd') chunk in file.\n");
+ if (!(progress & GOT_STACK))
+ p.print_string("error: no valid stack (`Stks') chunk in file.\n");
+ if (!(progress & GOT_MEMORY))
+ p.print_string("error: no valid memory (`CMem' or `UMem') chunk in file.\n");
+
+ return (progress == GOT_ALL ? 2 : fatal);
+}
+
+} // End of namespace Scott
+} // End of namespace Gargoyle
diff --git a/engines/gargoyle/frotz/quetzal.h b/engines/gargoyle/frotz/quetzal.h
new file mode 100644
index 0000000000..69751553e2
--- /dev/null
+++ b/engines/gargoyle/frotz/quetzal.h
@@ -0,0 +1,97 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#ifndef GARGOYLE_FROTZ_QUETZAL
+#define GARGOYLE_FROTZ_QUETZAL
+
+#include "gargoyle/glk_types.h"
+#include "gargoyle/frotz/frotz_types.h"
+
+namespace Gargoyle {
+namespace Frotz {
+
+enum QueztalTag {
+ ID_FORM = MKTAG('F', 'O', 'R', 'M'),
+ ID_IFZS = MKTAG('I', 'F', 'Z', 'S'),
+ ID_IFhd = MKTAG('I', 'F', 'h', 'd'),
+ ID_UMem = MKTAG('U', 'M', 'e', 'm'),
+ ID_CMem = MKTAG('C', 'M', 'e', 'm'),
+ ID_Stks = MKTAG('S', 't', 'k', 's'),
+ ID_ANNO = MKTAG('A', 'N', 'N', 'O')
+};
+
+class Processor;
+
+class Quetzal {
+private:
+ Common::SeekableReadStream *_storyFile;
+ Common::WriteStream *_out;
+ size_t _blorbOffset;
+ int _slot;
+ zword frames[STACK_SIZE / 4 + 1];
+private:
+ /**
+ * Read a 16-bit value from the file
+ */
+ bool read_word(Common::ReadStream *f, zword *result);
+
+ /**
+ * Read 32-bit value from the file
+ */
+ bool read_long(Common::ReadStream *f, uint *result);
+
+ void write_byte(zbyte b) { _out->writeByte(b); }
+ void write_bytx(zword b) { _out->writeByte(b & 0xFF); }
+ void write_word(zword w) { _out->writeUint16BE(w); }
+ void write_long(uint l) { _out->writeUint32BE(l); }
+ void write_run(zword run) { _out->writeUint16LE(run); }
+ void write_chnk(QueztalTag id, zword len) {
+ _out->writeUint32BE(id);
+ _out->writeUint32BE(len);
+ }
+public:
+ /**
+ * Constructor
+ */
+ Quetzal(Common::SeekableReadStream *storyFile, size_t blorbOffset, int slot) :
+ _storyFile(storyFile), _blorbOffset(blorbOffset), _slot(slot) {}
+
+ /*
+ * Save a game using Quetzal format.
+ * @param svf Savegame file
+ * @returns Returns true if OK, false if failed
+ */
+ bool save(Common::WriteStream *svf, Processor *proc);
+
+ /**
+ * Restore a saved game using Quetzal format
+ * @param svf Savegame file
+ * @returns Return 2 if OK, 0 if an error occurred before any damage was done,
+ * -1 on a fatal error
+ */
+ int restore(Common::SeekableReadStream *svf, Processor *proc);
+};
+
+} // End of namespace Frotz
+} // End of namespace Gargoyle
+
+#endif
diff --git a/engines/gargoyle/gargoyle.h b/engines/gargoyle/gargoyle.h
index aeaa0c0bad..676708ff41 100644
--- a/engines/gargoyle/gargoyle.h
+++ b/engines/gargoyle/gargoyle.h
@@ -174,6 +174,13 @@ public:
}
/**
+ * Return the filename for a given save slot
+ */
+ Common::String getSaveName(uint slot) const {
+ return Common::String::format("%s.%.3u", getTargetName().c_str(), slot);
+ }
+
+ /**
* Display a message in a GUI dialog
*/
void GUIError(const char *msg, ...);
diff --git a/engines/gargoyle/module.mk b/engines/gargoyle/module.mk
index 14cc04801e..4694449349 100644
--- a/engines/gargoyle/module.mk
+++ b/engines/gargoyle/module.mk
@@ -38,6 +38,7 @@ MODULE_OBJS := \
frotz/processor_table.o \
frotz/processor_text.o \
frotz/processor_variables.o \
+ frotz/quetzal.o \
scott/detection.o \
scott/scott.o
diff --git a/engines/gargoyle/streams.cpp b/engines/gargoyle/streams.cpp
index d5671f136a..6a8708425a 100644
--- a/engines/gargoyle/streams.cpp
+++ b/engines/gargoyle/streams.cpp
@@ -1563,7 +1563,7 @@ frefid_t Streams::iterate(frefid_t fref, glui32 *rock) {
const Common::String FileReference::getSaveName() const {
assert(_slotNumber != -1);
- return Common::String::format("%s.%.3d", g_vm->getTargetName().c_str(), _slotNumber);
+ return g_vm->getSaveName(_slotNumber);
}
bool FileReference::exists() const {