1 /* ScummVM - Graphic Adventure Engine
2 *
3 * ScummVM is the legal property of its developers, whose names
4 * are too numerous to list here. Please refer to the COPYRIGHT
5 * file distributed with this source distribution.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 *
21 */
22
23 #include "common/savefile.h"
24 #include "common/stream.h"
25 #include "common/memstream.h"
26
27 #include "sci/sci.h"
28 #include "sci/engine/file.h"
29 #include "sci/engine/kernel.h"
30 #include "sci/engine/savegame.h"
31 #include "sci/engine/selector.h"
32 #include "sci/engine/state.h"
33
34 namespace Sci {
35
36 #ifdef ENABLE_SCI32
read(void * dataPtr,uint32 dataSize)37 uint32 MemoryDynamicRWStream::read(void *dataPtr, uint32 dataSize) {
38 // Read at most as many bytes as are still available...
39 if (dataSize > _size - _pos) {
40 dataSize = _size - _pos;
41 _eos = true;
42 }
43 memcpy(dataPtr, _ptr, dataSize);
44
45 _ptr += dataSize;
46 _pos += dataSize;
47
48 return dataSize;
49 }
50
SaveFileRewriteStream(const Common::String & fileName,Common::SeekableReadStream * inFile,kFileOpenMode mode,bool compress)51 SaveFileRewriteStream::SaveFileRewriteStream(const Common::String &fileName,
52 Common::SeekableReadStream *inFile,
53 kFileOpenMode mode,
54 bool compress) :
55 MemoryDynamicRWStream(DisposeAfterUse::YES),
56 _fileName(fileName),
57 _compress(compress) {
58 const bool truncate = (mode == kFileOpenModeCreate);
59 const bool seekToEnd = (mode == kFileOpenModeOpenOrCreate);
60
61 if (!truncate && inFile) {
62 const uint s = inFile->size();
63 ensureCapacity(s);
64 inFile->read(_data, s);
65 if (seekToEnd) {
66 seek(0, SEEK_END);
67 }
68 _changed = false;
69 } else {
70 _changed = true;
71 }
72 }
73
~SaveFileRewriteStream()74 SaveFileRewriteStream::~SaveFileRewriteStream() {
75 commit();
76 }
77
commit()78 void SaveFileRewriteStream::commit() {
79 if (!_changed) {
80 return;
81 }
82
83 Common::ScopedPtr<Common::WriteStream> outFile(g_sci->getSaveFileManager()->openForSaving(_fileName, _compress));
84 outFile->write(_data, _size);
85 _changed = false;
86 }
87 #endif
88
findFreeFileHandle(EngineState * s)89 uint findFreeFileHandle(EngineState *s) {
90 // Find a free file handle
91 uint handle = 1; // Ignore _fileHandles[0]
92 while ((handle < s->_fileHandles.size()) && s->_fileHandles[handle].isOpen())
93 handle++;
94
95 if (handle == s->_fileHandles.size()) {
96 // Hit size limit => Allocate more space
97 s->_fileHandles.resize(s->_fileHandles.size() + 1);
98 }
99
100 return handle;
101 }
102
103 /*
104 * Note on how file I/O is implemented: In ScummVM, one can not create/write
105 * arbitrary data files, simply because many of our target platforms do not
106 * support this. The only files one can create are savestates. But SCI has an
107 * opcode to create and write to seemingly 'arbitrary' files. This is mainly
108 * used in LSL3 for LARRY3.DRV (which is a game data file, not a driver, used
109 * for persisting the results of the "age quiz" across restarts) and in LSL5
110 * for MEMORY.DRV (which is again a game data file and contains the game's
111 * password, XOR encrypted).
112 * To implement that opcode, we combine the SaveFileManager with regular file
113 * code, similarly to how the SCUMM HE engine does it.
114 *
115 * To handle opening a file called "foobar", what we do is this: First, we
116 * create an 'augmented file name', by prepending the game target and a dash,
117 * so if we running game target sq1sci, the name becomes "sq1sci-foobar".
118 * Next, we check if such a file is known to the SaveFileManager. If so, we
119 * we use that for reading/writing, delete it, whatever.
120 *
121 * If no such file is present but we were only asked to *read* the file,
122 * we fallback to looking for a regular file called "foobar", and open that
123 * for reading only.
124 */
125
file_open(EngineState * s,const Common::String & filename,kFileOpenMode mode,bool unwrapFilename)126 reg_t file_open(EngineState *s, const Common::String &filename, kFileOpenMode mode, bool unwrapFilename) {
127 Common::String englishName = g_sci->getSciLanguageString(filename, K_LANG_ENGLISH);
128 englishName.toLowercase();
129
130 Common::String wrappedName = unwrapFilename ? g_sci->wrapFilename(englishName) : englishName;
131 Common::SeekableReadStream *inFile = 0;
132 Common::WriteStream *outFile = 0;
133 Common::SaveFileManager *saveFileMan = g_sci->getSaveFileManager();
134
135 bool isCompressed = true;
136 const SciGameId gameId = g_sci->getGameId();
137
138 // QFG Characters are saved via the CharSave object.
139 // We leave them uncompressed so that they can be imported in later QFG
140 // games, even when using the original interpreter.
141 // We check for room numbers in here, because the file suffix can be changed by the user.
142 // Rooms/Scripts: QFG1(EGA/VGA): 601, QFG2: 840, QFG3/4: 52
143 switch (gameId) {
144 case GID_QFG1:
145 case GID_QFG1VGA:
146 if (s->currentRoomNumber() == 601)
147 isCompressed = false;
148 break;
149 case GID_QFG2:
150 if (s->currentRoomNumber() == 840)
151 isCompressed = false;
152 break;
153 case GID_QFG3:
154 case GID_QFG4:
155 if (s->currentRoomNumber() == 52)
156 isCompressed = false;
157 break;
158 #ifdef ENABLE_SCI32
159 // Hoyle5 has no save games, but creates very simple text-based game options
160 // files that do not need to be compressed
161 case GID_HOYLE5:
162 // Phantasmagoria game scripts create their own save files, so they are
163 // interoperable with the original interpreter just by renaming them as long
164 // as they are not compressed. They are also never larger than a couple
165 // hundred bytes, so compression does not do much here anyway
166 case GID_PHANTASMAGORIA:
167 isCompressed = false;
168 break;
169 #endif
170 default:
171 break;
172 }
173
174 #ifdef ENABLE_SCI32
175 bool isRewritableFile;
176 switch (g_sci->getGameId()) {
177 case GID_PHANTASMAGORIA:
178 isRewritableFile = (filename == "phantsg.dir" || filename == "chase.dat" || filename == "tmp.dat");
179 break;
180 case GID_PQSWAT:
181 isRewritableFile = (filename == "swat.dat");
182 break;
183 default:
184 isRewritableFile = false;
185 }
186
187 if (isRewritableFile) {
188 debugC(kDebugLevelFile, " -> file_open opening %s for rewriting", wrappedName.c_str());
189
190 inFile = saveFileMan->openForLoading(wrappedName);
191 // If no matching savestate exists: fall back to reading from a regular
192 // file
193 if (!inFile)
194 inFile = SearchMan.createReadStreamForMember(englishName);
195
196 if (mode == kFileOpenModeOpenOrFail && !inFile) {
197 debugC(kDebugLevelFile, " -> file_open(kFileOpenModeOpenOrFail): failed to open file '%s'", englishName.c_str());
198 return SIGNAL_REG;
199 }
200
201 SaveFileRewriteStream *stream;
202 stream = new SaveFileRewriteStream(wrappedName, inFile, mode, isCompressed);
203
204 delete inFile;
205
206 inFile = stream;
207 outFile = stream;
208 } else
209 #endif
210 if (mode == kFileOpenModeOpenOrFail) {
211 // Try to open file, abort if not possible
212 inFile = saveFileMan->openForLoading(wrappedName);
213 // If no matching savestate exists: fall back to reading from a regular
214 // file
215 if (!inFile)
216 inFile = SearchMan.createReadStreamForMember(englishName);
217
218 if (!inFile)
219 debugC(kDebugLevelFile, " -> file_open(kFileOpenModeOpenOrFail): failed to open file '%s'", englishName.c_str());
220 } else if (mode == kFileOpenModeCreate) {
221 // Create the file, destroying any content it might have had
222 outFile = saveFileMan->openForSaving(wrappedName, isCompressed);
223 if (!outFile)
224 debugC(kDebugLevelFile, " -> file_open(kFileOpenModeCreate): failed to create file '%s'", englishName.c_str());
225 } else if (mode == kFileOpenModeOpenOrCreate) {
226 // Try to open file, create it if it doesn't exist
227 outFile = saveFileMan->openForSaving(wrappedName, isCompressed);
228 if (!outFile)
229 debugC(kDebugLevelFile, " -> file_open(kFileOpenModeCreate): failed to create file '%s'", englishName.c_str());
230
231 // QfG1 opens the character export file with kFileOpenModeCreate first,
232 // closes it immediately and opens it again with this here. Perhaps
233 // other games use this for read access as well. I guess changing this
234 // whole code into using virtual files and writing them after close
235 // would be more appropriate.
236 } else {
237 error("file_open: unsupported mode %d (filename '%s')", mode, englishName.c_str());
238 }
239
240 if (!inFile && !outFile) { // Failed
241 debugC(kDebugLevelFile, " -> file_open() failed");
242 return SIGNAL_REG;
243 }
244
245 uint handle = findFreeFileHandle(s);
246
247 s->_fileHandles[handle]._in = inFile;
248 s->_fileHandles[handle]._out = outFile;
249 s->_fileHandles[handle]._name = englishName;
250
251 debugC(kDebugLevelFile, " -> opened file '%s' with handle %d", englishName.c_str(), handle);
252 return make_reg(0, handle);
253 }
254
getFileFromHandle(EngineState * s,uint handle)255 FileHandle *getFileFromHandle(EngineState *s, uint handle) {
256 if ((handle == 0) || ((handle >= kVirtualFileHandleStart) && (handle <= kVirtualFileHandleEnd))) {
257 error("Attempt to use invalid file handle (%d)", handle);
258 return 0;
259 }
260
261 if ((handle >= s->_fileHandles.size()) || !s->_fileHandles[handle].isOpen()) {
262 warning("Attempt to use invalid/unused file handle %d", handle);
263 return 0;
264 }
265
266 return &s->_fileHandles[handle];
267 }
268
fgets_wrapper(EngineState * s,char * dest,int maxsize,int handle)269 int fgets_wrapper(EngineState *s, char *dest, int maxsize, int handle) {
270 FileHandle *f = getFileFromHandle(s, handle);
271 if (!f)
272 return 0;
273
274 if (!f->_in) {
275 error("fgets_wrapper: Trying to read from file '%s' opened for writing", f->_name.c_str());
276 return 0;
277 }
278 int readBytes = 0;
279 if (maxsize > 1) {
280 memset(dest, 0, maxsize);
281 f->_in->readLine(dest, maxsize);
282 readBytes = Common::strnlen(dest, maxsize); // FIXME: sierra sci returned byte count and didn't react on NUL characters
283 // The returned string must not have an ending LF
284 if (readBytes > 0) {
285 if (dest[readBytes - 1] == 0x0A)
286 dest[readBytes - 1] = 0;
287 }
288 } else {
289 *dest = 0;
290 }
291
292 debugC(kDebugLevelFile, " -> FGets'ed \"%s\"", dest);
293 return readBytes;
294 }
295
_savegame_sort_byDate(const SavegameDesc & l,const SavegameDesc & r)296 static bool _savegame_sort_byDate(const SavegameDesc &l, const SavegameDesc &r) {
297 if (l.date != r.date)
298 return (l.date > r.date);
299 return (l.time > r.time);
300 }
301
fillSavegameDesc(const Common::String & filename,SavegameDesc & desc)302 bool fillSavegameDesc(const Common::String &filename, SavegameDesc &desc) {
303 Common::SaveFileManager *saveFileMan = g_sci->getSaveFileManager();
304 Common::ScopedPtr<Common::SeekableReadStream> in(saveFileMan->openForLoading(filename));
305 if (!in) {
306 return false;
307 }
308
309 SavegameMetadata meta;
310 if (!get_savegame_metadata(in.get(), meta) || meta.name.empty()) {
311 return false;
312 }
313
314 const int id = strtol(filename.end() - 3, NULL, 10);
315 desc.id = id;
316 // We need to fix date in here, because we save DDMMYYYY instead of
317 // YYYYMMDD, so sorting wouldn't work
318 desc.date = ((meta.saveDate & 0xFFFF) << 16) | ((meta.saveDate & 0xFF0000) >> 8) | ((meta.saveDate & 0xFF000000) >> 24);
319 desc.time = meta.saveTime;
320 desc.version = meta.version;
321 desc.gameVersion = meta.gameVersion;
322 desc.script0Size = meta.script0Size;
323 desc.gameObjectOffset = meta.gameObjectOffset;
324 #ifdef ENABLE_SCI32
325 if (g_sci->getGameId() == GID_SHIVERS) {
326 desc.lowScore = meta.lowScore;
327 desc.highScore = meta.highScore;
328 } else if (g_sci->getGameId() == GID_MOTHERGOOSEHIRES) {
329 desc.avatarId = meta.avatarId;
330 }
331 #endif
332
333 if (meta.name.lastChar() == '\n')
334 meta.name.deleteLastChar();
335
336 // At least Phant2 requires use of strncpy, since it creates save game
337 // names of exactly kMaxSaveNameLength
338 strncpy(desc.name, meta.name.c_str(), kMaxSaveNameLength);
339
340 return true;
341 }
342
343 // Create an array containing all found savedgames, sorted by creation date
listSavegames(Common::Array<SavegameDesc> & saves)344 void listSavegames(Common::Array<SavegameDesc> &saves) {
345 Common::SaveFileManager *saveFileMan = g_sci->getSaveFileManager();
346 Common::StringArray saveNames = saveFileMan->listSavefiles(g_sci->getSavegamePattern());
347
348 for (Common::StringArray::const_iterator iter = saveNames.begin(); iter != saveNames.end(); ++iter) {
349 const Common::String &filename = *iter;
350
351 #ifdef ENABLE_SCI32
352 // exclude new game and autosave slots, except for QFG4,
353 // whose autosave should appear as a normal saved game
354 if (g_sci->getGameId() != GID_QFG4) {
355 const int id = strtol(filename.end() - 3, NULL, 10);
356 if (id == kNewGameId || id == kAutoSaveId) {
357 continue;
358 }
359 }
360 #endif
361
362 SavegameDesc desc;
363 if (fillSavegameDesc(filename, desc)) {
364 saves.push_back(desc);
365 }
366 }
367
368 // Sort the list by creation date of the saves
369 Common::sort(saves.begin(), saves.end(), _savegame_sort_byDate);
370 }
371
372 // Find a savedgame according to virtualId and return the position within our array
findSavegame(Common::Array<SavegameDesc> & saves,int16 savegameId)373 int findSavegame(Common::Array<SavegameDesc> &saves, int16 savegameId) {
374 for (uint saveNr = 0; saveNr < saves.size(); saveNr++) {
375 if (saves[saveNr].id == savegameId)
376 return saveNr;
377 }
378 return -1;
379 }
380
381 #ifdef ENABLE_SCI32
makeCatalogue(const uint maxNumSaves,const uint gameNameSize,const Common::String & fileNamePattern,const bool ramaFormat)382 Common::MemoryReadStream *makeCatalogue(const uint maxNumSaves, const uint gameNameSize, const Common::String &fileNamePattern, const bool ramaFormat) {
383 enum {
384 kGameIdSize = sizeof(int16),
385 kNumSavesSize = sizeof(int16),
386 kFreeSlotSize = sizeof(int16),
387 kTerminatorSize = kGameIdSize,
388 kTerminator = 0xFFFF
389 };
390
391 Common::Array<SavegameDesc> games;
392 listSavegames(games);
393
394 const uint numSaves = MIN(games.size(), maxNumSaves);
395 const uint fileNameSize = fileNamePattern.empty() ? 0 : 12;
396 const uint entrySize = kGameIdSize + fileNameSize + gameNameSize;
397 uint dataSize = numSaves * entrySize + kTerminatorSize;
398 if (ramaFormat) {
399 dataSize += kNumSavesSize + kFreeSlotSize * maxNumSaves;
400 }
401
402 byte *out = (byte *)malloc(dataSize);
403 const byte *const data = out;
404
405 Common::Array<bool> usedSlots;
406 if (ramaFormat) {
407 WRITE_LE_UINT16(out, numSaves);
408 out += kNumSavesSize;
409 usedSlots.resize(maxNumSaves);
410 }
411
412 for (uint i = 0; i < numSaves; ++i) {
413 const SavegameDesc &save = games[i];
414 const uint16 id = save.id - kSaveIdShift;
415 if (!ramaFormat) {
416 WRITE_LE_UINT16(out, id);
417 out += kGameIdSize;
418 }
419 if (fileNameSize) {
420 const Common::String fileName = Common::String::format(fileNamePattern.c_str(), id);
421 strncpy(reinterpret_cast<char *>(out), fileName.c_str(), fileNameSize);
422 out += fileNameSize;
423 }
424 // Game names can be up to exactly gameNameSize
425 strncpy(reinterpret_cast<char *>(out), save.name, gameNameSize);
426 out += gameNameSize;
427 if (ramaFormat) {
428 WRITE_LE_UINT16(out, id);
429 out += kGameIdSize;
430
431 assert(id < maxNumSaves);
432 usedSlots[id] = true;
433 }
434 }
435
436 if (ramaFormat) {
437 // A table indicating which save game slots are occupied
438 for (uint i = 0; i < usedSlots.size(); ++i) {
439 WRITE_LE_UINT16(out, !usedSlots[i]);
440 out += kFreeSlotSize;
441 }
442 }
443
444 WRITE_LE_UINT16(out, kTerminator);
445
446 return new Common::MemoryReadStream(data, dataSize, DisposeAfterUse::YES);
447 }
448 #endif
449
FileHandle()450 FileHandle::FileHandle() : _in(0), _out(0) {
451 }
452
~FileHandle()453 FileHandle::~FileHandle() {
454 close();
455 }
456
close()457 void FileHandle::close() {
458 // NB: It is possible _in and _out are both non-null, but
459 // then they point to the same object.
460 if (_in)
461 delete _in;
462 else
463 delete _out;
464 _in = 0;
465 _out = 0;
466 _name.clear();
467 }
468
isOpen() const469 bool FileHandle::isOpen() const {
470 return _in || _out;
471 }
472
473
addAsVirtualFiles(Common::String title,Common::String fileMask)474 void DirSeeker::addAsVirtualFiles(Common::String title, Common::String fileMask) {
475 Common::SaveFileManager *saveFileMan = g_sci->getSaveFileManager();
476 Common::StringArray foundFiles = saveFileMan->listSavefiles(fileMask);
477 if (!foundFiles.empty()) {
478 // Sort all filenames alphabetically
479 Common::sort(foundFiles.begin(), foundFiles.end());
480
481 Common::StringArray::iterator it;
482 Common::StringArray::iterator it_end = foundFiles.end();
483 bool titleAdded = false;
484
485 for (it = foundFiles.begin(); it != it_end; it++) {
486 Common::String regularFilename = *it;
487 Common::String wrappedFilename = Common::String(regularFilename.c_str() + fileMask.size() - 1);
488
489 Common::SeekableReadStream *testfile = saveFileMan->openForLoading(regularFilename);
490 int32 testfileSize = testfile->size();
491 delete testfile;
492 if (testfileSize > 1024) // check, if larger than 1k. in that case its a saved game.
493 continue; // and we dont want to have those in the list
494
495 if (!titleAdded) {
496 _files.push_back(title);
497 _virtualFiles.push_back("");
498 titleAdded = true;
499 }
500
501 // We need to remove the prefix for display purposes
502 _files.push_back(wrappedFilename);
503 // but remember the actual name as well
504 _virtualFiles.push_back(regularFilename);
505 }
506 }
507 }
508
getVirtualFilename(uint fileNumber)509 Common::String DirSeeker::getVirtualFilename(uint fileNumber) {
510 if (fileNumber >= _virtualFiles.size())
511 error("invalid virtual filename access");
512 return _virtualFiles[fileNumber];
513 }
514
firstFile(const Common::String & mask,reg_t buffer,SegManager * segMan)515 reg_t DirSeeker::firstFile(const Common::String &mask, reg_t buffer, SegManager *segMan) {
516 // Verify that we are given a valid buffer
517 if (!buffer.getSegment()) {
518 error("DirSeeker::firstFile('%s') invoked with invalid buffer", mask.c_str());
519 return NULL_REG;
520 }
521 _outbuffer = buffer;
522 _files.clear();
523 _virtualFiles.clear();
524
525 int QfGImport = g_sci->inQfGImportRoom();
526 if (QfGImport) {
527 _files.clear();
528 addAsVirtualFiles("-QfG1-", "qfg1-*");
529 addAsVirtualFiles("-QfG1VGA-", "qfg1vga-*");
530 if (QfGImport > 2)
531 addAsVirtualFiles("-QfG2-", "qfg2-*");
532 if (QfGImport > 3)
533 addAsVirtualFiles("-QfG3-", "qfg3-*");
534
535 if (QfGImport == 3) {
536 // QfG3 sorts the filelisting itself, we can't let that happen otherwise our
537 // virtual list would go out-of-sync
538 reg_t savedHeros = segMan->findObjectByName("savedHeros");
539 if (!savedHeros.isNull())
540 writeSelectorValue(segMan, savedHeros, SELECTOR(sort), 0);
541 }
542
543 } else {
544 // Prefix the mask
545 const Common::String wrappedMask = g_sci->wrapFilename(mask);
546
547 // Obtain a list of all files matching the given mask
548 Common::SaveFileManager *saveFileMan = g_sci->getSaveFileManager();
549 _files = saveFileMan->listSavefiles(wrappedMask);
550 }
551
552 // Reset the list iterator and write the first match to the output buffer,
553 // if any.
554 _iter = _files.begin();
555 return nextFile(segMan);
556 }
557
nextFile(SegManager * segMan)558 reg_t DirSeeker::nextFile(SegManager *segMan) {
559 if (_iter == _files.end()) {
560 return NULL_REG;
561 }
562
563 Common::String string;
564
565 if (_virtualFiles.empty()) {
566 // Strip the prefix, if we don't got a virtual filelisting
567 const Common::String wrappedString = *_iter;
568 string = g_sci->unwrapFilename(wrappedString);
569 } else {
570 string = *_iter;
571 }
572 if (string.size() > 12)
573 string = Common::String(string.c_str(), 12);
574 segMan->strcpy(_outbuffer, string.c_str());
575
576 // Return the result and advance the list iterator :)
577 ++_iter;
578 return _outbuffer;
579 }
580
581 } // End of namespace Sci
582