mirror of
https://github.com/libretro/scummvm.git
synced 2025-01-07 02:12:14 +00:00
8f4f0d16fc
svn-id: r53430
466 lines
16 KiB
C++
466 lines
16 KiB
C++
/* 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
|