scummvm/engines/sword25/kernel/persistenceservice.cpp
2010-10-13 15:41:00 +00:00

466 lines
16 KiB
C++
Raw Blame History

/* 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 2
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* $URL$
* $Id$
*
*/
/*
* This code is based on Broken Sword 2.5 engine
*
* Copyright (c) Malte Thiesen, Daniel Queteschiner and Michael Elsdoerfer
*
* Licensed under GNU GPL v2
*
*/
// -----------------------------------------------------------------------------
// Includes
// -----------------------------------------------------------------------------
#include "common/fs.h"
#include "common/savefile.h"
#include "sword25/kernel/kernel.h"
#include "sword25/kernel/persistenceservice.h"
#include "sword25/kernel/inputpersistenceblock.h"
#include "sword25/kernel/outputpersistenceblock.h"
#include "sword25/kernel/filesystemutil.h"
#include "sword25/gfx/graphicengine.h"
#include "sword25/sfx/soundengine.h"
#include "sword25/input/inputengine.h"
#include "sword25/math/regionregistry.h"
#include "sword25/script/script.h"
#include <zlib.h>
#define BS_LOG_PREFIX "PERSISTENCESERVICE"
// -----------------------------------------------------------------------------
// Constants and utility functions
// -----------------------------------------------------------------------------
namespace Sword25 {
const char *SAVEGAME_EXTENSION = ".b25s";
const char *SAVEGAME_DIRECTORY = "saves";
const char *FILE_MARKER = "BS25SAVEGAME";
const uint SLOT_COUNT = 18;
const uint FILE_COPY_BUFFER_SIZE = 1024 * 10;
const char *VERSIONID = "SCUMMVM1";
// -------------------------------------------------------------------------
Common::String GenerateSavegameFilename(uint slotID) {
char buffer[10];
sprintf(buffer, "%d%s", slotID, SAVEGAME_EXTENSION);
return Common::String(buffer);
}
// -------------------------------------------------------------------------
Common::String GenerateSavegamePath(uint SlotID) {
Common::FSNode folder(PersistenceService::GetSavegameDirectory());
return folder.getChild(GenerateSavegameFilename(SlotID)).getPath();
}
// -------------------------------------------------------------------------
Common::String FormatTimestamp(TimeDate Time) {
// In the original BS2.5 engine, this used a local object to show the date/time as as a string.
// For now in ScummVM it's being hardcoded to 'dd-MON-yyyy hh:mm:ss'
Common::String monthList[12] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
char buffer[100];
snprintf(buffer, 100, "%.2d-%s-%.4d %.2d:%.2d:%.2d",
Time.tm_mday, monthList[Time.tm_mon].c_str(), 1900 + Time.tm_year,
Time.tm_hour, Time.tm_min, Time.tm_sec
);
return Common::String(buffer);
}
// -------------------------------------------------------------------------
Common::String LoadString(Common::InSaveFile *In, uint MaxSize = 999) {
Common::String Result;
char ch = (char)In->readByte();
while ((ch != '\0') && (ch != ' ')) {
Result += ch;
if (Result.size() >= MaxSize) break;
ch = (char)In->readByte();
}
return Result;
}
}
namespace Sword25 {
// -----------------------------------------------------------------------------
// Private Implementation
// -----------------------------------------------------------------------------
struct SavegameInformation {
bool IsOccupied;
bool IsCompatible;
Common::String Description;
Common::String Filename;
uint GamedataLength;
uint GamedataOffset;
uint GamedataUncompressedLength;
SavegameInformation() {
Clear();
}
void Clear() {
IsOccupied = false;
IsCompatible = false;
Description = "";
Filename = "";
GamedataLength = 0;
GamedataOffset = 0;
GamedataUncompressedLength = 0;
}
};
struct PersistenceService::Impl {
SavegameInformation m_SavegameInformations[SLOT_COUNT];
// -----------------------------------------------------------------------------
Impl() {
ReloadSlots();
}
// -----------------------------------------------------------------------------
void ReloadSlots() {
// <20>ber alle Spielstanddateien iterieren und deren Infos einlesen.
for (uint i = 0; i < SLOT_COUNT; ++i) {
ReadSlotSavegameInformation(i);
}
}
void ReadSlotSavegameInformation(uint SlotID) {
// Aktuelle Slotinformationen in den Ausgangszustand versetzen, er wird im Folgenden neu gef<65>llt.
SavegameInformation &CurSavegameInfo = m_SavegameInformations[SlotID];
CurSavegameInfo.Clear();
// Den Dateinamen f<>r den Spielstand des Slots generieren.
Common::String Filename = GenerateSavegameFilename(SlotID);
// Try to open the savegame for loading
Common::SaveFileManager *sfm = g_system->getSavefileManager();
Common::InSaveFile *File = sfm->openForLoading(Filename);
if (File) {
// Read in the header
Common::String StoredMarker = LoadString(File);
Common::String StoredVersionID = LoadString(File);
Common::String gameDataLength = LoadString(File);
CurSavegameInfo.GamedataLength = atoi(gameDataLength.c_str());
Common::String gamedataUncompressedLength = LoadString(File);
CurSavegameInfo.GamedataUncompressedLength = atoi(gamedataUncompressedLength.c_str());
// If the header can be read in and is detected to be valid, we will have a valid file
if (StoredMarker == FILE_MARKER) {
// Der Slot wird als belegt markiert.
CurSavegameInfo.IsOccupied = true;
// Speichern, ob der Spielstand kompatibel mit der aktuellen Engine-Version ist.
CurSavegameInfo.IsCompatible = (StoredVersionID == Common::String(VERSIONID));
// Dateinamen des Spielstandes speichern.
CurSavegameInfo.Filename = GenerateSavegameFilename(SlotID);
// Die Beschreibung des Spielstandes besteht aus einer textuellen Darstellung des <20>nderungsdatums der Spielstanddatei.
CurSavegameInfo.Description = FormatTimestamp(FileSystemUtil::GetInstance().GetFileTime(Filename));
// Den Offset zu den gespeicherten Spieldaten innerhalb der Datei speichern.
// Dieses entspricht der aktuellen Position, da nach der letzten Headerinformation noch ein Leerzeichen als trenner folgt.
CurSavegameInfo.GamedataOffset = static_cast<uint>(File->pos());
}
delete File;
}
}
};
// -----------------------------------------------------------------------------
// Construction / Destruction
// -----------------------------------------------------------------------------
PersistenceService &PersistenceService::GetInstance() {
static PersistenceService Instance;
return Instance;
}
// -----------------------------------------------------------------------------
PersistenceService::PersistenceService() : m_impl(new Impl) {
}
// -----------------------------------------------------------------------------
PersistenceService::~PersistenceService() {
delete m_impl;
}
// -----------------------------------------------------------------------------
// Implementation
// -----------------------------------------------------------------------------
void PersistenceService::ReloadSlots() {
m_impl->ReloadSlots();
}
// -----------------------------------------------------------------------------
uint PersistenceService::GetSlotCount() {
return SLOT_COUNT;
}
// -----------------------------------------------------------------------------
Common::String PersistenceService::GetSavegameDirectory() {
Common::FSNode node(FileSystemUtil::GetInstance().GetUserdataDirectory());
Common::FSNode childNode = node.getChild(SAVEGAME_DIRECTORY);
// Try and return the path using the savegame subfolder. But if doesn't exist, fall back on the data directory
if (childNode.exists())
return childNode.getPath();
return node.getPath();
}
// -----------------------------------------------------------------------------
namespace {
bool CheckSlotID(uint SlotID) {
// <20>berpr<70>fen, ob die Slot-ID zul<75>ssig ist.
if (SlotID >= SLOT_COUNT) {
BS_LOG_ERRORLN("Tried to access an invalid slot (%d). Only slot ids from 0 to %d are allowed.", SlotID, SLOT_COUNT - 1);
return false;
} else {
return true;
}
}
}
// -----------------------------------------------------------------------------
bool PersistenceService::IsSlotOccupied(uint SlotID) {
if (!CheckSlotID(SlotID)) return false;
return m_impl->m_SavegameInformations[SlotID].IsOccupied;
}
// -----------------------------------------------------------------------------
bool PersistenceService::IsSavegameCompatible(uint SlotID) {
if (!CheckSlotID(SlotID)) return false;
return m_impl->m_SavegameInformations[SlotID].IsCompatible;
}
// -----------------------------------------------------------------------------
Common::String &PersistenceService::GetSavegameDescription(uint SlotID) {
static Common::String EmptyString;
if (!CheckSlotID(SlotID)) return EmptyString;
return m_impl->m_SavegameInformations[SlotID].Description;
}
// -----------------------------------------------------------------------------
Common::String &PersistenceService::GetSavegameFilename(uint SlotID) {
static Common::String EmptyString;
if (!CheckSlotID(SlotID)) return EmptyString;
return m_impl->m_SavegameInformations[SlotID].Filename;
}
// -----------------------------------------------------------------------------
bool PersistenceService::SaveGame(uint SlotID, const Common::String &ScreenshotFilename) {
// <20>berpr<70>fen, ob die Slot-ID zul<75>ssig ist.
if (SlotID >= SLOT_COUNT) {
BS_LOG_ERRORLN("Tried to save to an invalid slot (%d). Only slot ids form 0 to %d are allowed.", SlotID, SLOT_COUNT - 1);
return false;
}
// Dateinamen erzeugen.
Common::String Filename = GenerateSavegameFilename(SlotID);
// Sicherstellen, dass das Verzeichnis f<>r die Spielstanddateien existiert.
FileSystemUtil::GetInstance().CreateDirectory(GetSavegameDirectory());
// Spielstanddatei <20>ffnen und die Headerdaten schreiben.
Common::SaveFileManager *sfm = g_system->getSavefileManager();
Common::OutSaveFile *File = sfm->openForSaving(Filename);
File->writeString(FILE_MARKER);
File->writeByte(' ');
File->writeString(VERSIONID);
File->writeByte(' ');
if (File->err()) {
error("Unable to write header data to savegame file \"%s\".", Filename.c_str());
}
// Alle notwendigen Module persistieren.
OutputPersistenceBlock Writer;
bool Success = true;
Success &= Kernel::GetInstance()->GetScript()->persist(Writer);
Success &= RegionRegistry::instance().persist(Writer);
Success &= Kernel::GetInstance()->GetGfx()->persist(Writer);
Success &= Kernel::GetInstance()->GetSfx()->persist(Writer);
Success &= Kernel::GetInstance()->GetInput()->persist(Writer);
if (!Success) {
error("Unable to persist modules for savegame file \"%s\".", Filename.c_str());
}
// Daten komprimieren.
uLongf CompressedLength = Writer.GetDataSize() + (Writer.GetDataSize() + 500) / 1000 + 12;
Bytef *CompressionBuffer = new Bytef[CompressedLength];
if (compress2(&CompressionBuffer[0], &CompressedLength, reinterpret_cast<const Bytef *>(Writer.GetData()), Writer.GetDataSize(), 6) != Z_OK) {
error("Unable to compress savegame data in savegame file \"%s\".", Filename.c_str());
}
// L<>nge der komprimierten Daten und der unkomprimierten Daten in die Datei schreiben.
char sBuffer[10];
snprintf(sBuffer, 10, "%ld", CompressedLength);
File->writeString(sBuffer);
File->writeByte(' ');
snprintf(sBuffer, 10, "%u", Writer.GetDataSize());
File->writeString(sBuffer);
File->writeByte(' ');
// Komprimierte Daten in die Datei schreiben.
File->write(reinterpret_cast<char *>(&CompressionBuffer[0]), CompressedLength);
if (File->err()) {
error("Unable to write game data to savegame file \"%s\".", Filename.c_str());
}
// Get the screenshot
Common::MemoryReadStream *thumbnail = (static_cast<GraphicEngine *>(
Kernel::GetInstance()->GetService("gfx")))->getThumbnail();
if (thumbnail) {
byte *Buffer = new Byte[FILE_COPY_BUFFER_SIZE];
while (!thumbnail->eos()) {
int bytesRead = thumbnail->read(&Buffer[0], FILE_COPY_BUFFER_SIZE);
File->write(&Buffer[0], bytesRead);
}
delete[] Buffer;
} else {
BS_LOG_WARNINGLN("The screenshot file \"%s\" does not exist. Savegame is written without a screenshot.", Filename.c_str());
}
// Savegameinformationen f<>r diesen Slot aktualisieren.
m_impl->ReadSlotSavegameInformation(SlotID);
File->finalize();
delete File;
delete[] CompressionBuffer;
// Erfolg signalisieren.
return true;
}
// -----------------------------------------------------------------------------
bool PersistenceService::LoadGame(uint SlotID) {
Common::SaveFileManager *sfm = g_system->getSavefileManager();
Common::InSaveFile *File;
// <20>berpr<70>fen, ob die Slot-ID zul<75>ssig ist.
if (SlotID >= SLOT_COUNT) {
BS_LOG_ERRORLN("Tried to load from an invalid slot (%d). Only slot ids form 0 to %d are allowed.", SlotID, SLOT_COUNT - 1);
return false;
}
SavegameInformation &CurSavegameInfo = m_impl->m_SavegameInformations[SlotID];
// <20>berpr<70>fen, ob der Slot belegt ist.
if (!CurSavegameInfo.IsOccupied) {
BS_LOG_ERRORLN("Tried to load from an empty slot (%d).", SlotID);
return false;
}
// <20>berpr<70>fen, ob der Spielstand im angegebenen Slot mit der aktuellen Engine-Version kompatibel ist.
// Im Debug-Modus wird dieser Test <20>bersprungen. F<>r das Testen ist es hinderlich auf die Einhaltung dieser strengen Bedingung zu bestehen,
// da sich die Versions-ID bei jeder Code<64>nderung mit<69>ndert.
#ifndef DEBUG
if (!CurSavegameInfo.IsCompatible) {
BS_LOG_ERRORLN("Tried to load a savegame (%d) that is not compatible with this engine version.", SlotID);
return false;
}
#endif
byte *CompressedDataBuffer = new byte[CurSavegameInfo.GamedataLength];
byte *UncompressedDataBuffer = new Bytef[CurSavegameInfo.GamedataUncompressedLength];
File = sfm->openForLoading(GenerateSavegameFilename(SlotID));
File->seek(CurSavegameInfo.GamedataOffset);
File->read(reinterpret_cast<char *>(&CompressedDataBuffer[0]), CurSavegameInfo.GamedataLength);
if (File->err()) {
BS_LOG_ERRORLN("Unable to load the gamedata from the savegame file \"%s\".", CurSavegameInfo.Filename.c_str());
delete[] CompressedDataBuffer;
delete[] UncompressedDataBuffer;
return false;
}
// Spieldaten dekomprimieren.
uLongf UncompressedBufferSize = CurSavegameInfo.GamedataUncompressedLength;
if (uncompress(reinterpret_cast<Bytef *>(&UncompressedDataBuffer[0]), &UncompressedBufferSize,
reinterpret_cast<Bytef *>(&CompressedDataBuffer[0]), CurSavegameInfo.GamedataLength) != Z_OK) {
BS_LOG_ERRORLN("Unable to decompress the gamedata from savegame file \"%s\".", CurSavegameInfo.Filename.c_str());
delete[] UncompressedDataBuffer;
delete[] CompressedDataBuffer;
delete File;
return false;
}
InputPersistenceBlock Reader(&UncompressedDataBuffer[0], CurSavegameInfo.GamedataUncompressedLength);
// Einzelne Engine-Module depersistieren.
bool Success = true;
Success &= Kernel::GetInstance()->GetScript()->unpersist(Reader);
// Muss unbedingt nach Script passieren. Da sonst die bereits wiederhergestellten Regions per Garbage-Collection gekillt werden.
Success &= RegionRegistry::instance().unpersist(Reader);
Success &= Kernel::GetInstance()->GetGfx()->unpersist(Reader);
Success &= Kernel::GetInstance()->GetSfx()->unpersist(Reader);
Success &= Kernel::GetInstance()->GetInput()->unpersist(Reader);
delete[] CompressedDataBuffer;
delete[] UncompressedDataBuffer;
delete File;
if (!Success) {
BS_LOG_ERRORLN("Unable to unpersist the gamedata from savegame file \"%s\".", CurSavegameInfo.Filename.c_str());
return false;
}
return true;
}
} // End of namespace Sword25