/* 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 3 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, see . * */ /************************************* * * USED IN: * Standard Director Xtra * Karma: Curse of the 12 Caves * *************************************/ /* * -- XObject version * * --FileIO, Tool, 1.5.0 , 31mar92 * --© 1989-1992 MacroMind, Inc. * --by John Thompson and Al McNeil * --------------------------------------------------------------------- * --------------------------------------------------------------------- * --=METHODS=-- * -- * ISS mNew, mode, fileNameOrType --Creates a new instance of the XObject. * -- Mode can be : * -- "read" - Read "fileName" * -- "?read" - Select and Read "fileType" * -- "write" - Write "fileName" * -- "?write" - Select and Write "fileName" * -- "append" - Append "fileName" * -- "?append" - Select and Append "fileName" * -- FileType for ?read can be : * -- "TEXT" - standard file type * -- "trak" - cd track type * -- etc... - Any four character combination. * -- * X mDispose --Disposes of XObject instance. * S mName --Returns the name of the XObject. * /IXX mWrite, countPtr, bufferPtr --Writes out a set of chars. Returns error code. * II mWriteChar, charNum --Writes a single character. Returns error code. * IS mWriteString, string --Writes out a string of chars. Returns error code. * /IXX mRead, countPtr, bufferPtr --Reads into buffer. Returns error code. * I mReadChar --Returns a single character. * S mReadWord --Returns the next word of an input file. * S mReadLine --Returns the next line of an input file. * S mReadFile --Returns the remainder of the file. * -- * SSS mReadToken, breakString, skipString * -- --breakstring designates character (or token) that signals to stop reading. * -- --skipstring designates what characters (or tokens) not to read. * I mGetPosition --Returns the file position. * II mSetPosition, newPos --Sets the file position. Returns error code. * I mGetLength --Returns the number of chars in the file. * ISS mSetFinderInfo, typeString, creatorString --Sets the finder info. Returns error code. * S mGetFinderInfo --Gets the finder info. * S mFileName --Returns the name of the file. * S mNativeFileName --Returns the name of the file. * I mDelete --Delete the file and dispose of me. * I mStatus --Returns result code of the last file io activity * -- * SI +mError, errorCode --Returns error message string. * -- Possible error codes: * -- -33 :: File directory full * -- -34 :: Volume full * -- -35 :: Volume not found * -- -36 :: I/O Error * -- -37 :: Bad file name * -- -38 :: File not open * -- -42 :: Too many files open * -- -43 :: File not found * -- -56 :: No such drive * -- -65 :: No disk in drive * -- -120 :: Directory not found * V mReadPICT * II +mSetOverrideDrive, driveLetter --Set override drive letter ('A' - 'Z') to use when loading linked castmembers. Use 0x00 to clear override. * * -- Xtra version -- xtra fileio -- CH May96 new object me -- create a new child instance -- FILEIO -- fileName object me -- return fileName string of the open file status object me -- return the error code of the last method called error object me, int error -- return the error string of the error setFilterMask me, string mask -- set the filter mask for dialogs openFile object me, string fileName, int mode -- opens named file. valid modes: 0=r/w 1=r 2=w closeFile object me -- close the file displayOpen object me -- displays an open dialog and returns the selected fileName to lingo displaySave object me, string title, string defaultFileName -- displays save dialog and returns selected fileName to lingo createFile object me, string fileName -- creates a new file called fileName setPosition object me, int position -- set the file position getPosition object me -- get the file position getLength object me -- get the length of the open file writeChar object me, string theChar -- write a single character (by ASCII code) to the file writeString object me, string theString -- write a null-terminated string to the file readChar object me -- read the next character of the file and return it as an ASCII code value readLine object me -- read the next line of the file (including the next RETURN) and return as a string readFile object me -- read from current position to EOF and return as a string readWord object me -- read the next word of the file and return it as a string readToken object me, string skip, string break -- read the next token and return it as a string getFinderInfo object me -- get the finder info for the open file (Mac Only) setFinderInfo object me, string attributes -- set the finder info for the open file (Mac Only) delete object me -- deletes the open file + version xtraRef -- display fileIO version and build information in the message window * getOSDirectory -- returns the full path to the Mac System Folder or Windows Directory */ #include "common/file.h" #include "common/macresman.h" #include "common/memstream.h" #include "common/savefile.h" #include "image/pict.h" #include "director/director.h" #include "director/picture.h" #include "director/types.h" #include "director/util.h" #include "director/lingo/lingo.h" #include "director/lingo/lingo-object.h" #include "director/lingo/lingo-utils.h" #include "director/lingo/xlibs/f/fileio.h" namespace Director { const char *const FileIO::xlibName = "FileIO"; const XlibFileDesc FileIO::fileNames[] = { { "FileIO", nullptr }, { "shFILEIO", nullptr }, // TD loads this up using openXLib("@:shFILEIO.DLL") { "FILE", nullptr }, { "FILEIO16", nullptr }, { "FileIOXtraFat", nullptr }, { nullptr, nullptr }, }; static const MethodProto xlibMethods[] = { // XObject { "delete", FileIO::m_delete, 0, 0, 200 }, // D2 { "error", FileIO::m_error, 1, 1, 200 }, // D2 { "fileName", FileIO::m_fileName, 0, 0, 200 }, // D2 { "getFinderInfo", FileIO::m_getFinderInfo, 0, 0, 200 }, // D2 { "getLength", FileIO::m_getLength, 0, 0, 200 }, // D2 { "getPosition", FileIO::m_getPosition, 0, 0, 200 }, // D2 { "new", FileIO::m_new, 0, 2, 200 }, // D2 { "readChar", FileIO::m_readChar, 0, 0, 200 }, // D2 { "readFile", FileIO::m_readFile, 0, 0, 200 }, // D2 { "readLine", FileIO::m_readLine, 0, 0, 200 }, // D2 { "readToken", FileIO::m_readToken, 2, 2, 200 }, // D2 { "readPict", FileIO::m_readPict, 0, 0, 300 }, // D3 { "readWord", FileIO::m_readWord, 0, 0, 200 }, // D2 { "setFinderInfo", FileIO::m_setFinderInfo, 2, 2, 200 }, // D2 { "setPosition", FileIO::m_setPosition, 1, 1, 200 }, // D2 { "status", FileIO::m_status, 0, 0, 200 }, // D2 { "writeChar", FileIO::m_writeChar, 1, 1, 200 }, // D2 { "writeString", FileIO::m_writeString, 1, 1, 200 }, // D2 // Windows only? { "setOverrideDrive", FileIO::m_setOverrideDrive, 1, 1, 300 }, // D3 // Xtra { "closeFile", FileIO::m_closeFile, 0, 0, 500 }, // D5 { "createFile", FileIO::m_createFile, 1, 1, 500 }, // D5 { "displayOpen", FileIO::m_displayOpen, 0, 0, 500 }, // D5 { "displaySave", FileIO::m_displaySave, 2, 2, 500 }, // D5 { "openFile", FileIO::m_openFile, 2, 2, 500 }, // D5 { "setFilterMask", FileIO::m_setFilterMask, 1, 1, 500 }, // D5 { nullptr, nullptr, 0, 0, 0 } }; static const BuiltinProto xlibBuiltins[] = { { "getOSDirectory", FileIO::m_getOSDirectory, 0, 0, 500, HBLTIN }, { nullptr, nullptr, 0, 0, 0, VOIDSYM } }; void FileIO::open(ObjectType type, const Common::Path &path) { FileObject::initMethods(xlibMethods); FileObject *xobj = new FileObject(type); if (type == kXtraObj) { g_lingo->_openXtras.push_back(xlibName); g_lingo->_openXtraObjects.push_back(xobj); } g_lingo->exposeXObject(xlibName, xobj); g_lingo->initBuiltIns(xlibBuiltins); } void FileIO::close(ObjectType type) { FileObject::cleanupMethods(); g_lingo->_globalvars[xlibName] = Datum(); } // Initialization/disposal FileObject::FileObject(ObjectType objType) : Object("FileIO") { _objType = objType; _filename = nullptr; _inStream = nullptr; _outFile = nullptr; _outStream = nullptr; _lastError = kErrorNone; } FileObject::FileObject(const FileObject &obj) : Object(obj) { _objType = obj.getObjType(); _filename = nullptr; _inStream = nullptr; _outFile = nullptr; _outStream = nullptr; _lastError = kErrorNone; } FileObject::~FileObject() { clear(); } bool FileObject::hasProp(const Common::String &propName) { return (propName == "name"); } Datum FileObject::getProp(const Common::String &propName) { if (propName == "name") return Datum(FileIO::xlibName); warning("FileIO::getProp: unknown property '%s'", propName.c_str()); return Datum(); } FileIOError FileObject::open(const Common::String &origpath, const Common::String &mode) { Common::SaveFileManager *saves = g_system->getSavefileManager(); Common::String path = origpath; Common::String option = mode; char dirSeparator = g_director->_dirSeparator; Common::String prefix = savePrefix(); if (option.hasPrefix("?")) { option = option.substr(1); path = getFileNameFromModal(option.equalsIgnoreCase("write"), origpath, Common::String(), "txt"); if (path.empty()) { return kErrorFileNotFound; } dirSeparator = '/'; } else if (!path.hasSuffixIgnoreCase(".txt")) { path += ".txt"; } // We pretend that drive E:\ is a read-only CD-ROM // It helps with CD checks in many games if (option.equalsIgnoreCase("write") || option.equalsIgnoreCase("append")) { if (origpath.hasPrefixIgnoreCase("E:\\")) return kErrorIO; } // In general, we assume that there are no drives beyond disk E: if (origpath.size() >= 2 && origpath[1] == ':' && (toupper(origpath[0]) > 'E')) { return kErrorIO; } // Enforce target to the created files so they do not mix up Common::String filenameOrig = lastPathComponent(path, dirSeparator); Common::String filename = filenameOrig; if (!filename.hasPrefixIgnoreCase(prefix)) filename = prefix + filenameOrig; if (option.equalsIgnoreCase("read")) { _inStream = saves->openForLoading(filename); if (!_inStream) { // Maybe we're trying to read one of the game files Common::Path location = findPath(origpath); Common::SeekableReadStream *file = Common::MacResManager::openFileOrDataFork(location); if (!file) { return saveFileError(); } _inStream = file; } } else if (option.equalsIgnoreCase("write")) { // OutSaveFile is not seekable so create a separate seekable stream // which will be written to the _outFile upon disposal _outFile = saves->openForSaving(filename, false); _outStream = new Common::MemoryWriteStreamDynamic(DisposeAfterUse::YES); if (!_outFile) { return saveFileError(); } } else if (option.equalsIgnoreCase("append")) { Common::SeekableReadStream *inFile = saves->openForLoading(filename); if (!inFile) { Common::Path location = findPath(origpath); Common::SeekableReadStream *file = Common::MacResManager::openFileOrDataFork(location); if (!file) { return saveFileError(); } inFile = file; } _outStream = new Common::MemoryWriteStreamDynamic(DisposeAfterUse::YES); byte b = inFile->readByte(); while (!inFile->eos() && !inFile->err()) { _outStream->writeByte(b); b = inFile->readByte(); } delete inFile; _outFile = saves->openForSaving(filename, false); if (!_outFile) { return saveFileError(); } } else { error("Unsupported FileIO option: '%s'", option.c_str()); } _filename = new Common::String(filename); return kErrorNone; } void FileObject::clear() { if (_filename) { delete _filename; _filename = nullptr; } if (_inStream) { delete _inStream; _inStream = nullptr; } if (_outFile) { _outFile->write(_outStream->getData(), _outStream->size()); _outFile->finalize(); delete _outFile; delete _outStream; _outFile = nullptr; _outStream = nullptr; } } void FileObject::dispose() { _disposed = true; clear(); } FileIOError FileObject::saveFileError() { Common::SaveFileManager *saves = g_system->getSavefileManager(); if (saves->getError().getCode()) { warning("SaveFileManager error %d: %s", saves->getError().getCode(), saves->getErrorDesc().c_str()); return kErrorIO; } else { return kErrorFileNotFound; } } void FileIO::m_new(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); Datum result = g_lingo->_state->me; if (nargs == 2) { if (me->getObjType() != kXObj) { warning("FileIO::m_new: called with XObject API however was expecting object type %d", me->getObjType()); } Datum d2 = g_lingo->pop(); Datum d1 = g_lingo->pop(); Common::String option = d1.asString(); Common::String path = d2.asString(); FileIOError err = me->open(path, option); // if there's an error, return an errorcode int instead of an object if (err != kErrorNone) { me->_lastError = err; warning("FileIO::m_new: couldn't open file at path %s, error %d", path.c_str(), err); g_lingo->push(Datum(err)); return; } } else if (nargs == 0) { if (me->getObjType() != kXtraObj) { warning("FileIO::m_new: called with Xtra API however was expecting object type %d", me->getObjType()); } } else { warning("FileIO::m_new: expected 0 or 2 args, assuming 0"); g_lingo->dropStack(nargs); } g_lingo->push(result); } void FileIO::m_openFile(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); Datum d1 = g_lingo->pop(); Datum d2 = g_lingo->pop(); int mode = d1.asInt(); Common::String option; switch (mode) { case 1: option = "read"; break; case 2: option = "write"; break; case 0: default: warning("FIXME: Mode %d not supported, falling back to read", mode); option = "read"; break; } Common::String path = d2.asString(); me->_lastError = me->open(path, option); } void FileIO::m_closeFile(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); me->clear(); } // FIXME: split out filename-to-savegame logic from open() so we can implement createFile XOBJSTUB(FileIO::m_createFile, 0) void FileIO::m_displayOpen(int nargs) { g_lingo->push(getFileNameFromModal(false, Common::String(), Common::String(), "txt")); } void FileIO::m_displaySave(int nargs) { Datum defaultFileName = g_lingo->pop(); Datum title = g_lingo->pop(); g_lingo->push(getFileNameFromModal(true, Common::String(), title.asString(), "txt")); } XOBJSTUB(FileIO::m_setFilterMask, 0) // Read void FileIO::m_readChar(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); if (!me->_inStream || me->_inStream->eos() || me->_inStream->err()) { g_lingo->push(Datum(kErrorEOF)); return; } int ch = me->_inStream->readByte(); if (me->_inStream->eos() || me->_inStream->err()) { ch = kErrorEOF; } g_lingo->push(Datum(ch)); } void FileIO::m_readLine(int nargs) { // file(mReadLine) is equivalent to file(mReadToken, "", RETURN) // See D4 Using Lingo p. 323 g_lingo->push(Datum("")); g_lingo->push(Datum("\r")); FileIO::m_readToken(2); } void FileIO::m_readWord(int nargs) { // file(mReadWord) is equivalent to file(mReadToken, " ", " " & RETURN) // See D4 Using Lingo p. 323 g_lingo->push(Datum(" ")); g_lingo->push(Datum(" \r")); FileIO::m_readToken(2); } void FileIO::m_readPict(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); Datum result; if (!me->_inStream || me->_inStream->err()) { g_lingo->push(result); return; } me->_inStream->seek(0, SEEK_SET); Image::PICTDecoder decoder; if (decoder.loadStream(*me->_inStream)) { result.type = PICTUREREF; result.u.picture = new PictureReference; result.u.picture->_picture = new Picture(decoder); } g_lingo->push(result); return; } bool FileIO::charInMatchString(char ch, const Common::String &matchString) { return matchString.contains(ch); } void FileIO::m_readToken(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); Datum d2 = g_lingo->pop(); Datum d1 = g_lingo->pop(); Common::String skipString = d1.asString(); Common::String breakString = d2.asString(); if (!me->_inStream || me->_inStream->eos() || me->_inStream->err()) { g_lingo->push(Datum("")); return; } Common::String tok = ""; char ch; do { ch = me->_inStream->readByte(); if (me->_inStream->eos() || me->_inStream->err()) { g_lingo->push(Datum(tok)); return; } } while (charInMatchString(ch, skipString)); while (!charInMatchString(ch, breakString)) { tok += ch; ch = me->_inStream->readByte(); if (me->_inStream->eos() || me->_inStream->err()) { g_lingo->push(Datum(tok)); return; } } // Include the break character when the skipString is empty if (skipString.size() == 0) { tok += ch; } else { me->_inStream->seek(-1, SEEK_CUR); } g_lingo->push(Datum(tok)); } void FileIO::m_readFile(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); if (!me->_inStream || me->_inStream->eos() || me->_inStream->err()) { g_lingo->push(Datum("")); return; } Common::String res; char ch = me->_inStream->readByte(); while (!me->_inStream->eos() && !me->_inStream->err()) { res += ch; ch = me->_inStream->readByte(); } g_lingo->push(res); } // Write void FileIO::m_writeChar(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); Datum d = g_lingo->pop(); if (!me->_outStream) { g_lingo->push(Datum(kErrorReadOnly)); return; } me->_outStream->writeByte(d.asInt()); g_lingo->push(Datum(kErrorNone)); } void FileIO::m_writeString(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); Datum d = g_lingo->pop(); if (!me->_outStream) { g_lingo->push(Datum(kErrorReadOnly)); return; } Common::U32String unicodeString = Common::U32String(d.asString(), Common::kUtf8); Common::String encodedString = unicodeString.encode(g_director->getPlatformEncoding()); me->_outStream->writeString(encodedString); g_lingo->push(Datum(kErrorNone)); } // Getters/Setters XOBJSTUB(FileIO::m_getFinderInfo, "") XOBJSTUB(FileIO::m_setFinderInfo, 0) void FileIO::m_getPosition(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); if (me->_inStream) { g_lingo->push(Datum((int)me->_inStream->pos())); } else if (me->_outStream) { g_lingo->push(Datum((int)me->_outStream->pos())); } else { warning("FileIO: No file open"); g_lingo->push(Datum(kErrorFileNotOpen)); } } void FileIO::m_setPosition(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); Datum d = g_lingo->pop(); int pos = d.asInt(); if (me->_inStream) { if (pos <= me->_inStream->size()) { me->_inStream->seek(pos, SEEK_SET); g_lingo->push(Datum(kErrorNone)); } else { me->_inStream->seek(me->_inStream->size(), SEEK_SET); g_lingo->push(Datum(kErrorInvalidPos)); } } else if (me->_outStream) { if (pos <= me->_outStream->size()) { me->_outStream->seek(pos, SEEK_SET); g_lingo->push(Datum(kErrorNone)); } else { me->_outStream->seek(me->_outStream->size(), SEEK_SET); g_lingo->push(Datum(kErrorInvalidPos)); } } else { warning("FileIO: No file open"); g_lingo->push(Datum(kErrorFileNotOpen)); } } void FileIO::m_getLength(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); if (me->_inStream) { g_lingo->push(Datum((int)me->_inStream->size())); } else if (me->_outStream) { g_lingo->push(Datum((int)me->_outStream->size())); } else { warning("FileIO: No file open"); g_lingo->push(Datum(kErrorFileNotOpen)); } } void FileIO::m_fileName(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); if (me->_filename) { Common::String prefix = savePrefix(); Common::String res = *me->_filename; if (res.hasPrefix(prefix)) { res = Common::String(&me->_filename->c_str()[prefix.size()]); } g_lingo->push(Datum(res)); } else { warning("FileIO: No file open"); g_lingo->push(Datum(kErrorFileNotOpen)); } } void FileIO::m_error(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); Datum d = g_lingo->pop(); Datum result(""); switch (d.asInt()) { case kErrorNone: if (me->getObjType() == kXtraObj) { result = Datum("OK"); } break; case kErrorMemAlloc: result = Datum("Memory allocation failure"); break; case kErrorDirectoryFull: result = Datum("File directory full"); break; case kErrorVolumeFull: result = Datum("Volume full"); break; case kErrorVolumeNotFound: result = Datum("Volume not found"); break; case kErrorIO: result = Datum("I/O Error"); break; case kErrorBadFileName: result = Datum("Bad file name"); break; case kErrorFileNotOpen: result = Datum("File not open"); break; case kErrorTooManyFilesOpen: result = Datum("Too many files open"); break; case kErrorFileNotFound: result = Datum("File not found"); break; case kErrorNoSuchDrive: result = Datum("No such drive"); break; case kErrorNoDiskInDrive: result = Datum("No disk in drive"); break; case kErrorDirectoryNotFound: result = Datum("Directory not found"); break; default: result = Datum("Unknown error"); break; } g_lingo->push(result); } void FileIO::m_status(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); g_lingo->push(Datum(me->_lastError)); } // Other void FileIO::m_delete(int nargs) { FileObject *me = static_cast(g_lingo->_state->me.u.obj); if (me->_filename) { Common::String filename = *me->_filename; // Xtra version does not dispose handle on file delete if (me->getObjType() == kXObj) { me->dispose(); } if (g_system->getSavefileManager()->removeSavefile(filename)) { g_lingo->push(Datum(kErrorNone)); } else { g_lingo->push(Datum(kErrorIO)); } } else { warning("FileIO: No file open"); g_lingo->push(Datum(kErrorFileNotOpen)); } } // Non-standard extensions XOBJSTUBNR(FileIO::m_setOverrideDrive) XOBJSTUB(FileIO::m_getOSDirectory, "") } // End of namespace Director