ULTIMA8: Support reading of Pentagram save game files

Fixes bug #14043
This commit is contained in:
Matthew Jimenez 2023-06-13 18:20:01 -05:00 committed by Matthew Jimenez
parent 12c42e2846
commit 4751cedb08
8 changed files with 154 additions and 27 deletions

View file

@ -213,6 +213,21 @@ SaveStateList UltimaMetaEngine::listSaves(const char *target) const {
return saveList; return saveList;
} }
SaveStateDescriptor UltimaMetaEngine::querySaveMetaInfos(const char *target, int slot) const {
SaveStateDescriptor desc = AdvancedMetaEngine::querySaveMetaInfos(target, slot);
if (!desc.isValid() && slot > 0) {
Common::String gameId = getGameId(target);
if (gameId == "ultima8") {
Common::String filename = getSavegameFile(slot, target);
desc = SaveStateDescriptor(this, slot, Common::U32String());
if (!Ultima::Ultima8::MetaEngine::querySaveMetaInfos(filename, desc))
return SaveStateDescriptor();
}
}
return desc;
}
Common::KeymapArray UltimaMetaEngine::initKeymaps(const char *target) const { Common::KeymapArray UltimaMetaEngine::initKeymaps(const char *target) const {
const Common::String gameId = getGameId(target); const Common::String gameId = getGameId(target);
if (gameId == "ultima4" || gameId == "ultima4_enh") if (gameId == "ultima4" || gameId == "ultima4_enh")

View file

@ -45,6 +45,11 @@ public:
*/ */
SaveStateList listSaves(const char *target) const override; SaveStateList listSaves(const char *target) const override;
/**
* Return meta information from the specified save state.
*/
SaveStateDescriptor querySaveMetaInfos(const char *target, int slot) const override;
/** /**
* Initialize keymaps * Initialize keymaps
*/ */

View file

@ -20,27 +20,39 @@
*/ */
#include "ultima/ultima8/filesys/savegame.h" #include "ultima/ultima8/filesys/savegame.h"
#include "common/bufferedstream.h"
#include "common/compression/unzip.h"
namespace Ultima { namespace Ultima {
namespace Ultima8 { namespace Ultima8 {
#define SAVEGAME_IDENT MKTAG('V', 'M', 'U', '8') #define SAVEGAME_IDENT MKTAG('V', 'M', 'U', '8')
#define PKZIP_IDENT MKTAG('P', 'K', 3, 4)
#define SAVEGAME_VERSION 6 #define SAVEGAME_VERSION 6
#define SAVEGAME_MIN_VERSION 2 #define SAVEGAME_MIN_VERSION 2
SavegameReader::SavegameReader(Common::SeekableReadStream *rs, bool metadataOnly) : _file(rs), _version(0) { class FileEntryArchive : public Common::Archive {
if (!MetaEngine::readSavegameHeader(rs, &_header)) struct FileEntry {
return; uint _offset;
uint _size;
FileEntry() : _offset(0), _size(0) {}
};
private:
Common::HashMap<Common::String, FileEntry> _index;
Common::SeekableReadStream *_file;
// Validate the identifier for a valid savegame public:
uint32 ident = _file->readUint32LE(); FileEntryArchive(Common::SeekableReadStream *rs);
if (ident != SAVEGAME_IDENT) ~FileEntryArchive() override;
return;
_version = _file->readUint32LE(); // Common::Archive API implementation
if (metadataOnly) bool hasFile(const Common::Path &path) const override;
return; int listMembers(Common::ArchiveMemberList &list) const override;
const Common::ArchiveMemberPtr getMember(const Common::Path &path) const override;
Common::SeekableReadStream *createReadStreamForMember(const Common::Path &path) const override;
};
FileEntryArchive::FileEntryArchive(Common::SeekableReadStream *rs) : _file(rs) {
// Load the index // Load the index
uint count = _file->readUint16LE(); uint count = _file->readUint16LE();
@ -58,7 +70,93 @@ SavegameReader::SavegameReader(Common::SeekableReadStream *rs, bool metadataOnly
} }
} }
FileEntryArchive::~FileEntryArchive() {
}
bool FileEntryArchive::hasFile(const Common::Path &path) const {
return _index.contains(path.toString());
}
int FileEntryArchive::listMembers(Common::ArchiveMemberList &list) const {
list.clear();
for (Common::HashMap<Common::String, FileEntry>::const_iterator it = _index.begin(); it != _index.end(); ++it)
list.push_back(Common::ArchiveMemberPtr(new Common::GenericArchiveMember(it->_key, this)));
return list.size();
}
const Common::ArchiveMemberPtr FileEntryArchive::getMember(const Common::Path &path) const {
if (!hasFile(path))
return nullptr;
Common::String name = path.toString();
return Common::ArchiveMemberPtr(new Common::GenericArchiveMember(name, this));
}
Common::SeekableReadStream *FileEntryArchive::createReadStreamForMember(const Common::Path &path) const {
assert(hasFile(path));
const FileEntry &fe = _index[path.toString()];
uint8 *data = (uint8 *)malloc(fe._size);
_file->seek(fe._offset);
_file->read(data, fe._size);
return new Common::MemoryReadStream(data, fe._size, DisposeAfterUse::YES);
}
SavegameReader::SavegameReader(Common::SeekableReadStream *rs, bool metadataOnly) : _archive(nullptr), _version(0) {
// Validate the identifier for a valid savegame
uint32 ident = rs->readUint32LE();
if (ident == SAVEGAME_IDENT) {
_version = rs->readUint32LE();
if (!MetaEngine::readSavegameHeader(rs, &_header))
return;
if (metadataOnly)
return;
_archive = new FileEntryArchive(rs);
} else if (SWAP_BYTES_32(ident) == PKZIP_IDENT) {
// Note: Pentagram save description is the zip global comment
_header.description = "Pentagram Save";
// Hack to pull the comment if length < 255
char data[256];
uint16 size = sizeof(data);
rs->seek(-size, SEEK_END);
rs->read(data, size);
for (uint16 i = size; i >= 2; i--) {
uint16 length = size - i;
if (data[i - 2] == length && data[i - 1] == 0) {
if (length > 0)
_header.description = Common::String(data + i, length);
break;
}
}
Common::SeekableReadStream *stream = wrapBufferedSeekableReadStream(rs, 4096, DisposeAfterUse::NO);
_archive = Common::makeZipArchive(stream);
if (!_archive)
return;
Common::ArchiveMemberPtr member = _archive->getMember("VERSION");
if (member) {
_version = member->createReadStream()->readUint32LE();
_header.version = _version;
}
if (metadataOnly) {
delete _archive;
_archive = nullptr;
return;
}
}
}
SavegameReader::~SavegameReader() { SavegameReader::~SavegameReader() {
if (_archive)
delete _archive;
} }
SavegameReader::State SavegameReader::isValid() const { SavegameReader::State SavegameReader::isValid() const {
@ -73,14 +171,9 @@ SavegameReader::State SavegameReader::isValid() const {
} }
Common::SeekableReadStream *SavegameReader::getDataSource(const Std::string &name) { Common::SeekableReadStream *SavegameReader::getDataSource(const Std::string &name) {
assert(_index.contains(name)); assert(_archive);
const FileEntry &fe = _index[name]; return _archive->createReadStreamForMember(name);
uint8 *data = (uint8 *)malloc(fe._size);
_file->seek(fe._offset);
_file->read(data, fe._size);
return new Common::MemoryReadStream(data, fe._size, DisposeAfterUse::YES);
} }

View file

@ -35,15 +35,9 @@ class ZipFile;
class IDataSource; class IDataSource;
class SavegameReader { class SavegameReader {
struct FileEntry {
uint _offset;
uint _size;
FileEntry() : _offset(0), _size(0) {}
};
private: private:
ExtendedSavegameHeader _header; ExtendedSavegameHeader _header;
Common::HashMap<Common::String, FileEntry> _index; Common::Archive *_archive;
Common::SeekableReadStream *_file;
uint32 _version; uint32 _version;
public: public:
explicit SavegameReader(Common::SeekableReadStream *rs, bool metadataOnly = false); explicit SavegameReader(Common::SeekableReadStream *rs, bool metadataOnly = false);

View file

@ -154,10 +154,10 @@ Std::string GameInfo::getPrintableMD5() const {
bool GameInfo::match(GameInfo &other, bool ignoreMD5) const { bool GameInfo::match(GameInfo &other, bool ignoreMD5) const {
if (_type != other._type) return false; if (_type != other._type) return false;
if (_language != other._language) return false; if (_language != other._language) return false;
if (version != other.version) return false;
if (ignoreMD5) return true; if (ignoreMD5) return true;
// NOTE: Version and MD5 hash are not currently set
if (version != other.version) return false;
return (memcmp(_md5, other._md5, 16) == 0); return (memcmp(_md5, other._md5, 16) == 0);
} }

View file

@ -22,6 +22,9 @@
#include "ultima/ultima8/metaengine.h" #include "ultima/ultima8/metaengine.h"
#include "ultima/ultima8/misc/debugger.h" #include "ultima/ultima8/misc/debugger.h"
#include "ultima/ultima8/ultima8.h" #include "ultima/ultima8/ultima8.h"
#include "ultima/ultima8/filesys/savegame.h"
#include "common/savefile.h"
#include "common/system.h"
#include "common/translation.h" #include "common/translation.h"
#include "backends/keymapper/action.h" #include "backends/keymapper/action.h"
#include "backends/keymapper/standard-actions.h" #include "backends/keymapper/standard-actions.h"
@ -237,5 +240,17 @@ Common::String MetaEngine::getMethod(KeybindingAction keyAction, bool isPress) {
return Common::String(); return Common::String();
} }
bool MetaEngine::querySaveMetaInfos(const Common::String &filename, SaveStateDescriptor& desc) {
Common::ScopedPtr<Common::InSaveFile> f(g_system->getSavefileManager()->openForLoading(filename));
if (f) {
SavegameReader sg(f.get(), true);
desc.setDescription(sg.getDescription());
return sg.isValid();
}
return false;
}
} // End of namespace Ultima8 } // End of namespace Ultima8
} // End of namespace Ultima } // End of namespace Ultima

View file

@ -78,6 +78,11 @@ public:
* Execute an engine keymap release action * Execute an engine keymap release action
*/ */
static void releaseAction(KeybindingAction keyAction); static void releaseAction(KeybindingAction keyAction);
/**
* Return meta information from the specified save state for saves that do not have ExtendedSavegameHeader
*/
static bool querySaveMetaInfos(const Common::String &filename, SaveStateDescriptor &desc);
}; };
} // End of namespace Ultima8 } // End of namespace Ultima8

View file

@ -1239,7 +1239,7 @@ Common::Error Ultima8Engine::loadGameStream(Common::SeekableReadStream *stream)
return Common::Error(Common::kReadingFailed, "Invalid or corrupt savegame: missing GameInfo"); return Common::Error(Common::kReadingFailed, "Invalid or corrupt savegame: missing GameInfo");
} }
if (!_gameInfo->match(saveinfo)) { if (!_gameInfo->match(saveinfo, true)) {
Std::string message = "Game mismatch\n"; Std::string message = "Game mismatch\n";
message += "Running _game: " + _gameInfo->getPrintDetails() + "\n"; message += "Running _game: " + _gameInfo->getPrintDetails() + "\n";
message += "Savegame : " + saveinfo.getPrintDetails(); message += "Savegame : " + saveinfo.getPrintDetails();