diff options
author | Paul Gilbert | 2018-11-12 15:45:21 -0800 |
---|---|---|
committer | Paul Gilbert | 2018-12-08 19:05:59 -0800 |
commit | c19d40fa781a414560341a1d357b64875791f17f (patch) | |
tree | 8c4b981694c236e86ebf1ad2b59c44d74a9c5658 /engines/gargoyle/frotz | |
parent | ce7113b34a808ef8136114881f9d19ce857d13bc (diff) | |
download | scummvm-rg350-c19d40fa781a414560341a1d357b64875791f17f.tar.gz scummvm-rg350-c19d40fa781a414560341a1d357b64875791f17f.tar.bz2 scummvm-rg350-c19d40fa781a414560341a1d357b64875791f17f.zip |
GLK: FROTZ: Implemented Quetzal class for savegames
Diffstat (limited to 'engines/gargoyle/frotz')
-rw-r--r-- | engines/gargoyle/frotz/err.h | 2 | ||||
-rw-r--r-- | engines/gargoyle/frotz/mem.cpp | 5 | ||||
-rw-r--r-- | engines/gargoyle/frotz/processor.h | 63 | ||||
-rw-r--r-- | engines/gargoyle/frotz/processor_streams.cpp | 5 | ||||
-rw-r--r-- | engines/gargoyle/frotz/quetzal.cpp | 485 | ||||
-rw-r--r-- | engines/gargoyle/frotz/quetzal.h | 97 |
6 files changed, 639 insertions, 18 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 |