diff options
Diffstat (limited to 'engines/sci/engine/kfile.cpp')
-rw-r--r-- | engines/sci/engine/kfile.cpp | 784 |
1 files changed, 538 insertions, 246 deletions
diff --git a/engines/sci/engine/kfile.cpp b/engines/sci/engine/kfile.cpp index 335763a35f..796eb08450 100644 --- a/engines/sci/engine/kfile.cpp +++ b/engines/sci/engine/kfile.cpp @@ -29,6 +29,7 @@ #include "common/savefile.h" #include "common/system.h" #include "common/translation.h" +#include "common/memstream.h" #include "gui/saveload.h" @@ -39,14 +40,18 @@ #include "sci/engine/savegame.h" #include "sci/sound/audio.h" #include "sci/console.h" +#ifdef ENABLE_SCI32 +#include "sci/resource.h" +#endif namespace Sci { -extern reg_t file_open(EngineState *s, const Common::String &filename, int mode, bool unwrapFilename); +extern reg_t file_open(EngineState *s, const Common::String &filename, kFileOpenMode mode, bool unwrapFilename); extern FileHandle *getFileFromHandle(EngineState *s, uint handle); extern int fgets_wrapper(EngineState *s, char *dest, int maxsize, int handle); extern void listSavegames(Common::Array<SavegameDesc> &saves); extern int findSavegame(Common::Array<SavegameDesc> &saves, int16 savegameId); +extern bool fillSavegameDesc(const Common::String &filename, SavegameDesc *desc); /** * Writes the cwd to the supplied address and returns the address in acc. @@ -154,33 +159,37 @@ reg_t kDeviceInfo(EngineState *s, int argc, reg_t *argv) { } reg_t kCheckFreeSpace(EngineState *s, int argc, reg_t *argv) { - if (argc > 1) { - // SCI1.1/SCI32 - // TODO: don't know if those are right for SCI32 as well - // Please note that sierra sci supported both calls either w/ or w/o opcode in SCI1.1 - switch (argv[1].toUint16()) { - case 0: // return saved game size - return make_reg(0, 0); // we return 0 - - case 1: // return free harddisc space (shifted right somehow) - return make_reg(0, 0x7fff); // we return maximum - - case 2: // same as call w/o opcode - break; - return make_reg(0, 1); - - default: - error("kCheckFreeSpace: called with unknown sub-op %d", argv[1].toUint16()); - } + // A file path to test is also passed to this function as a separate + // argument, but we do not actually check anything, so it is unused + + enum { + kSaveGameSize = 0, + kFreeDiskSpace = 1, + kEnoughSpaceToSave = 2 + }; + + int16 subop; + // In SCI2.1mid, the call is moved into kFileIO and the arguments are + // flipped + if (getSciVersion() >= SCI_VERSION_2_1_MIDDLE) { + subop = argc > 0 ? argv[0].toSint16() : 2; + } else { + subop = argc > 1 ? argv[1].toSint16() : 2; } - Common::String path = s->_segMan->getString(argv[0]); + switch (subop) { + case kSaveGameSize: + return make_reg(0, 0); - debug(3, "kCheckFreeSpace(%s)", path.c_str()); - // We simply always pretend that there is enough space. The alternative - // would be to write a big test file, which is not nice on systems where - // doing so is very slow. - return make_reg(0, 1); + case kFreeDiskSpace: // in KiB; up to 32MiB maximum + return make_reg(0, 0x7fff); + + case kEnoughSpaceToSave: + return make_reg(0, 1); + + default: + error("kCheckFreeSpace: called with unknown sub-op %d", subop); + } } reg_t kValidPath(EngineState *s, int argc, reg_t *argv) { @@ -195,32 +204,70 @@ reg_t kValidPath(EngineState *s, int argc, reg_t *argv) { #ifdef ENABLE_SCI32 reg_t kCD(EngineState *s, int argc, reg_t *argv) { - // TODO: Stub - switch (argv[0].toUint16()) { - case 0: - if (argc == 1) { - // Check if a disc is in the drive - return TRUE_REG; - } else { - // Check if the specified disc is in the drive - // and return the current disc number. We just - // return the requested disc number. - return argv[1]; - } - case 1: - // Return the current CD number - return make_reg(0, 1); - default: - warning("CD(%d)", argv[0].toUint16()); + if (!s) + return make_reg(0, getSciVersion()); + error("not supposed to call this"); +} + +reg_t kCheckCD(EngineState *s, int argc, reg_t *argv) { + const int16 cdNo = argc > 0 ? argv[0].toSint16() : 0; + + if (cdNo) { + g_sci->getResMan()->findDisc(cdNo); } - return NULL_REG; + return make_reg(0, g_sci->getResMan()->getCurrentDiscNo()); +} + +reg_t kGetSavedCD(EngineState *s, int argc, reg_t *argv) { + // TODO: This is wrong, CD number needs to be available prior to + // the save game being loaded + return make_reg(0, g_sci->getResMan()->getCurrentDiscNo()); } #endif // ---- FileIO operations ----------------------------------------------------- +#ifdef ENABLE_SCI32 +static bool isSaveCatalogue(const Common::String &name) { + return name == "autosave.cat" || name.hasSuffix("sg.cat"); +} + +// SCI32 save game scripts check for, and write directly to, the save game +// catalogue. Since ScummVM does not use these catalogues, when looking for a +// catalogue, we instead check for save games within ScummVM that are logically +// equivalent to the behaviour of SSCI. +static bool saveCatalogueExists(const Common::String &name) { + bool exists = false; + Common::SaveFileManager *saveFileMan = g_sci->getSaveFileManager(); + + // There will always be one save game in some games, the "new game" + // game, which should be ignored when deciding if there are any save + // games available + uint numPermanentSaves; + switch (g_sci->getGameId()) { + case GID_TORIN: + case GID_LSL7: + case GID_LIGHTHOUSE: + numPermanentSaves = 1; + break; + default: + numPermanentSaves = 0; + break; + } + + // Torin uses autosave.cat; LSL7 uses autosvsg.cat + if (name == "autosave.cat" || name == "autosvsg.cat") { + exists = !saveFileMan->listSavefiles(g_sci->getSavegameName(0)).empty(); + } else { + exists = saveFileMan->listSavefiles(g_sci->getSavegamePattern()).size() > numPermanentSaves; + } + + return exists; +} +#endif + reg_t kFileIO(EngineState *s, int argc, reg_t *argv) { if (!s) return make_reg(0, getSciVersion()); @@ -230,9 +277,13 @@ reg_t kFileIO(EngineState *s, int argc, reg_t *argv) { reg_t kFileIOOpen(EngineState *s, int argc, reg_t *argv) { Common::String name = s->_segMan->getString(argv[0]); - // SCI32 can call K_FILEIO_OPEN with only one argument. It seems to - // just be checking if it exists. - int mode = (argc < 2) ? (int)_K_FILE_MODE_OPEN_OR_FAIL : argv[1].toUint16(); + if (name.empty()) { + // Happens many times during KQ1 (e.g. when typing something) + debugC(kDebugLevelFile, "Attempted to open a file with an empty filename"); + return SIGNAL_REG; + } + + kFileOpenMode mode = (kFileOpenMode)argv[1].toUint16(); bool unwrapFilename = true; // SQ4 floppy prepends /\ to the filenames @@ -250,85 +301,158 @@ reg_t kFileIOOpen(EngineState *s, int argc, reg_t *argv) { return SIGNAL_REG; } - if (name.empty()) { - // Happens many times during KQ1 (e.g. when typing something) - debugC(kDebugLevelFile, "Attempted to open a file with an empty filename"); - return SIGNAL_REG; - } - debugC(kDebugLevelFile, "kFileIO(open): %s, 0x%x", name.c_str(), mode); - - if (name.hasPrefix("sciAudio\\")) { - // fan-made sciAudio extension, don't create those files and instead return a virtual handle - return make_reg(0, VIRTUALFILE_HANDLE_SCIAUDIO); - } - #ifdef ENABLE_SCI32 - if (name == PHANTASMAGORIA_SAVEGAME_INDEX) { - if (s->_virtualIndexFile) { - return make_reg(0, VIRTUALFILE_HANDLE_SCI32SAVE); - } else { - Common::String englishName = g_sci->getSciLanguageString(name, K_LANG_ENGLISH); - Common::String wrappedName = g_sci->wrapFilename(englishName); - if (!g_sci->getSaveFileManager()->listSavefiles(wrappedName).empty()) { - s->_virtualIndexFile = new VirtualIndexFile(wrappedName); - return make_reg(0, VIRTUALFILE_HANDLE_SCI32SAVE); - } + // GK1, GK2, KQ7, LSL6hires, Phant1, PQ4, PQ:SWAT, and SQ6 read in + // their game version from the VERSION file + if (name.compareToIgnoreCase("version") == 0) { + unwrapFilename = false; + + // LSL6hires version is in a file with an empty extension + if (Common::File::exists(name + ".")) { + name += "."; } } - // Shivers is trying to store savegame descriptions and current spots in - // separate .SG files, which are hardcoded in the scripts. - // Essentially, there is a normal save file, created by the executable - // and an extra hardcoded save file, created by the game scripts, probably - // because they didn't want to modify the save/load code to add the extra - // information. - // Each slot in the book then has two strings, the save description and a - // description of the current spot that the player is at. Currently, the - // spot strings are always empty (probably related to the unimplemented - // kString subop 14, which gets called right before this call). - // For now, we don't allow the creation of these files, which means that - // all the spot descriptions next to each slot description will be empty - // (they are empty anyway). Until a viable solution is found to handle these - // extra files and until the spot description strings are initialized - // correctly, we resort to virtual files in order to make the load screen - // useable. Without this code it is unusable, as the extra information is - // always saved to 0.SG for some reason, but on restore the correct file is - // used. Perhaps the virtual ID is not taken into account when saving. - // - // Future TODO: maintain spot descriptions and show them too, ideally without - // having to return to this logic of extra hardcoded files. if (g_sci->getGameId() == GID_SHIVERS && name.hasSuffix(".SG")) { + // Shivers stores the name and score of save games in separate %d.SG + // files, which are used by the save/load screen if (mode == _K_FILE_MODE_OPEN_OR_CREATE || mode == _K_FILE_MODE_CREATE) { - // Game scripts are trying to create a file with the save - // description, stop them here + // Suppress creation of the SG file, since it is not necessary debugC(kDebugLevelFile, "Not creating unused file %s", name.c_str()); return SIGNAL_REG; } else if (mode == _K_FILE_MODE_OPEN_OR_FAIL) { // Create a virtual file containing the save game description // and slot number, as the game scripts expect. - int slotNumber; - sscanf(name.c_str(), "%d.SG", &slotNumber); + int saveNo; + sscanf(name.c_str(), "%d.SG", &saveNo); + saveNo += kSaveIdShift; - Common::Array<SavegameDesc> saves; - listSavegames(saves); - int savegameNr = findSavegame(saves, slotNumber - SAVEGAMEID_OFFICIALRANGE_START); + SavegameDesc save; + fillSavegameDesc(g_sci->getSavegameName(saveNo), &save); - if (!s->_virtualIndexFile) { - // Make the virtual file buffer big enough to avoid having it grow dynamically. - // 50 bytes should be more than enough. - s->_virtualIndexFile = new VirtualIndexFile(50); + Common::String score; + if (!save.highScore) { + score = Common::String::format("%u", save.lowScore); + } else { + score = Common::String::format("%u%03u", save.highScore, save.lowScore); } - s->_virtualIndexFile->seek(0, SEEK_SET); - s->_virtualIndexFile->write(saves[savegameNr].name, strlen(saves[savegameNr].name)); - s->_virtualIndexFile->write("\0", 1); - s->_virtualIndexFile->write("\0", 1); // Spot description (empty) - s->_virtualIndexFile->seek(0, SEEK_SET); + const uint nameLength = strlen(save.name); + const uint size = nameLength + /* \r\n */ 2 + score.size(); + char *buffer = (char *)malloc(size); + memcpy(buffer, save.name, nameLength); + buffer[nameLength] = '\r'; + buffer[nameLength + 1] = '\n'; + memcpy(buffer + nameLength + 2, score.c_str(), score.size()); + + const uint handle = findFreeFileHandle(s); + + s->_fileHandles[handle]._in = new Common::MemoryReadStream((byte *)buffer, size, DisposeAfterUse::YES); + s->_fileHandles[handle]._out = nullptr; + s->_fileHandles[handle]._name = ""; + + return make_reg(0, handle); + } + } else if (g_sci->getGameId() == GID_MOTHERGOOSEHIRES && name.hasSuffix(".DTA")) { + // MGDX stores the name and avatar ID in separate %d.DTA files, which + // are used by the save/load screen + if (mode == _K_FILE_MODE_OPEN_OR_CREATE || mode == _K_FILE_MODE_CREATE) { + // Suppress creation of the DTA file, since it is not necessary + debugC(kDebugLevelFile, "Not creating unused file %s", name.c_str()); + return SIGNAL_REG; + } else if (mode == _K_FILE_MODE_OPEN_OR_FAIL) { + // Create a virtual file containing the save game description + // and slot number, as the game scripts expect. + int saveNo; + sscanf(name.c_str(), "%d.DTA", &saveNo); + saveNo += kSaveIdShift; + + SavegameDesc save; + fillSavegameDesc(g_sci->getSavegameName(saveNo), &save); + + const Common::String avatarId = Common::String::format("%02d", save.avatarId); + const uint nameLength = strlen(save.name); + const uint size = nameLength + /* \r\n */ 2 + avatarId.size() + 1; + char *buffer = (char *)malloc(size); + memcpy(buffer, save.name, nameLength); + buffer[nameLength] = '\r'; + buffer[nameLength + 1] = '\n'; + memcpy(buffer + nameLength + 2, avatarId.c_str(), avatarId.size() + 1); + + const uint handle = findFreeFileHandle(s); + + s->_fileHandles[handle]._in = new Common::MemoryReadStream((byte *)buffer, size, DisposeAfterUse::YES); + s->_fileHandles[handle]._out = nullptr; + s->_fileHandles[handle]._name = ""; + + return make_reg(0, handle); + } + } else if (g_sci->getGameId() == GID_KQ7) { + // KQ7 creates a temp.tmp file to perform an atomic rewrite of the + // catalogue, but since we do not create catalogues for most SCI32 + // games, ignore the write + if (name == "temp.tmp") { + return make_reg(0, VIRTUALFILE_HANDLE_SCI32SAVE); + } + + // KQ7 tries to read out game information from catalogues directly + // instead of using the standard kSaveGetFiles function + if (name == "kq7cdsg.cat") { + if (mode == _K_FILE_MODE_OPEN_OR_CREATE || mode == _K_FILE_MODE_CREATE) { + // Suppress creation of the catalogue file, since it is not necessary + debugC(kDebugLevelFile, "Not creating unused file %s", name.c_str()); + return SIGNAL_REG; + } else if (mode == _K_FILE_MODE_OPEN_OR_FAIL) { + Common::Array<SavegameDesc> saves; + listSavegames(saves); + + const uint recordSize = sizeof(int16) + SCI_MAX_SAVENAME_LENGTH; + const uint numSaves = MIN<uint>(saves.size(), 10); + const uint size = numSaves * recordSize + /* terminator */ 2; + byte *const buffer = (byte *)malloc(size); + + byte *out = buffer; + for (uint i = 0; i < numSaves; ++i) { + WRITE_UINT16(out, saves[i].id - kSaveIdShift); + Common::strlcpy((char *)out + sizeof(int16), saves[i].name, SCI_MAX_SAVENAME_LENGTH); + out += recordSize; + } + WRITE_UINT16(out, 0xFFFF); + + const uint handle = findFreeFileHandle(s); + s->_fileHandles[handle]._in = new Common::MemoryReadStream(buffer, size, DisposeAfterUse::YES); + s->_fileHandles[handle]._out = nullptr; + s->_fileHandles[handle]._name = ""; + + return make_reg(0, handle); + } + } + } + + // See kMakeSaveCatName + if (name == "fake.cat") { + return make_reg(0, VIRTUALFILE_HANDLE_SCI32SAVE); + } + + if (isSaveCatalogue(name)) { + const bool exists = saveCatalogueExists(name); + if (exists) { + // Dummy handle is used to represent the catalogue and ignore any + // direct game script writes return make_reg(0, VIRTUALFILE_HANDLE_SCI32SAVE); + } else { + return SIGNAL_REG; } } #endif + debugC(kDebugLevelFile, "kFileIO(open): %s, 0x%x", name.c_str(), mode); + + if (name.hasPrefix("sciAudio\\")) { + // fan-made sciAudio extension, don't create those files and instead return a virtual handle + return make_reg(0, VIRTUALFILE_HANDLE_SCIAUDIO); + } + // QFG import rooms get a virtual filelisting instead of an actual one if (g_sci->inQfGImportRoom()) { // We need to find out what the user actually selected, "savedHeroes" is @@ -349,16 +473,9 @@ reg_t kFileIOClose(EngineState *s, int argc, reg_t *argv) { uint16 handle = argv[0].toUint16(); -#ifdef ENABLE_SCI32 - if (handle == VIRTUALFILE_HANDLE_SCI32SAVE) { - s->_virtualIndexFile->close(); - return SIGNAL_REG; - } -#endif - if (handle >= VIRTUALFILE_HANDLE_START) { // it's a virtual handle? ignore it - return SIGNAL_REG; + return getSciVersion() >= SCI_VERSION_2 ? TRUE_REG : SIGNAL_REG; } FileHandle *f = getFileFromHandle(s, handle); @@ -366,7 +483,7 @@ reg_t kFileIOClose(EngineState *s, int argc, reg_t *argv) { f->close(); if (getSciVersion() <= SCI_VERSION_0_LATE) return s->r_acc; // SCI0 semantics: no value returned - return SIGNAL_REG; + return getSciVersion() >= SCI_VERSION_2 ? TRUE_REG : SIGNAL_REG; } if (getSciVersion() <= SCI_VERSION_0_LATE) @@ -381,17 +498,9 @@ reg_t kFileIOReadRaw(EngineState *s, int argc, reg_t *argv) { char *buf = new char[size]; debugC(kDebugLevelFile, "kFileIO(readRaw): %d, %d", handle, size); -#ifdef ENABLE_SCI32 - if (handle == VIRTUALFILE_HANDLE_SCI32SAVE) { - bytesRead = s->_virtualIndexFile->read(buf, size); - } else { -#endif - FileHandle *f = getFileFromHandle(s, handle); - if (f) - bytesRead = f->_in->read(buf, size); -#ifdef ENABLE_SCI32 - } -#endif + FileHandle *f = getFileFromHandle(s, handle); + if (f) + bytesRead = f->_in->read(buf, size); // TODO: What happens if less bytes are read than what has // been requested? (i.e. if bytesRead is non-zero, but still @@ -406,27 +515,37 @@ reg_t kFileIOReadRaw(EngineState *s, int argc, reg_t *argv) { reg_t kFileIOWriteRaw(EngineState *s, int argc, reg_t *argv) { uint16 handle = argv[0].toUint16(); uint16 size = argv[2].toUint16(); + +#ifdef ENABLE_SCI32 + if (handle == VIRTUALFILE_HANDLE_SCI32SAVE) { + return make_reg(0, size); + } +#endif + char *buf = new char[size]; + uint bytesWritten = 0; bool success = false; s->_segMan->memcpy((byte *)buf, argv[1], size); debugC(kDebugLevelFile, "kFileIO(writeRaw): %d, %d", handle, size); + FileHandle *f = getFileFromHandle(s, handle); + if (f) { + bytesWritten = f->_out->write(buf, size); + success = !f->_out->err(); + } + + delete[] buf; + #ifdef ENABLE_SCI32 - if (handle == VIRTUALFILE_HANDLE_SCI32SAVE) { - s->_virtualIndexFile->write(buf, size); - success = true; - } else { -#endif - FileHandle *f = getFileFromHandle(s, handle); - if (f) { - f->_out->write(buf, size); - success = true; + if (getSciVersion() >= SCI_VERSION_2) { + if (!success) { + return SIGNAL_REG; } -#ifdef ENABLE_SCI32 + + return make_reg(0, bytesWritten); } #endif - delete[] buf; if (success) return NULL_REG; return make_reg(0, 6); // DOS - invalid handle @@ -456,19 +575,21 @@ reg_t kFileIOUnlink(EngineState *s, int argc, reg_t *argv) { int savedir_nr = saves[slotNum].id; name = g_sci->getSavegameName(savedir_nr); result = saveFileMan->removeSavefile(name); +#ifdef ENABLE_SCI32 } else if (getSciVersion() >= SCI_VERSION_2) { + // Special case for KQ7, basically identical to the SQ4 case above, + // where the game hardcodes its save game names + if (name.hasPrefix("kq7cdsg.")) { + int saveNo = atoi(name.c_str() + name.size() - 3); + name = g_sci->getSavegameName(saveNo + kSaveIdShift); + } + // The file name may be already wrapped, so check both cases result = saveFileMan->removeSavefile(name); if (!result) { const Common::String wrappedName = g_sci->wrapFilename(name); result = saveFileMan->removeSavefile(wrappedName); } - -#ifdef ENABLE_SCI32 - if (name == PHANTASMAGORIA_SAVEGAME_INDEX) { - delete s->_virtualIndexFile; - s->_virtualIndexFile = 0; - } #endif } else { const Common::String wrappedName = g_sci->wrapFilename(name); @@ -476,6 +597,13 @@ reg_t kFileIOUnlink(EngineState *s, int argc, reg_t *argv) { } debugC(kDebugLevelFile, "kFileIO(unlink): %s", name.c_str()); + +#ifdef ENABLE_SCI32 + if (getSciVersion() >= SCI_VERSION_2) { + return make_reg(0, result); + } +#endif + if (result) return NULL_REG; return make_reg(0, 2); // DOS - file not found error code @@ -488,12 +616,7 @@ reg_t kFileIOReadString(EngineState *s, int argc, reg_t *argv) { debugC(kDebugLevelFile, "kFileIO(readString): %d, %d", handle, maxsize); uint32 bytesRead; -#ifdef ENABLE_SCI32 - if (handle == VIRTUALFILE_HANDLE_SCI32SAVE) - bytesRead = s->_virtualIndexFile->readLine(buf, maxsize); - else -#endif - bytesRead = fgets_wrapper(s, buf, maxsize, handle); + bytesRead = fgets_wrapper(s, buf, maxsize, handle); s->_segMan->memcpy(argv[0], (const byte*)buf, maxsize); delete[] buf; @@ -520,16 +643,9 @@ reg_t kFileIOWriteString(EngineState *s, int argc, reg_t *argv) { return NULL_REG; } -#ifdef ENABLE_SCI32 - if (handle == VIRTUALFILE_HANDLE_SCI32SAVE) { - s->_virtualIndexFile->write(str.c_str(), str.size()); - return NULL_REG; - } -#endif - FileHandle *f = getFileFromHandle(s, handle); - if (f) { + if (f && f->_out) { f->_out->write(str.c_str(), str.size()); if (getSciVersion() <= SCI_VERSION_0_LATE) return s->r_acc; // SCI0 semantics: no value returned @@ -543,27 +659,21 @@ reg_t kFileIOWriteString(EngineState *s, int argc, reg_t *argv) { reg_t kFileIOSeek(EngineState *s, int argc, reg_t *argv) { uint16 handle = argv[0].toUint16(); - uint16 offset = ABS<int16>(argv[1].toSint16()); // can be negative + int16 offset = argv[1].toSint16(); uint16 whence = argv[2].toUint16(); debugC(kDebugLevelFile, "kFileIO(seek): %d, %d, %d", handle, offset, whence); -#ifdef ENABLE_SCI32 - if (handle == VIRTUALFILE_HANDLE_SCI32SAVE) - return make_reg(0, s->_virtualIndexFile->seek(offset, whence)); -#endif - FileHandle *f = getFileFromHandle(s, handle); if (f && f->_in) { - // Backward seeking isn't supported in zip file streams, thus adapt the - // parameters accordingly if games ask for such a seek mode. A known - // case where this is requested is the save file manager in Phantasmagoria - if (whence == SEEK_END) { - whence = SEEK_SET; - offset = f->_in->size() - offset; + const bool success = f->_in->seek(offset, whence); + if (getSciVersion() >= SCI_VERSION_2) { + if (success) { + return make_reg(0, f->_in->pos()); + } + return SIGNAL_REG; } - - return make_reg(0, f->_in->seek(offset, whence)); + return make_reg(0, success); } else if (f && f->_out) { error("kFileIOSeek: Unsupported seek operation on a writeable stream (offset: %d, whence: %d)", offset, whence); } @@ -591,14 +701,6 @@ reg_t kFileIOFindNext(EngineState *s, int argc, reg_t *argv) { reg_t kFileIOExists(EngineState *s, int argc, reg_t *argv) { Common::String name = s->_segMan->getString(argv[0]); -#ifdef ENABLE_SCI32 - // Cache the file existence result for the Phantasmagoria - // save index file, as the game scripts keep checking for - // its existence. - if (name == PHANTASMAGORIA_SAVEGAME_INDEX && s->_virtualIndexFile) - return TRUE_REG; -#endif - bool exists = false; if (g_sci->getGameId() == GID_PEPPER) { @@ -611,6 +713,15 @@ reg_t kFileIOExists(EngineState *s, int argc, reg_t *argv) { return NULL_REG; } +#ifdef ENABLE_SCI32 + if (isSaveCatalogue(name)) { + return saveCatalogueExists(name) ? TRUE_REG : NULL_REG; + } +#endif + + // TODO: It may apparently be worth caching the existence of + // phantsg.dir, and possibly even keeping it open persistently + // Check for regular file exists = Common::File::exists(name); @@ -673,6 +784,15 @@ reg_t kFileIORename(EngineState *s, int argc, reg_t *argv) { Common::String oldName = s->_segMan->getString(argv[0]); Common::String newName = s->_segMan->getString(argv[1]); + // We don't fully implement all cases that could occur here, and + // assume the file to be renamed is a wrapped filename. + // Known usage: In Phant1 and KQ7 while deleting savegames. + // The scripts rewrite the dir file as a temporary file, and then + // rename it to the actual dir file. + + oldName = g_sci->wrapFilename(oldName); + newName = g_sci->wrapFilename(newName); + // SCI1.1 returns 0 on success and a DOS error code on fail. SCI32 // returns -1 on fail. We just return -1 for all versions. if (g_sci->getSaveFileManager()->renameSavefile(oldName, newName)) @@ -694,7 +814,7 @@ reg_t kFileIOWriteByte(EngineState *s, int argc, reg_t *argv) { FileHandle *f = getFileFromHandle(s, argv[0].toUint16()); if (f) f->_out->writeByte(argv[1].toUint16() & 0xff); - return s->r_acc; // FIXME: does this really not return anything? + return s->r_acc; } reg_t kFileIOReadWord(EngineState *s, int argc, reg_t *argv) { @@ -708,27 +828,13 @@ reg_t kFileIOWriteWord(EngineState *s, int argc, reg_t *argv) { FileHandle *f = getFileFromHandle(s, argv[0].toUint16()); if (f) f->_out->writeUint16LE(argv[1].toUint16()); - return s->r_acc; // FIXME: does this really not return anything? + return s->r_acc; } -reg_t kFileIOCreateSaveSlot(EngineState *s, int argc, reg_t *argv) { - // Used in Shivers when the user enters his name on the guest book - // in the beginning to start the game. - - // Creates a new save slot, and returns if the operation was successful - - // Argument 0 denotes the save slot as a negative integer, 2 means "0" - // Argument 1 is a string, with the file name, obtained from kSave(5). - // The interpreter checks if it can be written to (by checking for free - // disk space and write permissions) - - // We don't really use or need any of this... - - uint16 saveSlot = argv[0].toUint16(); - char* fileName = s->_segMan->lookupString(argv[1])->getRawData(); - warning("kFileIOCreateSaveSlot(%d, '%s')", saveSlot, fileName); - - return TRUE_REG; // slot creation was successful +reg_t kFileIOGetCWD(EngineState *s, int argc, reg_t *argv) { + SciArray &fileName = *s->_segMan->lookupArray(argv[0]); + fileName.fromString("C:\\SIERRA\\"); + return argv[0]; } reg_t kFileIOIsValidDirectory(EngineState *s, int argc, reg_t *argv) { @@ -753,7 +859,14 @@ reg_t kSave(EngineState *s, int argc, reg_t *argv) { #endif reg_t kSaveGame(EngineState *s, int argc, reg_t *argv) { - Common::String game_id; + // slot 0 is the ScummVM auto-save slot, which is not used by us, but is + // still reserved + enum { + SAVEGAMESLOT_FIRST = 1, + SAVEGAMESLOT_LAST = 99 + }; + + Common::String game_id = !argv[0].isNull() ? s->_segMan->getString(argv[0]) : ""; int16 virtualId = argv[1].toSint16(); int16 savegameId = -1; Common::String game_description; @@ -786,10 +899,8 @@ reg_t kSaveGame(EngineState *s, int argc, reg_t *argv) { g_sci->_soundCmd->pauseAll(false); // unpause music (we can't have it paused during save) if (savegameId < 0) return NULL_REG; - } else { // Real call from script - game_id = s->_segMan->getString(argv[0]); if (argv[2].isNull()) error("kSaveGame: called with description being NULL"); game_description = s->_segMan->getString(argv[2]); @@ -806,25 +917,47 @@ reg_t kSaveGame(EngineState *s, int argc, reg_t *argv) { return NULL_REG; } else if (virtualId < SAVEGAMEID_OFFICIALRANGE_START) { // virtualId is low, we assume that scripts expect us to create new slot - if (g_sci->getGameId() == GID_JONES) { + switch (g_sci->getGameId()) { + case GID_JONES: // Jones has one save slot only savegameId = 0; - } else if (virtualId == s->_lastSaveVirtualId) { - // if last virtual id is the same as this one, we assume that caller wants to overwrite last save - savegameId = s->_lastSaveNewId; - } else { - uint savegameNr; - // savegameId is in lower range, scripts expect us to create a new slot - for (savegameId = SAVEGAMESLOT_FIRST; savegameId <= SAVEGAMESLOT_LAST; savegameId++) { - for (savegameNr = 0; savegameNr < saves.size(); savegameNr++) { - if (savegameId == saves[savegameNr].id) + break; + case GID_FANMADE: { + // Fanmade game, try to identify the game + const char *gameName = g_sci->getGameObjectName(); + + if (strcmp(gameName, "CascadeQuest") == 0) { + // Cascade Quest calls us directly to auto-save and uses slot 99, + // put that save into slot 0 (ScummVM auto-save slot) see bug #7007 + if (virtualId == (SAVEGAMEID_OFFICIALRANGE_START - 1)) { + savegameId = 0; + } + } + break; + } + default: + break; + } + + if (savegameId < 0) { + // savegameId not set yet + if (virtualId == s->_lastSaveVirtualId) { + // if last virtual id is the same as this one, we assume that caller wants to overwrite last save + savegameId = s->_lastSaveNewId; + } else { + uint savegameNr; + // savegameId is in lower range, scripts expect us to create a new slot + for (savegameId = SAVEGAMESLOT_FIRST; savegameId <= SAVEGAMESLOT_LAST; savegameId++) { + for (savegameNr = 0; savegameNr < saves.size(); savegameNr++) { + if (savegameId == saves[savegameNr].id) + break; + } + if (savegameNr == saves.size()) // Slot not found, seems to be good to go break; } - if (savegameNr == saves.size()) // Slot not found, seems to be good to go - break; + if (savegameId > SAVEGAMESLOT_LAST) + error("kSavegame: no more savegame slots available"); } - if (savegameId > SAVEGAMESLOT_LAST) - error("kSavegame: no more savegame slots available"); } } else { error("kSaveGame: invalid savegameId used"); @@ -982,10 +1115,6 @@ reg_t kCheckSaveGame(EngineState *s, int argc, reg_t *argv) { } reg_t kGetSaveFiles(EngineState *s, int argc, reg_t *argv) { - Common::String game_id = s->_segMan->getString(argv[0]); - - debug(3, "kGetSaveFiles(%s)", game_id.c_str()); - // Scripts ask for current save files, we can assume that if afterwards they ask us to create a new slot they really // mean new slot instead of overwriting the old one s->_lastSaveVirtualId = SAVEGAMEID_OFFICIALRANGE_START; @@ -994,6 +1123,10 @@ reg_t kGetSaveFiles(EngineState *s, int argc, reg_t *argv) { listSavegames(saves); uint totalSaves = MIN<uint>(saves.size(), MAX_SAVEGAME_NR); + Common::String game_id = s->_segMan->getString(argv[0]); + + debug(3, "kGetSaveFiles(%s)", game_id.c_str()); + reg_t *slot = s->_segMan->derefRegPtr(argv[2], totalSaves); if (!slot) { @@ -1021,46 +1154,205 @@ reg_t kGetSaveFiles(EngineState *s, int argc, reg_t *argv) { #ifdef ENABLE_SCI32 -reg_t kMakeSaveCatName(EngineState *s, int argc, reg_t *argv) { - // Normally, this creates the name of the save catalogue/directory to save into. - // First parameter is the string to save the result into. Second is a string - // with game parameters. We don't have a use for this at all, as we have our own - // savegame directory management, thus we always return an empty string. - return argv[0]; +reg_t kSaveGame32(EngineState *s, int argc, reg_t *argv) { + const bool isScummVMSave = argv[0].isNull(); + Common::String gameName = ""; + int16 saveNo; + Common::String saveDescription; + Common::String gameVersion = (argc <= 3 || argv[3].isNull()) ? "" : s->_segMan->getString(argv[3]); + + if (isScummVMSave) { + // ScummVM call, from a patched Game::save + g_sci->_soundCmd->pauseAll(true); + GUI::SaveLoadChooser dialog(_("Save game:"), _("Save"), true); + saveNo = dialog.runModalWithCurrentTarget(); + g_sci->_soundCmd->pauseAll(false); + + if (saveNo < 0) { + // User cancelled save + return NULL_REG; + } + + saveDescription = dialog.getResultString(); + if (saveDescription.empty()) { + saveDescription = dialog.createDefaultSaveDescription(saveNo); + } + } else { + // Native script call + gameName = s->_segMan->getString(argv[0]); + saveNo = argv[1].toSint16(); + saveDescription = argv[2].isNull() ? "" : s->_segMan->getString(argv[2]); + } + + debugC(kDebugLevelFile, "Game name %s save %d desc %s ver %s", gameName.c_str(), saveNo, saveDescription.c_str(), gameVersion.c_str()); + + // Auto-save system used by Torin and LSL7 + if (gameName == "Autosave" || gameName == "Autosv") { + if (saveNo == 0) { + // Autosave slot 0 is the autosave + } else { + // Autosave slot 1 is a "new game" save + saveNo = kNewGameId; + } + } else if (!isScummVMSave) { + // ScummVM save screen will give a pre-corrected save number, but native + // save-load will not + saveNo += kSaveIdShift; + } + + Common::SaveFileManager *saveFileMan = g_sci->getSaveFileManager(); + const Common::String filename = g_sci->getSavegameName(saveNo); + Common::OutSaveFile *saveStream = saveFileMan->openForSaving(filename); + + if (saveStream == nullptr) { + warning("Error opening savegame \"%s\" for writing", filename.c_str()); + return NULL_REG; + } + + if (!gamestate_save(s, saveStream, saveDescription, gameVersion)) { + warning("Saving the game failed"); + saveStream->finalize(); + delete saveStream; + return NULL_REG; + } + + saveStream->finalize(); + if (saveStream->err()) { + warning("Writing the savegame failed"); + delete saveStream; + return NULL_REG; + } + + delete saveStream; + return TRUE_REG; } -reg_t kMakeSaveFileName(EngineState *s, int argc, reg_t *argv) { - // Creates a savegame name from a slot number. Used when deleting saved games. - // Param 0: the output buffer (same as in kMakeSaveCatName) - // Param 1: a string with game parameters, ignored - // Param 2: the selected slot +reg_t kRestoreGame32(EngineState *s, int argc, reg_t *argv) { + const bool isScummVMRestore = argv[0].isNull(); + Common::String gameName = ""; + int16 saveNo = argv[1].toSint16(); + const Common::String gameVersion = argv[2].isNull() ? "" : s->_segMan->getString(argv[2]); + + if (isScummVMRestore && saveNo == -1) { + // ScummVM call, either from lancher or a patched Game::restore + g_sci->_soundCmd->pauseAll(true); + GUI::SaveLoadChooser dialog(_("Restore game:"), _("Restore"), false); + saveNo = dialog.runModalWithCurrentTarget(); + g_sci->_soundCmd->pauseAll(false); + + if (saveNo < 0) { + // User cancelled restore + return s->r_acc; + } + } else { + gameName = s->_segMan->getString(argv[0]); + } + + if (gameName == "Autosave" || gameName == "Autosv") { + if (saveNo == 0) { + // Autosave slot 0 is the autosave + } else { + // Autosave slot 1 is a "new game" save + saveNo = kNewGameId; + } + } else if (!isScummVMRestore) { + // ScummVM save screen will give a pre-corrected save number, but native + // save-load will not + saveNo += kSaveIdShift; + } + + Common::SaveFileManager *saveFileMan = g_sci->getSaveFileManager(); + const Common::String filename = g_sci->getSavegameName(saveNo); + Common::SeekableReadStream *saveStream = saveFileMan->openForLoading(filename); + + if (saveStream == nullptr) { + warning("Savegame #%d not found", saveNo); + return NULL_REG; + } + + gamestate_restore(s, saveStream); + delete saveStream; - SciString *resultString = s->_segMan->lookupString(argv[0]); - uint16 virtualId = argv[2].toUint16(); - if ((virtualId < SAVEGAMEID_OFFICIALRANGE_START) || (virtualId > SAVEGAMEID_OFFICIALRANGE_END)) - error("kMakeSaveFileName: invalid savegame ID specified"); - uint saveSlot = virtualId - SAVEGAMEID_OFFICIALRANGE_START; + gamestate_afterRestoreFixUp(s, saveNo); + return TRUE_REG; +} + +reg_t kCheckSaveGame32(EngineState *s, int argc, reg_t *argv) { + const Common::String gameName = s->_segMan->getString(argv[0]); + int16 saveNo = argv[1].toSint16(); + const Common::String gameVersion = argv[2].isNull() ? "" : s->_segMan->getString(argv[2]); Common::Array<SavegameDesc> saves; listSavegames(saves); - Common::String filename = g_sci->getSavegameName(saveSlot); - resultString->fromString(filename); + if (gameName == "Autosave" || gameName == "Autosv") { + if (saveNo == 1) { + saveNo = kNewGameId; + } + } else { + saveNo += kSaveIdShift; + } - return argv[0]; -} + SavegameDesc save; + if (!fillSavegameDesc(g_sci->getSavegameName(saveNo), &save)) { + return NULL_REG; + } -reg_t kAutoSave(EngineState *s, int argc, reg_t *argv) { - // TODO - // This is a timer callback, with 1 parameter: the timer object - // (e.g. "timers"). - // It's used for auto-saving (i.e. save every X minutes, by checking - // the elapsed time from the timer object) + if (save.version < MINIMUM_SAVEGAME_VERSION || + save.version > CURRENT_SAVEGAME_VERSION || + save.gameVersion != gameVersion) { + + return NULL_REG; + } - // This function has to return something other than 0 to proceed return TRUE_REG; } +reg_t kGetSaveFiles32(EngineState *s, int argc, reg_t *argv) { + // argv[0] is gameName, used in SSCI as the name of the save game catalogue + // but unused here since ScummVM does not support multiple catalogues + SciArray &descriptions = *s->_segMan->lookupArray(argv[1]); + SciArray &saveIds = *s->_segMan->lookupArray(argv[2]); + + Common::Array<SavegameDesc> saves; + listSavegames(saves); + + // Normally SSCI limits to 20 games per directory, but ScummVM allows more + // than that with games that use the standard save-load dialogue + descriptions.resize(SCI_MAX_SAVENAME_LENGTH * saves.size() + 1, true); + saveIds.resize(saves.size() + 1, true); + + for (uint i = 0; i < saves.size(); ++i) { + const SavegameDesc &save = saves[i]; + char *target = &descriptions.charAt(SCI_MAX_SAVENAME_LENGTH * i); + Common::strlcpy(target, save.name, SCI_MAX_SAVENAME_LENGTH); + saveIds.setFromInt16(i, save.id - kSaveIdShift); + } + + descriptions.charAt(SCI_MAX_SAVENAME_LENGTH * saves.size()) = '\0'; + saveIds.setFromInt16(saves.size(), 0); + + return make_reg(0, saves.size()); +} + +reg_t kMakeSaveCatName(EngineState *s, int argc, reg_t *argv) { + // ScummVM does not use SCI catalogues for save games, but game scripts try + // to write out catalogues manually after a save game is deleted, so we need + // to be able to identify and ignore these IO operations by always giving + // back a fixed catalogue name and then ignoring it in kFileIO + SciArray &outCatName = *s->_segMan->lookupArray(argv[0]); + outCatName.fromString("fake.cat"); + return argv[0]; +} + +reg_t kMakeSaveFileName(EngineState *s, int argc, reg_t *argv) { + SciArray &outFileName = *s->_segMan->lookupArray(argv[0]); + // argv[1] is the game name, which is not used by ScummVM + const int16 saveNo = argv[2].toSint16(); + outFileName.fromString(g_sci->getSavegameName(saveNo + kSaveIdShift)); + return argv[0]; +} + #endif } // End of namespace Sci |