diff options
Diffstat (limited to 'libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp')
| -rw-r--r-- | libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp | 607 |
1 files changed, 607 insertions, 0 deletions
diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp b/libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp new file mode 100644 index 0000000..523df0d --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp @@ -0,0 +1,607 @@ +#include "gamebryosavegame.h" + +#include "iplugingame.h" +#include "log.h" +#include "scriptextender.h" + +#include <QDate> +#include <QFile> +#include <QFileInfo> +#include <QScopedArrayPointer> +#include <QTime> + +#ifdef _WIN32 +#include <Windows.h> +#endif +#include <lz4.h> +#include <zlib.h> + +#include <stdexcept> +#include <vector> + +#include "gamegamebryo.h" +#include "imoinfo.h" + +#define CHUNK 16384 + +GamebryoSaveGame::GamebryoSaveGame(QString const& file, GameGamebryo const* game, + bool const lightEnabled, bool const mediumEnabled) + : m_FileName(file), m_CreationTime(QFileInfo(file).lastModified()), m_Game(game), + m_MediumEnabled(mediumEnabled), m_LightEnabled(lightEnabled), + m_DataFields([this]() { + return fetchDataFields(); + }) +{} + +GamebryoSaveGame::~GamebryoSaveGame() {} + +QString GamebryoSaveGame::getFilepath() const +{ + return m_FileName; +} + +QDateTime GamebryoSaveGame::getCreationTime() const +{ + return m_CreationTime; +} + +QString GamebryoSaveGame::getName() const +{ + return QObject::tr("%1, #%2, Level %3, %4") + .arg(m_PCName) + .arg(m_SaveNumber) + .arg(m_PCLevel) + .arg(m_PCLocation); +} + +QString GamebryoSaveGame::getSaveGroupIdentifier() const +{ + return m_PCName; +} + +QStringList GamebryoSaveGame::allFiles() const +{ + // This returns all valid files associated with this game + QStringList res = {m_FileName}; + auto e = m_Game->m_Organizer->gameFeatures()->gameFeature<MOBase::ScriptExtender>(); + if (e != nullptr) { + QFileInfo file(m_FileName); + QFileInfo SEfile(file.absolutePath() + "/" + file.completeBaseName() + "." + + m_Game->savegameSEExtension()); + if (SEfile.exists()) { + res.push_back(SEfile.absoluteFilePath()); + } + } + return res; +} + +bool GamebryoSaveGame::hasScriptExtenderFile() const +{ + QFileInfo file(m_FileName); + QFileInfo SEfile(file.absolutePath() + "/" + file.completeBaseName() + "." + + m_Game->savegameSEExtension()); + return SEfile.exists(); +} + +void GamebryoSaveGame::setCreationTime(_SYSTEMTIME const& ctime) +{ + QDate date; + date.setDate(ctime.wYear, ctime.wMonth, ctime.wDay); + QTime time; + time.setHMS(ctime.wHour, ctime.wMinute, ctime.wSecond, ctime.wMilliseconds); + + m_CreationTime = QDateTime(date, time, Qt::UTC); +} + +GamebryoSaveGame::FileWrapper::FileWrapper(QString const& filepath, + QString const& expected) + : m_File(filepath), m_HasFieldMarkers(false), + m_PluginString(StringType::TYPE_WSTRING), + m_PluginStringFormat(StringFormat::UTF8), m_NextChunk(0) +{ + if (!m_File.open(QIODevice::ReadOnly)) { + throw std::runtime_error( + QObject::tr("failed to open %1").arg(filepath).toUtf8().constData()); + } + + std::vector<char> fileID(expected.length() + 1); + m_File.read(fileID.data(), expected.length()); + fileID[expected.length()] = '\0'; + + QString id(fileID.data()); + if (expected != id) { + throw std::runtime_error( + QObject::tr("wrong file format - expected %1 got \'%2\' for %3") + .arg(expected) + .arg(id) + .arg(filepath) + .toUtf8() + .constData()); + } +} + +void GamebryoSaveGame::FileWrapper::setHasFieldMarkers(bool state) +{ + m_HasFieldMarkers = state; +} + +void GamebryoSaveGame::FileWrapper::setPluginString(StringType type) +{ + m_PluginString = type; +} + +void GamebryoSaveGame::FileWrapper::setPluginStringFormat(StringFormat type) +{ + m_PluginStringFormat = type; +} + +void GamebryoSaveGame::FileWrapper::readQDataStream(QDataStream& data, void* buff, + std::size_t length) +{ + int read = data.readRawData(static_cast<char*>(buff), static_cast<int>(length)); + bool result = true; + if (read != length && m_CompressionType == 1) { + bool result = readNextChunk(); + if (result) { + read += data.readRawData(static_cast<char*>(buff) + read, + static_cast<int>(length - read)); + } + } + if (read != length || !result) { + throw std::runtime_error("unexpected end of file"); + } +} + +template <typename T> +void GamebryoSaveGame::FileWrapper::readQDataStream(QDataStream& data, T& value) +{ + static_assert(std::is_trivial_v<T> && std::is_standard_layout_v<T>); + readQDataStream(data, &value, sizeof(T)); +} + +void GamebryoSaveGame::FileWrapper::skipQDataStream(QDataStream& data, + std::size_t length) +{ + int skip = data.skipRawData(static_cast<int>(length)); + bool result = true; + if (skip != length && m_CompressionType == 1) { + result = readNextChunk(); + if (result) { + skip += data.skipRawData(static_cast<int>(length - skip)); + } + } + if (skip != length || !result) { + throw std::runtime_error("unexpected end of file"); + } +} + +template <> +void GamebryoSaveGame::FileWrapper::read<QString>(QString& value) +{ + if (m_CompressionType == 0) { + unsigned short length; + if (m_PluginString == StringType::TYPE_BSTRING || + m_PluginString == StringType::TYPE_BZSTRING) { + unsigned char len; + read(len); + length = m_PluginString == StringType::TYPE_BZSTRING ? len + 1 : len; + } else { + read(length); + } + + if (m_HasFieldMarkers) { + skip<char>(); + } + + QByteArray buffer; + buffer.resize(length); + + read(buffer.data(), + m_PluginString == StringType::TYPE_BZSTRING ? length - 1 : length); + + if (m_PluginString == StringType::TYPE_BZSTRING) + buffer[length - 1] = '\0'; + + if (m_HasFieldMarkers) { + skip<char>(); + } + + if (m_PluginStringFormat == StringFormat::UTF8) + value = QString::fromUtf8(buffer.constData()); + else + value = QString::fromLocal8Bit(buffer.constData()); + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + unsigned short length; + if (m_PluginString == StringType::TYPE_BSTRING || + m_PluginString == StringType::TYPE_BZSTRING) { + unsigned char len; + readQDataStream(*m_Data, len); + length = m_PluginString == StringType::TYPE_BZSTRING ? len + 1 : len; + } else { + readQDataStream(*m_Data, length); + } + + if (m_HasFieldMarkers) { + skip<char>(); + } + + QByteArray buffer; + buffer.resize(length); + + readQDataStream(*m_Data, buffer.data(), + m_PluginString == StringType::TYPE_BZSTRING ? length - 1 : length); + + if (m_PluginString == StringType::TYPE_BZSTRING) + buffer[length - 1] = '\0'; + + if (m_HasFieldMarkers) { + m_Data->skipRawData(1); + } + + if (m_PluginStringFormat == StringFormat::UTF8) + value = QString::fromUtf8(buffer.constData()); + else + value = QString::fromLocal8Bit(buffer.constData()); + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + } +} + +void GamebryoSaveGame::FileWrapper::read(void* buff, std::size_t length) +{ + int read = m_File.read(static_cast<char*>(buff), length); + if (read != length) { + throw std::runtime_error("unexpected end of file"); + } +} + +QImage GamebryoSaveGame::FileWrapper::readImage(int scale, bool alpha) +{ + uint32_t width; + read(width); + uint32_t height; + read(height); + return readImage(width, height, scale, alpha); +} + +QImage GamebryoSaveGame::FileWrapper::readImage(uint32_t width, uint32_t height, int scale, + bool alpha) +{ + int bpp = alpha ? 4 : 3; + QScopedArrayPointer<unsigned char> buffer(new unsigned char[width * height * bpp]); + read(buffer.data(), width * height * bpp); + QImage image(buffer.data(), width, height, + alpha ? QImage::Format_RGBA8888_Premultiplied : QImage::Format_RGB888); + + // We need to copy the image here because QImage does not make a copy of the + // buffer when constructed. + if (scale != 0) { + return image.copy().scaledToWidth(scale); + } else { + return image.copy(); + } +} + +void GamebryoSaveGame::FileWrapper::setCompressionType(uint16_t compressionType) +{ + m_CompressionType = compressionType; +} + +void GamebryoSaveGame::FileWrapper::closeCompressedData() +{ + if (m_CompressionType == 0) { + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + m_NextChunk = 0; + m_UncompressedSize = 0; + m_Data->device()->close(); + delete m_Data; + } else + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); +} + +bool GamebryoSaveGame::FileWrapper::openCompressedData(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip<char>(bytesToIgnore); + return false; + } else if (m_CompressionType == 1) { + read(m_NextChunk); + read(m_UncompressedSize); + QByteArray placeholder; + m_Data = new QDataStream(placeholder); + bool result = readNextChunk(); + if (result) + skipQDataStream(*m_Data, bytesToIgnore); + return result; + } else if (m_CompressionType == 2) { + uint32_t uncompressedSize; + read(uncompressedSize); + uint32_t compressedSize; + read(compressedSize); + QByteArray compressed; + compressed.resize(compressedSize); + read(compressed.data(), compressedSize); + QByteArray decompressed; + decompressed.resize(uncompressedSize); + LZ4_decompress_safe_partial(compressed.data(), decompressed.data(), compressedSize, + uncompressedSize, uncompressedSize); + compressed.clear(); + + m_Data = new QDataStream(decompressed); + skipQDataStream(*m_Data, bytesToIgnore); + + return true; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return false; + } +} + +bool GamebryoSaveGame::FileWrapper::readNextChunk() +{ + uint32_t have; + uint64_t read = 0; + std::unique_ptr<char[]> inBuffer = std::make_unique<char[]>(CHUNK); + std::unique_ptr<char[]> outBuffer = std::make_unique<char[]>(CHUNK); + QByteArray finalData; + m_Data->device()->close(); + delete m_Data; + z_stream stream{}; + try { + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + stream.avail_in = 0; + stream.next_in = Z_NULL; + if (m_NextChunk >= m_File.size() || finalData.size() == m_UncompressedSize) + return false; + m_File.seek(m_NextChunk); + int zlibRet = inflateInit2(&stream, 15 + 32); + if (zlibRet != Z_OK) { + return false; + } + do { + stream.avail_in = m_File.read(inBuffer.get(), CHUNK); + read += stream.avail_in; + if (!m_File.isReadable()) { + (void)inflateEnd(&stream); + return false; + } + if (stream.avail_in == 0) + break; + stream.next_in = reinterpret_cast<Bytef*>(inBuffer.get()); + do { + stream.avail_out = CHUNK; + stream.next_out = reinterpret_cast<Bytef*>(outBuffer.get()); + zlibRet = inflate(&stream, Z_NO_FLUSH); + if ((zlibRet != Z_OK) && (zlibRet != Z_STREAM_END) && + (zlibRet != Z_BUF_ERROR)) { + return false; + } + have = CHUNK - stream.avail_out; + finalData += QByteArray::fromRawData(outBuffer.get(), have); + } while (stream.avail_out == 0); + read -= stream.avail_in; + } while (zlibRet != Z_STREAM_END); + inflateEnd(&stream); + uint64_t remainder = (m_NextChunk + read) % 16; + uint64_t next = m_NextChunk + read + 16 - (remainder == 0 ? 16 : remainder); + m_NextChunk = next; + } catch (const std::exception&) { + inflateEnd(&stream); + return false; + } + m_Data = new QDataStream(finalData); + return true; +} + +uint8_t GamebryoSaveGame::FileWrapper::readChar(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip<char>(bytesToIgnore); + uint8_t version; + read(version); + return version; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint8_t version; + readQDataStream(*m_Data, version); + return version; + + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +uint16_t GamebryoSaveGame::FileWrapper::readShort(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip<char>(bytesToIgnore); + uint16_t size; + read(size); + return size; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint16_t size; + readQDataStream(*m_Data, size); + return size; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +uint32_t GamebryoSaveGame::FileWrapper::readInt(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip<char>(bytesToIgnore); + uint32_t size; + read(size); + return size; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint32_t size; + readQDataStream(*m_Data, size); + return size; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +uint64_t GamebryoSaveGame::FileWrapper::readLong(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip<char>(bytesToIgnore); + uint64_t size; + read(size); + return size; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint64_t size; + readQDataStream(*m_Data, size); + return size; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +float_t GamebryoSaveGame::FileWrapper::readFloat(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip<char>(bytesToIgnore); + float_t value; + read(value); + return value; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + float_t value; + readQDataStream(*m_Data, value); + return value; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +QStringList GamebryoSaveGame::FileWrapper::readPlugins(int bytesToIgnore, int extraData, + const QStringList& corePlugins) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip<char>(bytesToIgnore); + uint8_t count; + read(count); + return readPluginData(count, extraData, corePlugins); + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + skipQDataStream(*m_Data, bytesToIgnore); + uint8_t count; + readQDataStream(*m_Data, count); + return readPluginData(count, extraData, corePlugins); + } + return {}; +} + +QStringList +GamebryoSaveGame::FileWrapper::readLightPlugins(int bytesToIgnore, int extraData, + const QStringList& corePlugins) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip<char>(bytesToIgnore); + uint16_t count; + read(count); + return readPluginData(count, extraData, corePlugins); + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + skipQDataStream(*m_Data, bytesToIgnore); + uint16_t count; + readQDataStream(*m_Data, count); + return readPluginData(count, extraData, corePlugins); + } + return {}; +} + +QStringList +GamebryoSaveGame::FileWrapper::readMediumPlugins(int bytesToIgnore, int extraData, + const QStringList& corePlugins) +{ + if (m_CompressionType != 1) { + return {}; + } else { + skipQDataStream(*m_Data, bytesToIgnore); + uint32_t count; + readQDataStream(*m_Data, count); + return readPluginData(count, extraData, corePlugins); + } +} + +QStringList GamebryoSaveGame::FileWrapper::readPluginData(uint32_t count, int extraData, + const QStringList corePlugins) +{ + QStringList plugins; + plugins.reserve(count); + if (m_CompressionType == 0) { + for (std::size_t i = 0; i < count; ++i) { + QString name; + read(name); + plugins.push_back(name); + } + } else { + for (std::size_t i = 0; i < count; ++i) { + QString name; + read(name); + plugins.push_back(name); + bool isCustomPlugin; + if (extraData) { + if (extraData > 1) { + readQDataStream(*m_Data, isCustomPlugin); + } else { + isCustomPlugin = !corePlugins.contains(name); + } + if (isCustomPlugin) { + QString creationName; + QString creationId; + uint16_t flagsSize; + uint8_t isCreation; + read(creationName); + read(creationId); + readQDataStream(*m_Data, flagsSize); + skipQDataStream(*m_Data, flagsSize); + readQDataStream(*m_Data, isCreation); + } + } + } + } + return plugins; +} + +void GamebryoSaveGame::FileWrapper::close() +{ + m_File.close(); +} |
