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