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