From 4751cedb086483f0e873b216fcc89e788e42f89f Mon Sep 17 00:00:00 2001 From: Matthew Jimenez Date: Tue, 13 Jun 2023 18:20:01 -0500 Subject: [PATCH] ULTIMA8: Support reading of Pentagram save game files Fixes bug #14043 --- engines/ultima/metaengine.cpp | 15 +++ engines/ultima/metaengine.h | 5 + engines/ultima/ultima8/filesys/savegame.cpp | 127 +++++++++++++++++--- engines/ultima/ultima8/filesys/savegame.h | 8 +- engines/ultima/ultima8/games/game_info.cpp | 4 +- engines/ultima/ultima8/metaengine.cpp | 15 +++ engines/ultima/ultima8/metaengine.h | 5 + engines/ultima/ultima8/ultima8.cpp | 2 +- 8 files changed, 154 insertions(+), 27 deletions(-) diff --git a/engines/ultima/metaengine.cpp b/engines/ultima/metaengine.cpp index a4bb371a8f3..0cd7d624b5b 100644 --- a/engines/ultima/metaengine.cpp +++ b/engines/ultima/metaengine.cpp @@ -213,6 +213,21 @@ SaveStateList UltimaMetaEngine::listSaves(const char *target) const { 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 { const Common::String gameId = getGameId(target); if (gameId == "ultima4" || gameId == "ultima4_enh") diff --git a/engines/ultima/metaengine.h b/engines/ultima/metaengine.h index 08e6cb8173d..4318def2955 100644 --- a/engines/ultima/metaengine.h +++ b/engines/ultima/metaengine.h @@ -45,6 +45,11 @@ public: */ 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 */ diff --git a/engines/ultima/ultima8/filesys/savegame.cpp b/engines/ultima/ultima8/filesys/savegame.cpp index 66dc429935d..7d4603cbd9d 100644 --- a/engines/ultima/ultima8/filesys/savegame.cpp +++ b/engines/ultima/ultima8/filesys/savegame.cpp @@ -20,27 +20,39 @@ */ #include "ultima/ultima8/filesys/savegame.h" +#include "common/bufferedstream.h" +#include "common/compression/unzip.h" namespace Ultima { namespace Ultima8 { #define SAVEGAME_IDENT MKTAG('V', 'M', 'U', '8') +#define PKZIP_IDENT MKTAG('P', 'K', 3, 4) #define SAVEGAME_VERSION 6 #define SAVEGAME_MIN_VERSION 2 -SavegameReader::SavegameReader(Common::SeekableReadStream *rs, bool metadataOnly) : _file(rs), _version(0) { - if (!MetaEngine::readSavegameHeader(rs, &_header)) - return; +class FileEntryArchive : public Common::Archive { + struct FileEntry { + uint _offset; + uint _size; + FileEntry() : _offset(0), _size(0) {} + }; +private: + Common::HashMap _index; + Common::SeekableReadStream *_file; - // Validate the identifier for a valid savegame - uint32 ident = _file->readUint32LE(); - if (ident != SAVEGAME_IDENT) - return; +public: + FileEntryArchive(Common::SeekableReadStream *rs); + ~FileEntryArchive() override; - _version = _file->readUint32LE(); - if (metadataOnly) - return; + // Common::Archive API implementation + bool hasFile(const Common::Path &path) const override; + 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 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::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() { + if (_archive) + delete _archive; } SavegameReader::State SavegameReader::isValid() const { @@ -73,14 +171,9 @@ SavegameReader::State SavegameReader::isValid() const { } Common::SeekableReadStream *SavegameReader::getDataSource(const Std::string &name) { - assert(_index.contains(name)); + assert(_archive); - const FileEntry &fe = _index[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); + return _archive->createReadStreamForMember(name); } diff --git a/engines/ultima/ultima8/filesys/savegame.h b/engines/ultima/ultima8/filesys/savegame.h index 1305683a53f..64595c6bc76 100644 --- a/engines/ultima/ultima8/filesys/savegame.h +++ b/engines/ultima/ultima8/filesys/savegame.h @@ -35,15 +35,9 @@ class ZipFile; class IDataSource; class SavegameReader { - struct FileEntry { - uint _offset; - uint _size; - FileEntry() : _offset(0), _size(0) {} - }; private: ExtendedSavegameHeader _header; - Common::HashMap _index; - Common::SeekableReadStream *_file; + Common::Archive *_archive; uint32 _version; public: explicit SavegameReader(Common::SeekableReadStream *rs, bool metadataOnly = false); diff --git a/engines/ultima/ultima8/games/game_info.cpp b/engines/ultima/ultima8/games/game_info.cpp index c01f8123cb4..e4f571a96be 100644 --- a/engines/ultima/ultima8/games/game_info.cpp +++ b/engines/ultima/ultima8/games/game_info.cpp @@ -154,10 +154,10 @@ Std::string GameInfo::getPrintableMD5() const { bool GameInfo::match(GameInfo &other, bool ignoreMD5) const { if (_type != other._type) return false; if (_language != other._language) return false; - if (version != other.version) return false; - 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); } diff --git a/engines/ultima/ultima8/metaengine.cpp b/engines/ultima/ultima8/metaengine.cpp index a5f213117d2..88620602e2e 100644 --- a/engines/ultima/ultima8/metaengine.cpp +++ b/engines/ultima/ultima8/metaengine.cpp @@ -22,6 +22,9 @@ #include "ultima/ultima8/metaengine.h" #include "ultima/ultima8/misc/debugger.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 "backends/keymapper/action.h" #include "backends/keymapper/standard-actions.h" @@ -237,5 +240,17 @@ Common::String MetaEngine::getMethod(KeybindingAction keyAction, bool isPress) { return Common::String(); } +bool MetaEngine::querySaveMetaInfos(const Common::String &filename, SaveStateDescriptor& desc) { + Common::ScopedPtr 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 Ultima diff --git a/engines/ultima/ultima8/metaengine.h b/engines/ultima/ultima8/metaengine.h index 4c67c2a77b0..d577cb9d9fb 100644 --- a/engines/ultima/ultima8/metaengine.h +++ b/engines/ultima/ultima8/metaengine.h @@ -78,6 +78,11 @@ public: * Execute an engine keymap release action */ 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 diff --git a/engines/ultima/ultima8/ultima8.cpp b/engines/ultima/ultima8/ultima8.cpp index 0255bdbc997..bc1bf8216db 100644 --- a/engines/ultima/ultima8/ultima8.cpp +++ b/engines/ultima/ultima8/ultima8.cpp @@ -1239,7 +1239,7 @@ Common::Error Ultima8Engine::loadGameStream(Common::SeekableReadStream *stream) 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"; message += "Running _game: " + _gameInfo->getPrintDetails() + "\n"; message += "Savegame : " + saveinfo.getPrintDetails();