scummvm/engines/sci/engine/kfile.cpp
Martin Kiewitz d42d7ad6c5 SCI: allowing kCheckSaveGame with id 0
qfg2 - when trying to restore from an empty saved game list

svn-id: r51663
2010-08-02 21:44:41 +00:00

1014 lines
32 KiB
C++

/* 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.
*
* $URL$
* $Id$
*
*/
#include "common/archive.h"
#include "common/file.h"
#include "common/str.h"
#include "common/savefile.h"
#include "sci/sci.h"
#include "sci/engine/state.h"
#include "sci/engine/kernel.h"
#include "sci/engine/savegame.h"
#include "sci/console.h"
namespace Sci {
struct SavegameDesc {
uint id;
int virtualId; // straight numbered, according to id but w/o gaps
int date;
int time;
int version;
char name[SCI_MAX_SAVENAME_LENGTH];
};
/*
* Note on how file I/O is implemented: In ScummVM, one can not create/write
* arbitrary data files, simply because many of our target platforms do not
* support this. The only files one can create are savestates. But SCI has an
* opcode to create and write to seemingly 'arbitrary' files. This is mainly
* used in LSL3 for LARRY3.DRV (which is a game data file, not a driver) and
* in LSL5 for MEMORY.DRV (which is again a game data file and contains the
* game's password).
* To implement that opcode, we combine the SaveFileManager with regular file
* code, similarly to how the SCUMM HE engine does it.
*
* To handle opening a file called "foobar", what we do is this: First, we
* create an 'augmented file name', by prepending the game target and a dash,
* so if we running game target sq1sci, the name becomes "sq1sci-foobar".
* Next, we check if such a file is known to the SaveFileManager. If so, we
* we use that for reading/writing, delete it, whatever.
*
* If no such file is present but we were only asked to *read* the file,
* we fallback to looking for a regular file called "foobar", and open that
* for reading only.
*/
FileHandle::FileHandle() : _in(0), _out(0) {
}
FileHandle::~FileHandle() {
close();
}
void FileHandle::close() {
delete _in;
delete _out;
_in = 0;
_out = 0;
_name.clear();
}
bool FileHandle::isOpen() const {
return _in || _out;
}
enum {
_K_FILE_MODE_OPEN_OR_CREATE = 0,
_K_FILE_MODE_OPEN_OR_FAIL = 1,
_K_FILE_MODE_CREATE = 2
};
reg_t file_open(EngineState *s, const char *filename, int mode) {
// QfG3 character import prepends /\ to the filenames.
if (filename[0] == '/' && filename[1] == '\\')
filename += 2;
Common::String englishName = g_sci->getSciLanguageString(filename, K_LANG_ENGLISH);
const Common::String wrappedName = g_sci->wrapFilename(englishName);
Common::SeekableReadStream *inFile = 0;
Common::WriteStream *outFile = 0;
Common::SaveFileManager *saveFileMan = g_engine->getSaveFileManager();
if (mode == _K_FILE_MODE_OPEN_OR_FAIL) {
// Try to open file, abort if not possible
inFile = saveFileMan->openForLoading(wrappedName);
// If no matching savestate exists: fall back to reading from a regular
// file
if (!inFile)
inFile = SearchMan.createReadStreamForMember(englishName);
// Special case for LSL3: It tries to create a new dummy file,
// LARRY3.DRV. Apparently, if the file doesn't exist here, it should be
// created. The game scripts then go ahead and fill its contents with
// data. It seems to be a similar case as the dummy MEMORY.DRV file in
// LSL5, but LSL5 creates the file if it can't find it with a separate
// call to file_open().
if (!inFile && englishName == "LARRY3.DRV") {
outFile = saveFileMan->openForSaving(wrappedName);
outFile->finalize();
delete outFile;
outFile = 0;
inFile = SearchMan.createReadStreamForMember(wrappedName);
}
if (!inFile)
debugC(2, kDebugLevelFile, " -> file_open(_K_FILE_MODE_OPEN_OR_FAIL): failed to open file '%s'", englishName.c_str());
} else if (mode == _K_FILE_MODE_CREATE) {
// Create the file, destroying any content it might have had
outFile = saveFileMan->openForSaving(wrappedName);
if (!outFile)
debugC(2, kDebugLevelFile, " -> file_open(_K_FILE_MODE_CREATE): failed to create file '%s'", englishName.c_str());
} else if (mode == _K_FILE_MODE_OPEN_OR_CREATE) {
// Try to open file, create it if it doesn't exist
outFile = saveFileMan->openForSaving(wrappedName);
if (!outFile)
debugC(2, kDebugLevelFile, " -> file_open(_K_FILE_MODE_CREATE): failed to create file '%s'", englishName.c_str());
// QfG1 opens the character export file with _K_FILE_MODE_CREATE first,
// closes it immediately and opens it again with this here. Perhaps
// other games use this for read access as well. I guess changing this
// whole code into using virtual files and writing them after close
// would be more appropriate.
} else {
error("file_open: unsupported mode %d (filename '%s')", mode, englishName.c_str());
}
if (!inFile && !outFile) { // Failed
debugC(2, kDebugLevelFile, " -> file_open() failed");
return SIGNAL_REG;
}
// Find a free file handle
uint handle = 1; // Ignore _fileHandles[0]
while ((handle < s->_fileHandles.size()) && s->_fileHandles[handle].isOpen())
handle++;
if (handle == s->_fileHandles.size()) {
// Hit size limit => Allocate more space
s->_fileHandles.resize(s->_fileHandles.size() + 1);
}
s->_fileHandles[handle]._in = inFile;
s->_fileHandles[handle]._out = outFile;
s->_fileHandles[handle]._name = englishName;
debugC(2, kDebugLevelFile, " -> opened file '%s' with handle %d", englishName.c_str(), handle);
return make_reg(0, handle);
}
reg_t kFOpen(EngineState *s, int argc, reg_t *argv) {
Common::String name = s->_segMan->getString(argv[0]);
int mode = argv[1].toUint16();
debugC(2, kDebugLevelFile, "kFOpen(%s,0x%x)", name.c_str(), mode);
return file_open(s, name.c_str(), mode);
}
static FileHandle *getFileFromHandle(EngineState *s, uint handle) {
if (handle == 0) {
error("Attempt to use file handle 0");
return 0;
}
if ((handle >= s->_fileHandles.size()) || !s->_fileHandles[handle].isOpen()) {
warning("Attempt to use invalid/unused file handle %d", handle);
return 0;
}
return &s->_fileHandles[handle];
}
reg_t kFClose(EngineState *s, int argc, reg_t *argv) {
debugC(2, kDebugLevelFile, "kFClose(%d)", argv[0].toUint16());
if (argv[0] != SIGNAL_REG) {
FileHandle *f = getFileFromHandle(s, argv[0].toUint16());
if (f)
f->close();
}
return s->r_acc;
}
reg_t kFPuts(EngineState *s, int argc, reg_t *argv) {
int handle = argv[0].toUint16();
Common::String data = s->_segMan->getString(argv[1]);
FileHandle *f = getFileFromHandle(s, handle);
if (f)
f->_out->write(data.c_str(), data.size());
return s->r_acc;
}
static void fgets_wrapper(EngineState *s, char *dest, int maxsize, int handle) {
FileHandle *f = getFileFromHandle(s, handle);
if (!f)
return;
if (!f->_in) {
error("fgets_wrapper: Trying to read from file '%s' opened for writing", f->_name.c_str());
return;
}
if (maxsize > 1) {
memset(dest, 0, maxsize);
f->_in->readLine(dest, maxsize);
// The returned string must not have an ending LF
int strSize = strlen(dest);
if (strSize > 0) {
if (dest[strSize - 1] == 0x0A)
dest[strSize - 1] = 0;
}
} else {
*dest = f->_in->readByte();
}
debugC(2, kDebugLevelFile, " -> FGets'ed \"%s\"", dest);
}
reg_t kFGets(EngineState *s, int argc, reg_t *argv) {
int maxsize = argv[1].toUint16();
char *buf = new char[maxsize];
int handle = argv[2].toUint16();
debugC(2, kDebugLevelFile, "kFGets(%d, %d)", handle, maxsize);
fgets_wrapper(s, buf, maxsize, handle);
s->_segMan->memcpy(argv[0], (const byte*)buf, maxsize);
return argv[0];
}
/**
* Writes the cwd to the supplied address and returns the address in acc.
*/
reg_t kGetCWD(EngineState *s, int argc, reg_t *argv) {
// We do not let the scripts see the file system, instead pretending
// we are always in the same directory.
// TODO/FIXME: Is "/" a good value? Maybe "" or "." or "C:\" are better?
s->_segMan->strcpy(argv[0], "/");
debugC(2, kDebugLevelFile, "kGetCWD() -> %s", "/");
return argv[0];
}
static void listSavegames(Common::Array<SavegameDesc> &saves);
static int findSavegame(Common::Array<SavegameDesc> &saves, uint saveId);
enum {
K_DEVICE_INFO_GET_DEVICE = 0,
K_DEVICE_INFO_GET_CURRENT_DEVICE = 1,
K_DEVICE_INFO_PATHS_EQUAL = 2,
K_DEVICE_INFO_IS_FLOPPY = 3,
K_DEVICE_INFO_GET_CONFIG_PATH = 5,
K_DEVICE_INFO_GET_SAVECAT_NAME = 7,
K_DEVICE_INFO_GET_SAVEFILE_NAME = 8
};
reg_t kDeviceInfo(EngineState *s, int argc, reg_t *argv) {
if (g_sci->getGameId() == GID_FANMADE && argc == 1) {
// WORKAROUND: The fan game script library calls kDeviceInfo with one parameter.
// According to the scripts, it wants to call CurDevice. However, it fails to
// provide the subop to the function.
s->_segMan->strcpy(argv[0], "/");
return s->r_acc;
}
int mode = argv[0].toUint16();
switch (mode) {
case K_DEVICE_INFO_GET_DEVICE: {
Common::String input_str = s->_segMan->getString(argv[1]);
s->_segMan->strcpy(argv[2], "/");
debug(3, "K_DEVICE_INFO_GET_DEVICE(%s) -> %s", input_str.c_str(), "/");
break;
}
case K_DEVICE_INFO_GET_CURRENT_DEVICE:
s->_segMan->strcpy(argv[1], "/");
debug(3, "K_DEVICE_INFO_GET_CURRENT_DEVICE() -> %s", "/");
break;
case K_DEVICE_INFO_PATHS_EQUAL: {
Common::String path1_s = s->_segMan->getString(argv[1]);
Common::String path2_s = s->_segMan->getString(argv[2]);
debug(3, "K_DEVICE_INFO_PATHS_EQUAL(%s,%s)", path1_s.c_str(), path2_s.c_str());
return make_reg(0, Common::matchString(path2_s.c_str(), path1_s.c_str(), false, true));
}
break;
case K_DEVICE_INFO_IS_FLOPPY: {
Common::String input_str = s->_segMan->getString(argv[1]);
debug(3, "K_DEVICE_INFO_IS_FLOPPY(%s)", input_str.c_str());
return NULL_REG; /* Never */
}
case K_DEVICE_INFO_GET_CONFIG_PATH: {
// Early versions return drive letter, later versions a path string
// FIXME: Implement if needed, for now return NULL_REG
return NULL_REG;
}
/* SCI uses these in a less-than-portable way to delete savegames.
** Read http://www-plan.cs.colorado.edu/creichen/freesci-logs/2005.10/log20051019.html
** for more information on our workaround for this.
*/
case K_DEVICE_INFO_GET_SAVECAT_NAME: {
Common::String game_prefix = s->_segMan->getString(argv[2]);
s->_segMan->strcpy(argv[1], "__throwaway");
debug(3, "K_DEVICE_INFO_GET_SAVECAT_NAME(%s) -> %s", game_prefix.c_str(), "__throwaway");
}
break;
case K_DEVICE_INFO_GET_SAVEFILE_NAME: {
Common::String game_prefix = s->_segMan->getString(argv[2]);
uint virtualId = argv[3].toUint16();
s->_segMan->strcpy(argv[1], "__throwaway");
debug(3, "K_DEVICE_INFO_GET_SAVEFILE_NAME(%s,%d) -> %s", game_prefix.c_str(), virtualId, "__throwaway");
if ((virtualId < SAVEGAMEID_OFFICIALRANGE_START) || (virtualId > SAVEGAMEID_OFFICIALRANGE_END))
error("kDeviceInfo(deleteSave): invalid savegame-id specified");
uint savegameId = virtualId - SAVEGAMEID_OFFICIALRANGE_START;
Common::Array<SavegameDesc> saves;
listSavegames(saves);
if (findSavegame(saves, savegameId) != -1) {
// Confirmed that this id still lives...
Common::String filename = g_sci->getSavegameName(savegameId);
Common::SaveFileManager *saveFileMan = g_engine->getSaveFileManager();
saveFileMan->removeSavefile(filename);
}
break;
}
default:
error("Unknown DeviceInfo() sub-command: %d", mode);
break;
}
return s->r_acc;
}
reg_t kGetSaveDir(EngineState *s, int argc, reg_t *argv) {
#ifdef ENABLE_SCI32
// TODO: SCI32 uses a parameter here.
if (argc > 0)
warning("kGetSaveDir called with %d parameter(s): %04x:%04x", argc, PRINT_REG(argv[0]));
#endif
return make_reg(s->_segMan->getSysStringsSegment(), SYS_STRING_SAVEDIR);
}
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());
}
}
Common::String path = s->_segMan->getString(argv[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);
}
static bool _savegame_sort_byDate(const SavegameDesc &l, const SavegameDesc &r) {
if (l.date != r.date)
return (l.date > r.date);
return (l.time > r.time);
}
// Create a sorted array containing all found savedgames
static void listSavegames(Common::Array<SavegameDesc> &saves) {
Common::SaveFileManager *saveFileMan = g_engine->getSaveFileManager();
// Load all saves
Common::StringArray saveNames = saveFileMan->listSavefiles(g_sci->getSavegamePattern());
for (Common::StringArray::const_iterator iter = saveNames.begin(); iter != saveNames.end(); ++iter) {
Common::String filename = *iter;
Common::SeekableReadStream *in;
if ((in = saveFileMan->openForLoading(filename))) {
SavegameMetadata meta;
if (!get_savegame_metadata(in, &meta) || meta.savegame_name.empty()) {
// invalid
delete in;
continue;
}
delete in;
SavegameDesc desc;
desc.id = strtol(filename.end() - 3, NULL, 10);
desc.date = meta.savegame_date;
// We need to fix date in here, because we save DDMMYYYY instead of
// YYYYMMDD, so sorting wouldn't work
desc.date = ((desc.date & 0xFFFF) << 16) | ((desc.date & 0xFF0000) >> 8) | ((desc.date & 0xFF000000) >> 24);
desc.time = meta.savegame_time;
desc.version = meta.savegame_version;
if (meta.savegame_name.lastChar() == '\n')
meta.savegame_name.deleteLastChar();
Common::strlcpy(desc.name, meta.savegame_name.c_str(), SCI_MAX_SAVENAME_LENGTH);
debug(3, "Savegame in file %s ok, id %d", filename.c_str(), desc.id);
saves.push_back(desc);
}
}
// Sort the list by creation date of the saves
Common::sort(saves.begin(), saves.end(), _savegame_sort_byDate);
}
// Find a savedgame according to virtualId and return the position within our array
static int findSavegame(Common::Array<SavegameDesc> &saves, uint savegameId) {
for (uint saveNr = 0; saveNr < saves.size(); saveNr++) {
if (saves[saveNr].id == savegameId)
return saveNr;
}
return -1;
}
// The scripts get IDs ranging from 1000->1999, because the scripts require us to assign unique ids THAT EVEN STAY BETWEEN
// SAVES and the scripts also use "saves-count + 1" to create a new savedgame slot.
// SCI1.1 actually recycles ids, in that case we will currently get "0".
// This behaviour is required especially for LSL6. In this game, it's possible to quick save. The scripts will use
// the last-used id for that feature. If we don't assign sticky ids, the feature will overwrite different saves all the
// time. And sadly we can't just use the actual filename ids directly, because of the creation method for new slots.
bool Console::cmdListSaves(int argc, const char **argv) {
Common::Array<SavegameDesc> saves;
listSavegames(saves);
for (uint i = 0; i < saves.size(); i++) {
Common::String filename = g_sci->getSavegameName(saves[i].id);
DebugPrintf("%s: '%s'\n", filename.c_str(), saves[i].name);
}
return true;
}
reg_t kCheckSaveGame(EngineState *s, int argc, reg_t *argv) {
Common::String game_id = s->_segMan->getString(argv[0]);
uint16 virtualId = argv[1].toUint16();
debug(3, "kCheckSaveGame(%s, %d)", game_id.c_str(), virtualId);
Common::Array<SavegameDesc> saves;
listSavegames(saves);
// we allow 0 (happens in QfG2 when trying to restore from an empty saved game list) and return false in that case
if (virtualId == 0)
return NULL_REG;
// Find saved-game
if ((virtualId < SAVEGAMEID_OFFICIALRANGE_START) || (virtualId > SAVEGAMEID_OFFICIALRANGE_END))
error("kCheckSaveGame: called with invalid savegameId!");
uint savegameId = virtualId - SAVEGAMEID_OFFICIALRANGE_START;
int savegameNr = findSavegame(saves, savegameId);
if (savegameNr == -1)
return NULL_REG;
// Check for compatible savegame version
int ver = saves[savegameNr].version;
if (ver < MINIMUM_SAVEGAME_VERSION || ver > CURRENT_SAVEGAME_VERSION)
return NULL_REG;
// Otherwise we assume the savegame is OK
return TRUE_REG;
}
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;
Common::Array<SavegameDesc> saves;
listSavegames(saves);
uint totalSaves = MIN<uint>(saves.size(), MAX_SAVEGAME_NR);
reg_t *slot = s->_segMan->derefRegPtr(argv[2], totalSaves);
if (!slot) {
warning("kGetSaveFiles: %04X:%04X invalid or too small to hold slot data", PRINT_REG(argv[2]));
totalSaves = 0;
}
const uint bufSize = (totalSaves * SCI_MAX_SAVENAME_LENGTH) + 1;
char *saveNames = new char[bufSize];
char *saveNamePtr = saveNames;
for (uint i = 0; i < totalSaves; i++) {
*slot++ = make_reg(0, saves[i].id + SAVEGAMEID_OFFICIALRANGE_START); // Store the virtual savegame-id ffs. see above
strcpy(saveNamePtr, saves[i].name);
saveNamePtr += SCI_MAX_SAVENAME_LENGTH;
}
*saveNamePtr = 0; // Terminate list
s->_segMan->memcpy(argv[1], (byte *)saveNames, bufSize);
delete[] saveNames;
return make_reg(0, totalSaves);
}
reg_t kSaveGame(EngineState *s, int argc, reg_t *argv) {
Common::String game_id = s->_segMan->getString(argv[0]);
uint virtualId = argv[1].toUint16();
Common::String game_description = s->_segMan->getString(argv[2]);
Common::String version;
if (argc > 3)
version = s->_segMan->getString(argv[3]);
debug(3, "kSaveGame(%s,%d,%s,%s)", game_id.c_str(), virtualId, game_description.c_str(), version.c_str());
// We check here, we don't want to delete a users save in case we are within a kernel function
if (s->executionStackBase) {
warning("kSaveGame - won't save from within kernel function");
return NULL_REG;
}
Common::Array<SavegameDesc> saves;
listSavegames(saves);
uint savegameId;
if ((virtualId >= SAVEGAMEID_OFFICIALRANGE_START) && (virtualId <= SAVEGAMEID_OFFICIALRANGE_END)) {
// savegameId is an actual Id, so search for it just to make sure
savegameId = virtualId - SAVEGAMEID_OFFICIALRANGE_START;
if (findSavegame(saves, savegameId) == -1)
return NULL_REG;
} else if (virtualId < SAVEGAMEID_OFFICIALRANGE_START) {
// virtualId is low, we assume that scripts expect us to create new slot
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 = 0; savegameId < SAVEGAMEID_OFFICIALRANGE_START; savegameId++) {
for (savegameNr = 0; savegameNr < saves.size(); savegameNr++) {
if (savegameId == saves[savegameNr].id)
break;
}
if (savegameNr == saves.size())
break;
}
if (savegameId == SAVEGAMEID_OFFICIALRANGE_START)
error("kSavegame: no more savegame slots available");
}
} else {
error("kSaveGame: invalid savegameId used");
}
// Save in case caller wants to overwrite last newly created save
s->_lastSaveVirtualId = virtualId;
s->_lastSaveNewId = savegameId;
Common::String filename = g_sci->getSavegameName(savegameId);
Common::SaveFileManager *saveFileMan = g_engine->getSaveFileManager();
Common::OutSaveFile *out;
if (!(out = saveFileMan->openForSaving(filename))) {
warning("Error opening savegame \"%s\" for writing", filename.c_str());
s->r_acc = NULL_REG;
return NULL_REG;
}
if (!gamestate_save(s, out, game_description.c_str(), version.c_str())) {
warning("Saving the game failed.");
s->r_acc = NULL_REG;
} else {
out->finalize();
if (out->err()) {
delete out;
warning("Writing the savegame failed.");
s->r_acc = NULL_REG;
} else {
delete out;
s->r_acc = make_reg(0, 1);
}
}
return s->r_acc;
}
reg_t kRestoreGame(EngineState *s, int argc, reg_t *argv) {
Common::String game_id = !argv[0].isNull() ? s->_segMan->getString(argv[0]) : "";
uint savegameId = argv[1].toUint16();
debug(3, "kRestoreGame(%s,%d)", game_id.c_str(), savegameId);
if (argv[0].isNull()) {
// Loading from the launcher, don't adjust the ID of the saved game
} else {
if ((savegameId < 1000) || (savegameId > 1999)) {
warning("Savegame ID %d is not allowed", savegameId);
return TRUE_REG;
}
savegameId -= 1000;
}
Common::Array<SavegameDesc> saves;
listSavegames(saves);
if (findSavegame(saves, savegameId) == -1) {
warning("Savegame ID %d not found", savegameId);
return TRUE_REG;
}
Common::SaveFileManager *saveFileMan = g_engine->getSaveFileManager();
Common::String filename = g_sci->getSavegameName(savegameId);
Common::SeekableReadStream *in;
if ((in = saveFileMan->openForLoading(filename))) {
// found a savegame file
gamestate_restore(s, in);
delete in;
return s->r_acc;
}
s->r_acc = TRUE_REG;
warning("Savegame #%d not found", savegameId);
return s->r_acc;
}
reg_t kValidPath(EngineState *s, int argc, reg_t *argv) {
Common::String path = s->_segMan->getString(argv[0]);
debug(3, "kValidPath(%s) -> %d", path.c_str(), s->r_acc.offset);
// Always return true
return make_reg(0, 1);
}
reg_t DirSeeker::firstFile(const Common::String &mask, reg_t buffer, SegManager *segMan) {
// Verify that we are given a valid buffer
if (!buffer.segment) {
error("DirSeeker::firstFile('%s') invoked with invalid buffer", mask.c_str());
return NULL_REG;
}
_outbuffer = buffer;
// Prefix the mask
const Common::String wrappedMask = g_sci->wrapFilename(mask);
// Obtain a list of all savefiles matching the given mask
Common::SaveFileManager *saveFileMan = g_engine->getSaveFileManager();
_savefiles = saveFileMan->listSavefiles(wrappedMask);
// Reset the list iterator and write the first match to the output buffer,
// if any.
_iter = _savefiles.begin();
return nextFile(segMan);
}
reg_t DirSeeker::nextFile(SegManager *segMan) {
if (_iter == _savefiles.end()) {
return NULL_REG;
}
const Common::String wrappedString = *_iter;
// Strip the prefix
Common::String string = g_sci->unwrapFilename(wrappedString);
if (string.size() > 12)
string = Common::String(string.c_str(), 12);
segMan->strcpy(_outbuffer, string.c_str());
// Return the result and advance the list iterator :)
++_iter;
return _outbuffer;
}
reg_t kFileIO(EngineState *s, int argc, reg_t *argv) {
if (!s)
return make_reg(0, getSciVersion());
error("not supposed to call this");
}
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();
// SQ4 floppy prepends /\ to the filenames
if (name.hasPrefix("/\\")) {
name.deleteChar(0);
name.deleteChar(0);
}
// SQ4 floppy attempts to update the savegame index file sq4sg.dir when
// deleting saved games. We don't use an index file for saving or
// loading, so just stop the game from modifying the file here in order
// to avoid having it saved in the ScummVM save directory.
if (name == "sq4sg.dir") {
debugC(2, kDebugLevelFile, "Not opening unused file sq4sg.dir");
return SIGNAL_REG;
}
if (name.empty()) {
warning("Attempted to open a file with an empty filename");
return SIGNAL_REG;
}
debugC(2, kDebugLevelFile, "kFileIO(open): %s, 0x%x", name.c_str(), mode);
return file_open(s, name.c_str(), mode);
}
reg_t kFileIOClose(EngineState *s, int argc, reg_t *argv) {
debugC(2, kDebugLevelFile, "kFileIO(close): %d", argv[0].toUint16());
FileHandle *f = getFileFromHandle(s, argv[0].toUint16());
if (f) {
f->close();
return SIGNAL_REG;
}
return NULL_REG;
}
reg_t kFileIOReadRaw(EngineState *s, int argc, reg_t *argv) {
int handle = argv[0].toUint16();
int size = argv[2].toUint16();
int bytesRead = 0;
char *buf = new char[size];
debugC(2, kDebugLevelFile, "kFileIO(readRaw): %d, %d", handle, size);
FileHandle *f = getFileFromHandle(s, handle);
if (f) {
bytesRead = f->_in->read(buf, size);
s->_segMan->memcpy(argv[1], (const byte*)buf, size);
}
delete[] buf;
return make_reg(0, bytesRead);
}
reg_t kFileIOWriteRaw(EngineState *s, int argc, reg_t *argv) {
int handle = argv[0].toUint16();
int size = argv[2].toUint16();
char *buf = new char[size];
bool success = false;
s->_segMan->memcpy((byte*)buf, argv[1], size);
debugC(2, kDebugLevelFile, "kFileIO(writeRaw): %d, %d", handle, size);
FileHandle *f = getFileFromHandle(s, handle);
if (f) {
f->_out->write(buf, size);
success = true;
}
delete[] buf;
if (success)
return NULL_REG;
return make_reg(0, 6); // DOS - invalid handle
}
reg_t kFileIOUnlink(EngineState *s, int argc, reg_t *argv) {
Common::String name = s->_segMan->getString(argv[0]);
Common::SaveFileManager *saveFileMan = g_engine->getSaveFileManager();
bool result;
// SQ4 floppy prepends /\ to the filenames
if (name.hasPrefix("/\\")) {
name.deleteChar(0);
name.deleteChar(0);
}
// Special case for SQ4 floppy: This game has hardcoded names for all of
// its savegames, and they are all named "sq4sg.xxx", where xxx is the
// slot. We just take the slot number here, and delete the appropriate
// save game.
if (name.hasPrefix("sq4sg.")) {
// Special handling for SQ4... get the slot number and construct the
// save game name.
int slotNum = atoi(name.c_str() + name.size() - 3);
Common::Array<SavegameDesc> saves;
listSavegames(saves);
int savedir_nr = saves[slotNum].id;
name = g_sci->getSavegameName(savedir_nr);
result = saveFileMan->removeSavefile(name);
} else {
const Common::String wrappedName = g_sci->wrapFilename(name);
result = saveFileMan->removeSavefile(wrappedName);
}
debugC(2, kDebugLevelFile, "kFileIO(unlink): %s", name.c_str());
if (result)
return NULL_REG;
return make_reg(0, 2); // DOS - file not found error code
}
reg_t kFileIOReadString(EngineState *s, int argc, reg_t *argv) {
int size = argv[1].toUint16();
char *buf = new char[size];
int handle = argv[2].toUint16();
debugC(2, kDebugLevelFile, "kFileIO(readString): %d, %d", handle, size);
fgets_wrapper(s, buf, size, handle);
s->_segMan->memcpy(argv[0], (const byte*)buf, size);
delete[] buf;
return argv[0];
}
reg_t kFileIOWriteString(EngineState *s, int argc, reg_t *argv) {
int handle = argv[0].toUint16();
Common::String str = s->_segMan->getString(argv[1]);
debugC(2, kDebugLevelFile, "kFileIO(writeString): %d", handle);
FileHandle *f = getFileFromHandle(s, handle);
if (f)
f->_out->write(str.c_str(), str.size());
return NULL_REG;
return make_reg(0, 6); // DOS - invalid handle
}
reg_t kFileIOSeek(EngineState *s, int argc, reg_t *argv) {
int handle = argv[0].toUint16();
int offset = argv[1].toUint16();
int whence = argv[2].toUint16();
debugC(2, kDebugLevelFile, "kFileIO(seek): %d, %d, %d", handle, offset, whence);
FileHandle *f = getFileFromHandle(s, handle);
if (f)
s->r_acc = make_reg(0, f->_in->seek(offset, whence));
return SIGNAL_REG;
}
reg_t kFileIOFindFirst(EngineState *s, int argc, reg_t *argv) {
Common::String mask = s->_segMan->getString(argv[0]);
reg_t buf = argv[1];
int attr = argv[2].toUint16(); // We won't use this, Win32 might, though...
debugC(2, kDebugLevelFile, "kFileIO(findFirst): %s, 0x%x", mask.c_str(), attr);
// QfG3 uses "/\*.*" for the character import, QfG4 uses "/\*"
if (mask.hasPrefix("/\\")) {
mask.deleteChar(0);
mask.deleteChar(0);
}
// We remove ".*". mask will get prefixed, so we will return all additional files for that gameid
if (mask == "*.*")
mask = "*";
return s->_dirseeker.firstFile(mask, buf, s->_segMan);
}
reg_t kFileIOFindNext(EngineState *s, int argc, reg_t *argv) {
debugC(2, kDebugLevelFile, "kFileIO(findNext)");
return s->_dirseeker.nextFile(s->_segMan);
}
reg_t kFileIOExists(EngineState *s, int argc, reg_t *argv) {
Common::String name = s->_segMan->getString(argv[0]);
// Check for regular file
bool exists = Common::File::exists(name);
Common::SaveFileManager *saveFileMan = g_engine->getSaveFileManager();
const Common::String wrappedName = g_sci->wrapFilename(name);
if (!exists)
exists = !saveFileMan->listSavefiles(name).empty();
if (!exists) {
// Try searching for the file prepending target-
Common::SeekableReadStream *inFile = saveFileMan->openForLoading(wrappedName);
exists = (inFile != 0);
delete inFile;
}
// Special case for non-English versions of LSL5: The English version of
// LSL5 calls kFileIO(), case K_FILEIO_OPEN for reading to check if
// memory.drv exists (which is where the game's password is stored). If
// it's not found, it calls kFileIO() again, case K_FILEIO_OPEN for
// writing and creates a new file. Non-English versions call kFileIO(),
// case K_FILEIO_FILE_EXISTS instead, and fail if memory.drv can't be
// found. We create a default memory.drv file with no password, so that
// the game can continue.
if (!exists && name == "memory.drv") {
// Create a new file, and write the bytes for the empty password
// string inside
byte defaultContent[] = { 0xE9, 0xE9, 0xEB, 0xE1, 0x0D, 0x0A, 0x31, 0x30, 0x30, 0x30 };
Common::WriteStream *outFile = saveFileMan->openForSaving(wrappedName);
for (int i = 0; i < 10; i++)
outFile->writeByte(defaultContent[i]);
outFile->finalize();
delete outFile;
exists = true;
}
debugC(2, kDebugLevelFile, "kFileIO(fileExists) %s -> %d", name.c_str(), exists);
return make_reg(0, exists);
}
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]);
// 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_engine->getSaveFileManager()->renameSavefile(oldName, newName))
return NULL_REG;
else
return SIGNAL_REG;
}
#ifdef ENABLE_SCI32
reg_t kFileIOReadByte(EngineState *s, int argc, reg_t *argv) {
// Read the byte into the low byte of the accumulator
FileHandle *f = getFileFromHandle(s, argv[0].toUint16());
if (!f)
return NULL_REG;
return make_reg(0, (s->r_acc.toUint16() & 0xff00) | f->_in->readByte());
}
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 doesn't return anything?
}
reg_t kFileIOReadWord(EngineState *s, int argc, reg_t *argv) {
FileHandle *f = getFileFromHandle(s, argv[0].toUint16());
if (!f)
return NULL_REG;
return make_reg(0, f->_in->readUint16LE());
}
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 doesn't return anything?
}
reg_t kCD(EngineState *s, int argc, reg_t *argv) {
// TODO: Stub
switch (argv[0].toUint16()) {
case 0:
// Return whether the contents of disc argv[1] is available.
return TRUE_REG;
default:
warning("CD(%d)", argv[0].toUint16());
}
return NULL_REG;
}
reg_t kSave(EngineState *s, int argc, reg_t *argv) {
switch (argv[0].toUint16()) {
case 0: // Called by kq7 when starting chapters
return SIGNAL_REG;
case 2: // GetSaveDir
// Yay! Reusing the old kernel function!
return kGetSaveDir(s, argc - 1, argv + 1);
case 8:
// TODO
// This function has to return something other than 0 to proceed
return s->r_acc;
default:
warning("Unknown/unhandled kSave subop %d", argv[0].toUint16());
}
return NULL_REG;
}
#endif
} // End of namespace Sci