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 /*
24  * This code is based on Broken Sword 2.5 engine
25  *
26  * Copyright (c) Malte Thiesen, Daniel Queteschiner and Michael Elsdoerfer
27  *
28  * Licensed under GNU GPL v2
29  *
30  */
31 
32 #include "common/fs.h"
33 #include "common/savefile.h"
34 #include "common/zlib.h"
35 #include "sword25/kernel/kernel.h"
36 #include "sword25/kernel/persistenceservice.h"
37 #include "sword25/kernel/inputpersistenceblock.h"
38 #include "sword25/kernel/outputpersistenceblock.h"
39 #include "sword25/kernel/filesystemutil.h"
40 #include "sword25/gfx/graphicengine.h"
41 #include "sword25/sfx/soundengine.h"
42 #include "sword25/input/inputengine.h"
43 #include "sword25/math/regionregistry.h"
44 #include "sword25/script/script.h"
45 
46 namespace Sword25 {
47 
48 //static const char *SAVEGAME_EXTENSION = ".b25s";
49 static const char *SAVEGAME_DIRECTORY = "saves";
50 static const char *FILE_MARKER = "BS25SAVEGAME";
51 static const uint  SLOT_COUNT = 18;
52 static const uint  FILE_COPY_BUFFER_SIZE = 1024 * 10;
53 static const char *VERSIONIDOLD = "SCUMMVM1";
54 static const char *VERSIONID = "SCUMMVM2";
55 static const int   VERSIONNUM = 3;
56 
57 #define MAX_SAVEGAME_SIZE 100
58 
59 char gameTarget[MAX_SAVEGAME_SIZE];
60 
setGameTarget(const char * target)61 void setGameTarget(const char *target) {
62 	strncpy(gameTarget, target, MAX_SAVEGAME_SIZE - 1);
63 }
64 
generateSavegameFilename(uint slotID)65 static Common::String generateSavegameFilename(uint slotID) {
66 	char buffer[MAX_SAVEGAME_SIZE];
67 	snprintf(buffer, MAX_SAVEGAME_SIZE, "%s.%.3d", gameTarget, slotID);
68 	return Common::String(buffer);
69 }
70 
formatTimestamp(TimeDate time)71 static Common::String formatTimestamp(TimeDate time) {
72 	// In the original BS2.5 engine, this used a local object to show the date/time as as a string.
73 	// For now in ScummVM it's being hardcoded to 'dd-MON-yyyy hh:mm:ss'
74 	Common::String monthList[12] = {
75 		"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
76 	};
77 	char buffer[100];
78 	snprintf(buffer, 100, "%.2d-%s-%.4d %.2d:%.2d:%.2d",
79 	         time.tm_mday, monthList[time.tm_mon].c_str(), 1900 + time.tm_year,
80 	         time.tm_hour, time.tm_min, time.tm_sec
81 	        );
82 
83 	return Common::String(buffer);
84 }
85 
loadString(Common::InSaveFile * in,uint maxSize=999)86 static Common::String loadString(Common::InSaveFile *in, uint maxSize = 999) {
87 	Common::String result;
88 
89 	char ch = (char)in->readByte();
90 	while (ch != '\0') {
91 		result += ch;
92 		if (result.size() >= maxSize)
93 			break;
94 		ch = (char)in->readByte();
95 	}
96 
97 	return result;
98 }
99 
100 struct SavegameInformation {
101 	bool isOccupied;
102 	bool isCompatible;
103 	Common::String description;
104 	int  version;
105 	uint gamedataLength;
106 	uint gamedataOffset;
107 	uint gamedataUncompressedLength;
108 
SavegameInformationSword25::SavegameInformation109 	SavegameInformation() {
110 		clear();
111 	}
112 
clearSword25::SavegameInformation113 	void clear() {
114 		isOccupied = false;
115 		isCompatible = false;
116 		description = "";
117 		gamedataLength = 0;
118 		gamedataOffset = 0;
119 		gamedataUncompressedLength = 0;
120 	}
121 };
122 
123 struct PersistenceService::Impl {
124 	SavegameInformation _savegameInformations[SLOT_COUNT];
125 
ImplSword25::PersistenceService::Impl126 	Impl() {
127 		reloadSlots();
128 	}
129 
reloadSlotsSword25::PersistenceService::Impl130 	void reloadSlots() {
131 		// Iterate through all the saved games, and read their thumbnails.
132 		for (uint i = 0; i < SLOT_COUNT; ++i) {
133 			readSlotSavegameInformation(i);
134 		}
135 	}
136 
readSlotSavegameInformationSword25::PersistenceService::Impl137 	void readSlotSavegameInformation(uint slotID) {
138 		// Get the information corresponding to the requested save slot.
139 		SavegameInformation &curSavegameInfo = _savegameInformations[slotID];
140 		curSavegameInfo.clear();
141 
142 		// Generate the save slot file name.
143 		Common::String filename = generateSavegameFilename(slotID);
144 
145 		// Try to open the savegame for loading
146 		Common::SaveFileManager *sfm = g_system->getSavefileManager();
147 		Common::InSaveFile *file = sfm->openForLoading(filename);
148 
149 		if (file) {
150 			// Read in the header
151 			Common::String storedMarker = loadString(file);
152 			Common::String storedVersionID = loadString(file);
153 			if (storedVersionID == VERSIONIDOLD) {
154 				curSavegameInfo.version = 1;
155 			} else {
156 				Common::String versionNum = loadString(file);
157 				curSavegameInfo.version = atoi(versionNum.c_str());
158 			}
159 			Common::String gameDescription = loadString(file);
160 			Common::String gamedataLength = loadString(file);
161 			curSavegameInfo.gamedataLength = atoi(gamedataLength.c_str());
162 			Common::String gamedataUncompressedLength = loadString(file);
163 			curSavegameInfo.gamedataUncompressedLength = atoi(gamedataUncompressedLength.c_str());
164 
165 			// If the header can be read in and is detected to be valid, we will have a valid file
166 			if (storedMarker == FILE_MARKER) {
167 				// The slot is marked as occupied.
168 				curSavegameInfo.isOccupied = true;
169 				// Check if the saved game is compatible with the current engine version.
170 				curSavegameInfo.isCompatible = (curSavegameInfo.version <= VERSIONNUM);
171 				// Load the save game description.
172 				curSavegameInfo.description = gameDescription;
173 				// The offset to the stored save game data within the file.
174 				// This reflects the current position, as the header information
175 				// is still followed by a space as separator.
176 				curSavegameInfo.gamedataOffset = static_cast<uint>(file->pos());
177 			}
178 
179 			delete file;
180 		}
181 	}
182 };
183 
getInstance()184 PersistenceService &PersistenceService::getInstance() {
185 	static PersistenceService instance;
186 	return instance;
187 }
188 
PersistenceService()189 PersistenceService::PersistenceService() : _impl(new Impl) {
190 }
191 
~PersistenceService()192 PersistenceService::~PersistenceService() {
193 	delete _impl;
194 }
195 
reloadSlots()196 void PersistenceService::reloadSlots() {
197 	_impl->reloadSlots();
198 }
199 
getSlotCount()200 uint PersistenceService::getSlotCount() {
201 	return SLOT_COUNT;
202 }
203 
getSavegameDirectory()204 Common::String PersistenceService::getSavegameDirectory() {
205 	Common::FSNode node(FileSystemUtil::getUserdataDirectory());
206 	Common::FSNode childNode = node.getChild(SAVEGAME_DIRECTORY);
207 
208 	// Try and return the path using the savegame subfolder. But if doesn't exist, fall back on the data directory
209 	if (childNode.exists())
210 		return childNode.getPath();
211 
212 	return node.getPath();
213 }
214 
215 namespace {
checkslotID(uint slotID)216 bool checkslotID(uint slotID) {
217 	// �berpr�fen, ob die Slot-ID zul�ssig ist.
218 	if (slotID >= SLOT_COUNT) {
219 		error("Tried to access an invalid slot (%d). Only slot ids from 0 to %d are allowed.", slotID, SLOT_COUNT - 1);
220 		return false;
221 	} else {
222 		return true;
223 	}
224 }
225 }
226 
isSlotOccupied(uint slotID)227 bool PersistenceService::isSlotOccupied(uint slotID) {
228 	if (!checkslotID(slotID))
229 		return false;
230 	return _impl->_savegameInformations[slotID].isOccupied;
231 }
232 
isSavegameCompatible(uint slotID)233 bool PersistenceService::isSavegameCompatible(uint slotID) {
234 	if (!checkslotID(slotID))
235 		return false;
236 	return _impl->_savegameInformations[slotID].isCompatible;
237 }
238 
getSavegameDescription(uint slotID)239 Common::String &PersistenceService::getSavegameDescription(uint slotID) {
240 	static Common::String emptyString;
241 	if (!checkslotID(slotID))
242 		return emptyString;
243 	return _impl->_savegameInformations[slotID].description;
244 }
245 
getSavegameFilename(uint slotID)246 Common::String &PersistenceService::getSavegameFilename(uint slotID) {
247 	static Common::String result;
248 	if (!checkslotID(slotID))
249 		return result;
250 	result = generateSavegameFilename(slotID);
251 	return result;
252 }
253 
getSavegameVersion(uint slotID)254 int PersistenceService::getSavegameVersion(uint slotID) {
255 	if (!checkslotID(slotID))
256 		return -1;
257 	return _impl->_savegameInformations[slotID].version;
258 }
259 
saveGame(uint slotID,const Common::String & screenshotFilename)260 bool PersistenceService::saveGame(uint slotID, const Common::String &screenshotFilename) {
261 	// FIXME: This code is a hack which bypasses the savefile API,
262 	// and should eventually be removed.
263 
264 	// �berpr�fen, ob die Slot-ID zul�ssig ist.
265 	if (slotID >= SLOT_COUNT) {
266 		error("Tried to save to an invalid slot (%d). Only slot ids form 0 to %d are allowed.", slotID, SLOT_COUNT - 1);
267 		return false;
268 	}
269 
270 	// Dateinamen erzeugen.
271 	Common::String filename = generateSavegameFilename(slotID);
272 
273 	// Spielstanddatei �ffnen und die Headerdaten schreiben.
274 	Common::SaveFileManager *sfm = g_system->getSavefileManager();
275 	Common::OutSaveFile *file = sfm->openForSaving(filename);
276 
277 	file->writeString(FILE_MARKER);
278 	file->writeByte(0);
279 	file->writeString(VERSIONID);
280 	file->writeByte(0);
281 
282 	char buf[20];
283 	snprintf(buf, 20, "%d", VERSIONNUM);
284 	file->writeString(buf);
285 	file->writeByte(0);
286 
287 	TimeDate dt;
288 	g_system->getTimeAndDate(dt);
289 	file->writeString(formatTimestamp(dt));
290 	file->writeByte(0);
291 
292 	if (file->err()) {
293 		error("Unable to write header data to savegame file \"%s\".", filename.c_str());
294 	}
295 
296 	// Alle notwendigen Module persistieren.
297 	OutputPersistenceBlock writer;
298 	bool success = true;
299 	success &= Kernel::getInstance()->getScript()->persist(writer);
300 	success &= RegionRegistry::instance().persist(writer);
301 	success &= Kernel::getInstance()->getGfx()->persist(writer);
302 	success &= Kernel::getInstance()->getSfx()->persist(writer);
303 	success &= Kernel::getInstance()->getInput()->persist(writer);
304 	if (!success) {
305 		error("Unable to persist modules for savegame file \"%s\".", filename.c_str());
306 	}
307 
308 	// Write the save game data uncompressed, since the final saved game will be
309 	// compressed anyway.
310 	char sBuffer[10];
311 	snprintf(sBuffer, 10, "%u", writer.getDataSize());
312 	file->writeString(sBuffer);
313 	file->writeByte(0);
314 	snprintf(sBuffer, 10, "%u", writer.getDataSize());
315 	file->writeString(sBuffer);
316 	file->writeByte(0);
317 	file->write(writer.getData(), writer.getDataSize());
318 
319 	// Get the screenshot
320 	Common::SeekableReadStream *thumbnail = Kernel::getInstance()->getGfx()->getThumbnail();
321 
322 	if (thumbnail) {
323 		byte *buffer = new byte[FILE_COPY_BUFFER_SIZE];
324 		thumbnail->seek(0, SEEK_SET);
325 		while (!thumbnail->eos()) {
326 			int bytesRead = thumbnail->read(&buffer[0], FILE_COPY_BUFFER_SIZE);
327 			file->write(&buffer[0], bytesRead);
328 		}
329 
330 		delete[] buffer;
331 	} else {
332 		warning("The screenshot file \"%s\" does not exist. Savegame is written without a screenshot.", filename.c_str());
333 	}
334 
335 	file->finalize();
336 	delete file;
337 
338 	// Savegameinformationen f�r diesen Slot aktualisieren.
339 	_impl->readSlotSavegameInformation(slotID);
340 
341 	// Empty the cache, to remove old thumbnails
342 	Kernel::getInstance()->getResourceManager()->emptyThumbnailCache();
343 
344 	// Erfolg signalisieren.
345 	return true;
346 }
347 
loadGame(uint slotID)348 bool PersistenceService::loadGame(uint slotID) {
349 	Common::SaveFileManager *sfm = g_system->getSavefileManager();
350 	Common::InSaveFile *file;
351 
352 	// �berpr�fen, ob die Slot-ID zul�ssig ist.
353 	if (slotID >= SLOT_COUNT) {
354 		error("Tried to load from an invalid slot (%d). Only slot ids form 0 to %d are allowed.", slotID, SLOT_COUNT - 1);
355 		return false;
356 	}
357 
358 	SavegameInformation &curSavegameInfo = _impl->_savegameInformations[slotID];
359 
360 	// �berpr�fen, ob der Slot belegt ist.
361 	if (!curSavegameInfo.isOccupied) {
362 		error("Tried to load from an empty slot (%d).", slotID);
363 		return false;
364 	}
365 
366 	// �berpr�fen, ob der Spielstand im angegebenen Slot mit der aktuellen Engine-Version kompatibel ist.
367 	// Im Debug-Modus wird dieser Test �bersprungen. F�r das Testen ist es hinderlich auf die Einhaltung dieser strengen Bedingung zu bestehen,
368 	// da sich die Versions-ID bei jeder Code�nderung mit�ndert.
369 #ifndef DEBUG
370 	if (!curSavegameInfo.isCompatible) {
371 		error("Tried to load a savegame (%d) that is not compatible with this engine version.", slotID);
372 		return false;
373 	}
374 #endif
375 
376 	byte *compressedDataBuffer = new byte[curSavegameInfo.gamedataLength];
377 	byte *uncompressedDataBuffer = new byte[curSavegameInfo.gamedataUncompressedLength];
378 	Common::String filename = generateSavegameFilename(slotID);
379 	file = sfm->openForLoading(filename);
380 
381 	file->seek(curSavegameInfo.gamedataOffset);
382 	file->read(reinterpret_cast<char *>(&compressedDataBuffer[0]), curSavegameInfo.gamedataLength);
383 	if (file->err()) {
384 		error("Unable to load the gamedata from the savegame file \"%s\".", filename.c_str());
385 		delete[] compressedDataBuffer;
386 		delete[] uncompressedDataBuffer;
387 		return false;
388 	}
389 
390 	// Uncompress game data, if needed.
391 	unsigned long uncompressedBufferSize = curSavegameInfo.gamedataUncompressedLength;
392 
393 	if (uncompressedBufferSize > curSavegameInfo.gamedataLength) {
394 		// Older saved game, where the game data was compressed again.
395 		if (!Common::uncompress(reinterpret_cast<byte *>(&uncompressedDataBuffer[0]), &uncompressedBufferSize,
396 					   reinterpret_cast<byte *>(&compressedDataBuffer[0]), curSavegameInfo.gamedataLength)) {
397 			error("Unable to decompress the gamedata from savegame file \"%s\".", filename.c_str());
398 			delete[] uncompressedDataBuffer;
399 			delete[] compressedDataBuffer;
400 			delete file;
401 			return false;
402 		}
403 	} else {
404 		// Newer saved game with uncompressed game data, copy it as-is.
405 		memcpy(uncompressedDataBuffer, compressedDataBuffer, uncompressedBufferSize);
406 	}
407 
408 	InputPersistenceBlock reader(&uncompressedDataBuffer[0], curSavegameInfo.gamedataUncompressedLength, curSavegameInfo.version);
409 
410 	// Einzelne Engine-Module depersistieren.
411 	bool success = true;
412 	success &= Kernel::getInstance()->getScript()->unpersist(reader);
413 	// Muss unbedingt nach Script passieren. Da sonst die bereits wiederhergestellten Regions per Garbage-Collection gekillt werden.
414 	success &= RegionRegistry::instance().unpersist(reader);
415 	success &= Kernel::getInstance()->getGfx()->unpersist(reader);
416 	success &= Kernel::getInstance()->getSfx()->unpersist(reader);
417 	success &= Kernel::getInstance()->getInput()->unpersist(reader);
418 
419 	delete[] compressedDataBuffer;
420 	delete[] uncompressedDataBuffer;
421 	delete file;
422 
423 	if (!success) {
424 		error("Unable to unpersist the gamedata from savegame file \"%s\".", filename.c_str());
425 		return false;
426 	}
427 
428 	return true;
429 }
430 
431 } // End of namespace Sword25
432