diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /libs/game_gamebryo/src | |
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem,
Proton/umu-run integration, and Flatpak packaging.
Key features:
- FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak)
- Proton/GE-Proton/umu-run launcher with env var forwarding
- Flatpak support (sandbox-aware VFS, NXM handler, umu-run)
- Wine prefix management UI
- Case-insensitive path resolution for Linux filesystems
- QSettings-safe INI handling (avoids Bethesda INI corruption)
- Portable instance support with auto-generated launcher scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/game_gamebryo/src')
34 files changed, 4666 insertions, 0 deletions
diff --git a/libs/game_gamebryo/src/creation/CMakeLists.txt b/libs/game_gamebryo/src/creation/CMakeLists.txt new file mode 100644 index 0000000..f2dce97 --- /dev/null +++ b/libs/game_gamebryo/src/creation/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_creation STATIC) +mo2_configure_library(game_creation + WARNINGS OFF + TRANSLATIONS ON + PUBLIC_DEPENDS uibase + PRIVATE_DEPENDS lz4) +target_link_libraries(game_creation PUBLIC game_gamebryo) +mo2_install_target(game_creation) diff --git a/libs/game_gamebryo/src/creation/creationgameplugins.cpp b/libs/game_gamebryo/src/creation/creationgameplugins.cpp new file mode 100644 index 0000000..f5cef62 --- /dev/null +++ b/libs/game_gamebryo/src/creation/creationgameplugins.cpp @@ -0,0 +1,183 @@ +#include "creationgameplugins.h" +#include <ipluginlist.h> +#include <report.h> +#include <safewritefile.h> +#include <scopeguard.h> + +#include <QDir> +#include <QSet> +#include <QStringEncoder> +#include <QStringList> + +using MOBase::IOrganizer; +using MOBase::IPluginGame; +using MOBase::IPluginList; +using MOBase::reportError; +using MOBase::SafeWriteFile; + +CreationGamePlugins::CreationGamePlugins(IOrganizer* organizer) + : GamebryoGamePlugins(organizer) +{} + +QStringList CreationGamePlugins::getLoadOrder() +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (loadOrderIsNew || !pluginsIsNew) { + return readLoadOrderList(m_Organizer->pluginList(), loadOrderPath); + } else { + return readPluginList(m_Organizer->pluginList()); + } +} + +void CreationGamePlugins::writePluginList(const IPluginList* pluginList, + const QString& filePath) +{ + SafeWriteFile file(filePath); + + QStringEncoder encoder(QStringConverter::Encoding::System); + + file->resize(0); + + file->write( + encoder.encode("# This file was automatically generated by Mod Organizer.\r\n")); + + bool invalidFileNames = false; + int writtenCount = 0; + + QStringList plugins = pluginList->pluginNames(); + std::sort(plugins.begin(), plugins.end(), + [pluginList](const QString& lhs, const QString& rhs) { + return pluginList->priority(lhs) < pluginList->priority(rhs); + }); + + QStringList PrimaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList DLCPlugins = organizer()->managedGame()->DLCPlugins(); + QSet<QString> ManagedMods = + QSet<QString>(PrimaryPlugins.begin(), PrimaryPlugins.end()); + QSet<QString> DLCSet = QSet<QString>(DLCPlugins.begin(), DLCPlugins.end()); + ManagedMods.subtract(DLCSet); + PrimaryPlugins.append(QList<QString>(ManagedMods.begin(), ManagedMods.end())); + + // TODO: do not write plugins in OFFICIAL_FILES container + for (const QString& pluginName : plugins) { + if (!PrimaryPlugins.contains(pluginName, Qt::CaseInsensitive)) { + if (pluginList->state(pluginName) == IPluginList::STATE_ACTIVE) { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write("*"); + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } else { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } + } + } + + if (invalidFileNames) { + reportError(QObject::tr("Some of your plugins have invalid names! These " + "plugins can not be loaded by the game. Please see " + "mo_interface.log for a list of affected plugins " + "and rename them.")); + } + + file->commit(); +} + +QStringList CreationGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + const auto plugins = pluginList->pluginNames(); + const auto primaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList loadOrder(primaryPlugins); + + for (const QString& pluginName : loadOrder) { + if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + } + } + + QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt"; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + qWarning("%s not found", qUtf8Printable(filePath)); + return loadOrder; + } + ON_BLOCK_EXIT([&]() { + file.close(); + }); + + if (file.size() == 0) { + // MO stores at least a header in the file. if it's completely empty the + // file is broken + qWarning("%s empty", qUtf8Printable(filePath)); + return loadOrder; + } + + QStringList pluginsFound; + while (!file.atEnd()) { + QByteArray line = file.readLine(); + QString pluginName; + if ((line.size() > 0) && (line.at(0) != '#')) { + pluginName = QStringEncoder(QStringConverter::Encoding::System) + .encode(line.trimmed().constData()); + } + if (!primaryPlugins.contains(pluginName, Qt::CaseInsensitive)) { + if (pluginName.startsWith('*')) { + pluginName.remove(0, 1); + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + pluginsFound.append(pluginName); + if (!loadOrder.contains(pluginName, Qt::CaseInsensitive)) { + loadOrder.append(pluginName); + } + } + } else { + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + pluginsFound.append(pluginName); + if (!loadOrder.contains(pluginName, Qt::CaseInsensitive)) { + loadOrder.append(pluginName); + } + } + } + } else { + pluginName.remove(0, 1); + pluginsFound.append(pluginName); + } + } + + file.close(); + + // set all plugins not found inactive + for (const auto& pluginName : plugins) { + if (!pluginsFound.contains(pluginName, Qt::CaseInsensitive)) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + + return loadOrder; +} + +bool CreationGamePlugins::lightPluginsAreSupported() +{ + return true; +} diff --git a/libs/game_gamebryo/src/creation/creationgameplugins.h b/libs/game_gamebryo/src/creation/creationgameplugins.h new file mode 100644 index 0000000..f7afd80 --- /dev/null +++ b/libs/game_gamebryo/src/creation/creationgameplugins.h @@ -0,0 +1,22 @@ +#ifndef CREATIONGAMEPLUGINS_H +#define CREATIONGAMEPLUGINS_H + +#include <gamebryogameplugins.h> +#include <imoinfo.h> +#include <iplugingame.h> +#include <map> + +class CreationGamePlugins : public GamebryoGamePlugins +{ +public: + CreationGamePlugins(MOBase::IOrganizer* organizer); + +protected: + virtual void writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) override; + virtual QStringList readPluginList(MOBase::IPluginList* pluginList) override; + virtual QStringList getLoadOrder() override; + virtual bool lightPluginsAreSupported() override; +}; + +#endif // CREATIONGAMEPLUGINS_H diff --git a/libs/game_gamebryo/src/creation/game_creation_en.ts b/libs/game_gamebryo/src/creation/game_creation_en.ts new file mode 100644 index 0000000..f217448 --- /dev/null +++ b/libs/game_gamebryo/src/creation/game_creation_en.ts @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="en_US"> +<context> + <name>QObject</name> + <message> + <location filename="creationgameplugins.cpp" line="97"/> + <source>Some of your plugins have invalid names! These plugins can not be loaded by the game. Please see mo_interface.log for a list of affected plugins and rename them.</source> + <translation type="unfinished"></translation> + </message> +</context> +</TS> diff --git a/libs/game_gamebryo/src/gamebryo/CMakeLists.txt b/libs/game_gamebryo/src/gamebryo/CMakeLists.txt new file mode 100644 index 0000000..cff88fc --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_gamebryo STATIC) +mo2_configure_library(game_gamebryo + WARNINGS OFF + TRANSLATIONS ON + AUTOMOC ON + PUBLIC_DEPENDS uibase + PRIVATE_DEPENDS zlib lz4) +mo2_install_target(game_gamebryo) diff --git a/libs/game_gamebryo/src/gamebryo/dummybsa.cpp b/libs/game_gamebryo/src/gamebryo/dummybsa.cpp new file mode 100644 index 0000000..ba6f766 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/dummybsa.cpp @@ -0,0 +1,192 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer 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 3 of the License, or +(at your option) any later version. + +Mod Organizer 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 Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "dummybsa.h" +#include <QFile> +#define WIN32_LEAN_AND_MEAN +#include <Windows.h> + +static void writeUlong(unsigned char* buffer, int offset, unsigned long value) +{ + union + { + unsigned long ulValue; + unsigned char cValue[4]; + }; + ulValue = value; + memcpy(buffer + offset, cValue, 4); +} + +static void writeUlonglong(unsigned char* buffer, int offset, unsigned long long value) +{ + union + { + unsigned long long ullValue; + unsigned char cValue[8]; + }; + ullValue = value; + memcpy(buffer + offset, cValue, 8); +} + +static unsigned long genHashInt(const unsigned char* pos, const unsigned char* end) +{ + unsigned long hash = 0; + for (; pos < end; ++pos) { + hash *= 0x1003f; + hash += *pos; + } + return hash; +} + +static unsigned long long genHash(const char* fileName) +{ + char fileNameLower[MAX_PATH + 1]; + int i = 0; + for (; i < MAX_PATH && fileName[i] != '\0'; ++i) { + fileNameLower[i] = static_cast<char>(tolower(fileName[i])); + if (fileNameLower[i] == '\\') { + fileNameLower[i] = '/'; + } + } + fileNameLower[i] = '\0'; + + unsigned char* fileNameLowerU = reinterpret_cast<unsigned char*>(fileNameLower); + + char* ext = strrchr(fileNameLower, '.'); + if (ext == nullptr) { + ext = fileNameLower + strlen(fileNameLower); + } + unsigned char* extU = reinterpret_cast<unsigned char*>(ext); + + int length = ext - fileNameLower; + + unsigned long long hash = 0ULL; + + if (length > 0) { + hash = *(extU - 1) | ((length > 2 ? *(ext - 2) : 0) << 8) | (length << 16) | + (fileNameLowerU[0] << 24); + } + + if (strlen(ext) > 0) { + if (strcmp(ext + 1, "kf") == 0) { + hash |= 0x80; + } else if (strcmp(ext + 1, "nif") == 0) { + hash |= 0x8000; + } else if (strcmp(ext + 1, "dds") == 0) { + hash |= 0x8080; + } else if (strcmp(ext + 1, "wav") == 0) { + hash |= 0x80000000; + } + + unsigned long long temp = + static_cast<unsigned long long>(genHashInt(fileNameLowerU + 1, extU - 2)); + temp += static_cast<unsigned long long>(genHashInt(extU, extU + strlen(ext))); + + hash |= (temp & 0xFFFFFFFF) << 32; + } + return hash; +} + +DummyBSA::DummyBSA(unsigned long bsaVersion) + : m_Version(bsaVersion), m_FolderName(""), m_FileName("dummy.dds"), + m_TotalFileNameLength(0) +{} + +void DummyBSA::writeHeader(QFile& file) +{ + unsigned char header[] = { + 'B', 'S', 'A', '\0', // magic string + 0xDE, 0xAD, 0xBE, 0xEF, // version - insert later + 0x24, 0x00, 0x00, 0x00, // offset to folder recors. header size is static + 0xDE, 0xAD, 0xBE, 0xEF, // archive flags - insert later + 0x01, 0x00, 0x00, 0x00, // folder count + 0x01, 0x00, 0x00, 0x00, // file count + 0xDE, 0xAD, 0xBE, 0xEF, // total folder names length - insert later + 0xDE, 0xAD, 0xBE, 0xEF, // total file names length - insert later + 0xDE, 0xAD, 0xBE, 0xEF // file flags - insert later + }; + + writeUlong(header, 4, m_Version); + writeUlong(header, 12, 0x01 | 0x02); // has directories and has files. + writeUlong(header, 24, + static_cast<unsigned long>(m_FolderName.length()) + + 1); // empty folder name + writeUlong(header, 28, m_TotalFileNameLength); // single character file name + + writeUlong(header, 32, 2); // has dds + + file.write(reinterpret_cast<char*>(header), sizeof(header)); +} + +void DummyBSA::writeFolderRecord(QFile& file, const std::string& folderName) +{ + unsigned char folderRecord[] = { + 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, // folder hash + 0x01, 0x00, 0x00, 0x00, // file count + 0xDE, 0xAD, 0xBE, 0xEF, // offset to folder name + }; + // we'd usually have to sort folders be the hash value generated here + writeUlonglong(folderRecord, 0, genHash(folderName.c_str())); + writeUlong(folderRecord, 12, + 0x34 + m_TotalFileNameLength); // TODO: this should be calculated properly + + file.write(reinterpret_cast<char*>(folderRecord), sizeof(folderRecord)); +} + +void DummyBSA::writeFileRecord(QFile& file, const std::string& fileName) +{ + unsigned char fileRecord[] = { + 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, // file name hash + 0xDE, 0xAD, 0xBE, 0xEF, // size + 0xDE, 0xAD, 0xBE, 0xEF, // offset to file data + }; + + // we'd usually have to sort files by the value generated here + writeUlonglong(fileRecord, 0, genHash(fileName.c_str())); + writeUlong(fileRecord, 8, 0); + writeUlong( + fileRecord, 12, + 0x44 + static_cast<unsigned long>(fileName.length() + 1) + + 4); // after this record we expect the filename and 4 bytes of file size + + file.write(reinterpret_cast<char*>(fileRecord), sizeof(fileRecord)); +} + +void DummyBSA::writeFileRecordBlocks(QFile& file, const std::string& folderName) +{ + file.write(folderName.c_str(), folderName.length() + 1); + + writeFileRecord(file, m_FileName); +} + +void DummyBSA::write(const QString& fileName) +{ + QFile file(fileName); + file.open(QIODevice::WriteOnly); + + m_TotalFileNameLength = static_cast<unsigned long>(m_FileName.length() + 1); + + writeHeader(file); + writeFolderRecord(file, m_FolderName); + writeFileRecordBlocks(file, m_FolderName); + file.write(m_FileName.c_str(), m_FileName.length() + 1); + char fileSize[] = {0x00, 0x00, 0x00, 0x00}; + file.write(fileSize, sizeof(fileSize)); + file.close(); +} diff --git a/libs/game_gamebryo/src/gamebryo/dummybsa.h b/libs/game_gamebryo/src/gamebryo/dummybsa.h new file mode 100644 index 0000000..8ea071b --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/dummybsa.h @@ -0,0 +1,59 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer 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 3 of the License, or +(at your option) any later version. + +Mod Organizer 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 Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef DUMMYBSA_H +#define DUMMYBSA_H + +#include <QFile> +#include <QString> + +/** + * @brief Class for creating a dummy bsa used for archive invalidation + **/ +class DummyBSA +{ + +public: + /** + * @brief constructor + * + **/ + DummyBSA(unsigned long bsaVersion); + + /** + * @brief write to the specified file + * + * @param fileName name of the file to write to + **/ + void write(const QString& fileName); + +private: + void writeHeader(QFile& file); + void writeFolderRecord(QFile& file, const std::string& folderName); + void writeFileRecord(QFile& file, const std::string& fileName); + void writeFileRecordBlocks(QFile& file, const std::string& folderName); + +private: + unsigned long m_Version; + std::string m_FolderName; + std::string m_FileName; + unsigned long m_TotalFileNameLength; +}; + +#endif // DUMMYBSA_H diff --git a/libs/game_gamebryo/src/gamebryo/game_gamebryo_en.ts b/libs/game_gamebryo/src/gamebryo/game_gamebryo_en.ts new file mode 100644 index 0000000..f924131 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/game_gamebryo_en.ts @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="en_US"> +<context> + <name>GamebryoModDataContent</name> + <message> + <location filename="gamebryomoddatacontent.cpp" line="15"/> + <source>Plugins (ESP/ESM/ESL)</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryomoddatacontent.cpp" line="16"/> + <source>Optional Plugins</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryomoddatacontent.cpp" line="17"/> + <source>Interface</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryomoddatacontent.cpp" line="18"/> + <source>Meshes</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryomoddatacontent.cpp" line="19"/> + <source>Bethesda Archive</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryomoddatacontent.cpp" line="20"/> + <source>Scripts (Papyrus)</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryomoddatacontent.cpp" line="21"/> + <source>Script Extender Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryomoddatacontent.cpp" line="22"/> + <source>Script Extender Files</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryomoddatacontent.cpp" line="23"/> + <source>SkyProc Patcher</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryomoddatacontent.cpp" line="24"/> + <source>Sound or Music</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryomoddatacontent.cpp" line="25"/> + <source>Textures</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryomoddatacontent.cpp" line="26"/> + <source>MCM Configuration</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryomoddatacontent.cpp" line="27"/> + <source>INI Files</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryomoddatacontent.cpp" line="28"/> + <source>FaceGen Data</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryomoddatacontent.cpp" line="29"/> + <source>ModGroup Files</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>GamebryoSaveGameInfoWidget</name> + <message> + <location filename="gamebryosavegameinfowidget.ui" line="39"/> + <source>Save #</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryosavegameinfowidget.ui" line="51"/> + <source>Character</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryosavegameinfowidget.ui" line="63"/> + <source>Level</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryosavegameinfowidget.ui" line="75"/> + <source>Location</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryosavegameinfowidget.ui" line="87"/> + <source>Date</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryosavegameinfowidget.cpp" line="78"/> + <source>Has Script Extender Data</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryosavegameinfowidget.cpp" line="83"/> + <source>Missing ESPs</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryosavegameinfowidget.cpp" line="116"/> + <location filename="gamebryosavegameinfowidget.cpp" line="154"/> + <location filename="gamebryosavegameinfowidget.cpp" line="193"/> + <source>None</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryosavegameinfowidget.cpp" line="122"/> + <source>Missing ESHs</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryosavegameinfowidget.cpp" line="161"/> + <source>Missing ESLs</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>QObject</name> + <message> + <location filename="gamebryogameplugins.cpp" line="130"/> + <source>Some of your plugins have invalid names! These plugins can not be loaded by the game. Please see mo_interface.log for a list of affected plugins and rename them.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryosavegame.cpp" line="48"/> + <source>%1, #%2, Level %3, %4</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryosavegame.cpp" line="102"/> + <source>failed to open %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamebryosavegame.cpp" line="112"/> + <source>wrong file format - expected %1 got '%2' for %3</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamegamebryo.cpp" line="318"/> + <source>failed to query registry path (preflight): %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamegamebryo.cpp" line="326"/> + <source>failed to query registry path (read): %1</source> + <translation type="unfinished"></translation> + </message> +</context> +</TS> diff --git a/libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.cpp b/libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.cpp new file mode 100644 index 0000000..1b7144b --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.cpp @@ -0,0 +1,133 @@ +#include "gamebryobsainvalidation.h" + +#include "dummybsa.h" +#include "iplugingame.h" +#include "iprofile.h" +#include "registry.h" +#include <imoinfo.h> +#include <utility.h> + +#include <QDir> +#include <QStringList> + +#include <Windows.h> + +GamebryoBSAInvalidation::GamebryoBSAInvalidation(MOBase::DataArchives* dataArchives, + const QString& iniFilename, + MOBase::IPluginGame const* game) + : m_DataArchives(dataArchives), m_IniFileName(iniFilename), m_Game(game) +{} + +bool GamebryoBSAInvalidation::isInvalidationBSA(const QString& bsaName) +{ + static QStringList invalidation{invalidationBSAName()}; + + for (const QString& file : invalidation) { + if (file.compare(bsaName, Qt::CaseInsensitive) == 0) { + return true; + } + } + return false; +} + +void GamebryoBSAInvalidation::deactivate(MOBase::IProfile* profile) +{ + prepareProfile(profile); +} + +void GamebryoBSAInvalidation::activate(MOBase::IProfile* profile) +{ + prepareProfile(profile); +} + +bool GamebryoBSAInvalidation::prepareProfile(MOBase::IProfile* profile) +{ + bool dirty = false; + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : m_Game->documentsDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + WCHAR setting[MAX_PATH]; + + // write bInvalidateOlderFiles = 1, if needed + if (!::GetPrivateProfileStringW(L"Archive", L"bInvalidateOlderFiles", L"0", setting, + MAX_PATH, iniFilePath.toStdWString().c_str()) || + wcstol(setting, nullptr, 10) != 1) { + dirty = true; + if (!MOBase::WriteRegistryValue(L"Archive", L"bInvalidateOlderFiles", L"1", + iniFilePath.toStdWString().c_str())) { + qWarning("failed to activate BSA invalidation in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + + if (profile->invalidationActive(nullptr)) { + + // add the dummy bsa to the archive string, if needed + QStringList archives = m_DataArchives->archives(profile); + bool bsaInstalled = false; + for (const QString& archive : archives) { + if (isInvalidationBSA(archive)) { + bsaInstalled = true; + break; + } + } + if (!bsaInstalled) { + m_DataArchives->addArchive(profile, 0, invalidationBSAName()); + dirty = true; + } + + // create the dummy bsa if necessary + QString bsaFile = m_Game->dataDirectory().absoluteFilePath(invalidationBSAName()); + if (!QFile::exists(bsaFile)) { + DummyBSA bsa(bsaVersion()); + bsa.write(bsaFile); + dirty = true; + } + + // write SInvalidationFile = "", if needed + if (::GetPrivateProfileStringW(L"Archive", L"SInvalidationFile", + L"ArchiveInvalidation.txt", setting, MAX_PATH, + iniFilePath.toStdWString().c_str()) || + wcscmp(setting, L"") != 0) { + dirty = true; + if (!MOBase::WriteRegistryValue(L"Archive", L"SInvalidationFile", L"", + iniFilePath.toStdWString().c_str())) { + qWarning("failed to activate BSA invalidation in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } else { + + // remove the dummy bsa from the archive string, if needed + QStringList archivesBefore = m_DataArchives->archives(profile); + for (const QString& archive : archivesBefore) { + if (isInvalidationBSA(archive)) { + m_DataArchives->removeArchive(profile, archive); + dirty = true; + } + } + + // delete the dummy bsa, if needed + QString bsaFile = m_Game->dataDirectory().absoluteFilePath(invalidationBSAName()); + if (QFile::exists(bsaFile)) { + MOBase::shellDeleteQuiet(bsaFile); + dirty = true; + } + + // write SInvalidationFile = "ArchiveInvalidation.txt", if needed + if (!::GetPrivateProfileStringW(L"Archive", L"SInvalidationFile", L"", setting, + MAX_PATH, iniFilePath.toStdWString().c_str()) || + wcscmp(setting, L"ArchiveInvalidation.txt") != 0) { + dirty = true; + if (!MOBase::WriteRegistryValue(L"Archive", L"SInvalidationFile", + L"ArchiveInvalidation.txt", + iniFilePath.toStdWString().c_str())) { + qWarning("failed to activate BSA invalidation in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } + + return dirty; +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.h b/libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.h new file mode 100644 index 0000000..efd3d7b --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.h @@ -0,0 +1,36 @@ +#ifndef GAMEBRYOBSAINVALIDATION_H +#define GAMEBRYOBSAINVALIDATION_H + +#include <QString> +#include <bsainvalidation.h> +#include <dataarchives.h> +#include <memory> + +namespace MOBase +{ +class IPluginGame; +} + +class GamebryoBSAInvalidation : public MOBase::BSAInvalidation +{ +public: + GamebryoBSAInvalidation(MOBase::DataArchives* dataArchives, + const QString& iniFilename, MOBase::IPluginGame const* game); + + virtual bool isInvalidationBSA(const QString& bsaName) override; + virtual void deactivate(MOBase::IProfile* profile) override; + virtual void activate(MOBase::IProfile* profile) override; + virtual bool prepareProfile(MOBase::IProfile* profile) override; + +private: + virtual QString invalidationBSAName() const = 0; + virtual unsigned long + bsaVersion() const = 0; // 0x67 for oblivion, 0x68 for everything else + +private: + MOBase::DataArchives* m_DataArchives; + QString m_IniFileName; + MOBase::IPluginGame const* m_Game; +}; + +#endif // GAMEBRYOBSAINVALIDATION_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryodataarchives.cpp b/libs/game_gamebryo/src/gamebryo/gamebryodataarchives.cpp new file mode 100644 index 0000000..3dcba8a --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryodataarchives.cpp @@ -0,0 +1,80 @@ +#include "gamebryodataarchives.h" + +#include <Windows.h> + +#include <registry.h> +#include <utility.h> + +#include "gamegamebryo.h" + +GamebryoDataArchives::GamebryoDataArchives(const GameGamebryo* game) : m_Game{game} {} + +QDir GamebryoDataArchives::gameDirectory() const +{ + return QDir(m_Game->gameDirectory()).absolutePath(); +} + +QDir GamebryoDataArchives::localGameDirectory() const +{ + return QDir(m_Game->myGamesPath()).absolutePath(); +} + +QStringList GamebryoDataArchives::getArchivesFromKey(const QString& iniFile, + const QString& key, + const int size) const +{ + wchar_t* buffer = new wchar_t[size]; + QStringList result; + std::wstring iniFileW = QDir::toNativeSeparators(iniFile).toStdWString(); + + // epic ms fail: GetPrivateProfileString uses errno (for whatever reason) to signal a + // fail since the return value has a different meaning (number of bytes copied). + // HOWEVER, it will not set errno to 0 if NO error occured + errno = 0; + + if (::GetPrivateProfileStringW(L"Archive", key.toStdWString().c_str(), L"", buffer, + size, iniFileW.c_str()) != 0) { + result.append(QString::fromStdWString(buffer).split(',')); + } + + for (int i = 0; i < result.count(); ++i) { + result[i] = result[i].trimmed(); + } + delete[] buffer; + return result; +} + +void GamebryoDataArchives::setArchivesToKey(const QString& iniFile, const QString& key, + const QString& value) +{ + if (!MOBase::WriteRegistryValue(L"Archive", key.toStdWString().c_str(), + value.toStdWString().c_str(), + iniFile.toStdWString().c_str())) { + qWarning("failed to set archives in \"%s\"", qUtf8Printable(iniFile)); + } +} + +void GamebryoDataArchives::addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) +{ + QStringList current = archives(profile); + if (current.contains(archiveName, Qt::CaseInsensitive)) { + return; + } + + current.insert(index != INT_MAX ? index : current.size(), archiveName); + + writeArchiveList(profile, current); +} + +void GamebryoDataArchives::removeArchive(MOBase::IProfile* profile, + const QString& archiveName) +{ + QStringList current = archives(profile); + if (!current.contains(archiveName, Qt::CaseInsensitive)) { + return; + } + current.removeAll(archiveName); + + writeArchiveList(profile, current); +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryodataarchives.h b/libs/game_gamebryo/src/gamebryo/gamebryodataarchives.h new file mode 100644 index 0000000..13f4130 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryodataarchives.h @@ -0,0 +1,37 @@ +#ifndef GAMEBRYODATAARCHIVES_H +#define GAMEBRYODATAARCHIVES_H + +#include <QDir> + +#include "dataarchives.h" + +class GameGamebryo; + +class GamebryoDataArchives : public MOBase::DataArchives +{ + +public: + GamebryoDataArchives(const GameGamebryo* game); + + virtual void addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) override; + virtual void removeArchive(MOBase::IProfile* profile, + const QString& archiveName) override; + +protected: + QDir gameDirectory() const; + QDir localGameDirectory() const; + + QStringList getArchivesFromKey(const QString& iniFile, const QString& key, + int size = 256) const; + void setArchivesToKey(const QString& iniFile, const QString& key, + const QString& value); + +private: + const GameGamebryo* m_Game; + + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) = 0; +}; + +#endif // GAMEBRYODATAARCHIVES_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryogameplugins.cpp b/libs/game_gamebryo/src/gamebryo/gamebryogameplugins.cpp new file mode 100644 index 0000000..83ad50f --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryogameplugins.cpp @@ -0,0 +1,248 @@ +#include "gamebryogameplugins.h" +#include <imodinterface.h> +#include <iplugingame.h> +#include <ipluginlist.h> +#include <report.h> +#include <safewritefile.h> +#include <scopeguard.h> +#include <utility.h> + +#include <QDateTime> +#include <QDir> +#include <QString> +#include <QStringEncoder> +#include <QStringList> + +using MOBase::IOrganizer; +using MOBase::IPluginList; +using MOBase::reportError; +using MOBase::SafeWriteFile; + +GamebryoGamePlugins::GamebryoGamePlugins(IOrganizer* organizer) : m_Organizer(organizer) +{} + +void GamebryoGamePlugins::writePluginLists(const IPluginList* pluginList) +{ + if (!m_LastRead.isValid()) { + // attempt to write uninitialized plugin lists + return; + } + + writePluginList(pluginList, m_Organizer->profile()->absolutePath() + "/plugins.txt"); + writeLoadOrderList(pluginList, + m_Organizer->profile()->absolutePath() + "/loadorder.txt"); + + m_LastRead = QDateTime::currentDateTime(); +} + +void GamebryoGamePlugins::readPluginLists(MOBase::IPluginList* pluginList) +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (loadOrderIsNew || !pluginsIsNew) { + // read both files if they are both new or both older than the last read + QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath); + pluginList->setLoadOrder(loadOrder); + readPluginList(pluginList); + } else { + // If the plugins is new but not loadorder, we must reparse the load order from the + // plugin files + QStringList loadOrder = readPluginList(pluginList); + pluginList->setLoadOrder(loadOrder); + } + + m_LastRead = QDateTime::currentDateTime(); +} + +QStringList GamebryoGamePlugins::getLoadOrder() +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (loadOrderIsNew || !pluginsIsNew) { + return readLoadOrderList(m_Organizer->pluginList(), loadOrderPath); + } else { + return readPluginList(m_Organizer->pluginList()); + } +} + +void GamebryoGamePlugins::writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) +{ + return writeList(pluginList, filePath, false); +} + +void GamebryoGamePlugins::writeLoadOrderList(const MOBase::IPluginList* pluginList, + const QString& filePath) +{ + return writeList(pluginList, filePath, true); +} + +void GamebryoGamePlugins::writeList(const IPluginList* pluginList, + const QString& filePath, bool loadOrder) +{ + SafeWriteFile file(filePath); + + QStringEncoder encoder = loadOrder + ? QStringEncoder(QStringConverter::Encoding::Utf8) + : QStringEncoder(QStringConverter::Encoding::System); + + file->resize(0); + + file->write( + encoder.encode("# This file was automatically generated by Mod Organizer.\r\n")); + + bool invalidFileNames = false; + int writtenCount = 0; + + QStringList plugins = pluginList->pluginNames(); + std::sort(plugins.begin(), plugins.end(), + [pluginList](const QString& lhs, const QString& rhs) { + return pluginList->priority(lhs) < pluginList->priority(rhs); + }); + + for (const QString& pluginName : plugins) { + if (loadOrder || (pluginList->state(pluginName) == IPluginList::STATE_ACTIVE)) { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } + } + + if (invalidFileNames) { + reportError(QObject::tr("Some of your plugins have invalid names! These " + "plugins can not be loaded by the game. Please see " + "mo_interface.log for a list of affected plugins " + "and rename them.")); + } + + if (writtenCount == 0) { + qWarning("plugin list would be empty, this is almost certainly wrong. Not " + "saving."); + } else { + file->commit(); + } +} + +QStringList GamebryoGamePlugins::readLoadOrderList(MOBase::IPluginList* pluginList, + const QString& filePath) +{ + QStringList pluginNames = organizer()->managedGame()->primaryPlugins(); + + std::set<QString> pluginLookup; + for (auto&& name : pluginNames) { + pluginLookup.insert(name.toLower()); + } + + const auto b = MOBase::forEachLineInFile(filePath, [&](QString s) { + if (!pluginLookup.contains(s.toLower())) { + pluginLookup.insert(s); + pluginNames.push_back(std::move(s)); + } + }); + + if (!b) { + return readPluginList(pluginList); + } + + return pluginNames; +} + +QStringList GamebryoGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + QStringList primary = organizer()->managedGame()->primaryPlugins(); + for (const QString& pluginName : primary) { + if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + } + } + QStringList plugins = pluginList->pluginNames(); + QStringList pluginsClone(plugins); + // Do not sort the primary plugins. Their load order should be locked as defined in + // "primaryPlugins". + for (const auto& plugin : pluginsClone) { + if (primary.contains(plugin, Qt::CaseInsensitive)) + plugins.removeAll(plugin); + } + + // Always use filetime loadorder to get the actual load order + std::sort(plugins.begin(), plugins.end(), + [&](const QString& lhs, const QString& rhs) { + MOBase::IModInterface* lhm = + organizer()->modList()->getMod(pluginList->origin(lhs)); + MOBase::IModInterface* rhm = + organizer()->modList()->getMod(pluginList->origin(rhs)); + QDir lhd = organizer()->managedGame()->dataDirectory(); + QDir rhd = organizer()->managedGame()->dataDirectory(); + if (lhm != nullptr) + lhd = lhm->absolutePath(); + if (rhm != nullptr) + rhd = rhm->absolutePath(); + QString lhp = lhd.absoluteFilePath(lhs); + QString rhp = rhd.absoluteFilePath(rhs); + return QFileInfo(lhp).lastModified() < QFileInfo(rhp).lastModified(); + }); + + // Determine plugin active state by the plugins.txt file. + bool pluginsTxtExists = true; + QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt"; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + pluginsTxtExists = false; + } + ON_BLOCK_EXIT([&]() { + file.close(); + }); + + if (file.size() == 0) { + // MO stores at least a header in the file. if it's completely empty the + // file is broken + pluginsTxtExists = false; + } + + QStringList activePlugins; + QStringList inactivePlugins; + if (pluginsTxtExists) { + while (!file.atEnd()) { + QByteArray line = file.readLine(); + QString pluginName; + if ((line.size() > 0) && (line.at(0) != '#')) { + QStringEncoder encoder(QStringConverter::Encoding::System); + pluginName = encoder.encode(line.trimmed().constData()); + } + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + activePlugins.push_back(pluginName); + } + } + + for (const auto& pluginName : plugins) { + if (!activePlugins.contains(pluginName, Qt::CaseInsensitive)) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + } else { + for (const QString& pluginName : plugins) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + + return primary + plugins; +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryogameplugins.h b/libs/game_gamebryo/src/gamebryo/gamebryogameplugins.h new file mode 100644 index 0000000..ca70c75 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryogameplugins.h @@ -0,0 +1,38 @@ +#ifndef GAMEBRYOGAMEPLUGINS_H +#define GAMEBRYOGAMEPLUGINS_H + +#include <QDateTime> +#include <QStringList> +#include <gameplugins.h> +#include <imoinfo.h> + +class GamebryoGamePlugins : public MOBase::GamePlugins +{ +public: + GamebryoGamePlugins(MOBase::IOrganizer* organizer); + + virtual void writePluginLists(const MOBase::IPluginList* pluginList) override; + virtual void readPluginLists(MOBase::IPluginList* pluginList) override; + virtual QStringList getLoadOrder() override; + +protected: + MOBase::IOrganizer* organizer() const { return m_Organizer; } + + virtual void writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath); + virtual void writeLoadOrderList(const MOBase::IPluginList* pluginList, + const QString& filePath); + virtual QStringList readLoadOrderList(MOBase::IPluginList* pluginList, + const QString& filePath); + virtual QStringList readPluginList(MOBase::IPluginList* pluginList); + +protected: + MOBase::IOrganizer* m_Organizer; + QDateTime m_LastRead; + +private: + void writeList(const MOBase::IPluginList* pluginList, const QString& filePath, + bool loadOrder); +}; + +#endif // GAMEBRYOGAMEPLUGINS_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.cpp b/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.cpp new file mode 100644 index 0000000..cf78eba --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.cpp @@ -0,0 +1,140 @@ +/* +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "gamebryolocalsavegames.h" +#include "registry.h" +#include <QtDebug> +#include <iprofile.h> +#include <stddef.h> +#include <string> +#include <windows.h> + +#include "gamegamebryo.h" + +GamebryoLocalSavegames::GamebryoLocalSavegames(const GameGamebryo* game, + const QString& iniFileName) + : m_Game{game}, m_IniFileName(iniFileName) +{} + +MappingType GamebryoLocalSavegames::mappings(const QDir& profileSaveDir) const +{ + return {{profileSaveDir.absolutePath(), localSavesDirectory().absolutePath(), true, + true}}; +} + +QString GamebryoLocalSavegames::localSavesDummy() const +{ + return "__MO_Saves\\"; +} + +QDir GamebryoLocalSavegames::localSavesDirectory() const +{ + return QDir(m_Game->myGamesPath()).absoluteFilePath(localSavesDummy()); +} + +QDir GamebryoLocalSavegames::localGameDirectory() const +{ + return QDir(m_Game->myGamesPath()).absolutePath(); +} + +bool GamebryoLocalSavegames::prepareProfile(MOBase::IProfile* profile) +{ + bool enable = profile->localSavesEnabled(); + + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : localGameDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + QString saveIni = profile->absolutePath() + "/" + "savepath.ini"; + + // Get the current sLocalSavePath + WCHAR currentPath[MAX_PATH]; + GetPrivateProfileStringW(L"General", L"sLocalSavePath", L"SKIP_ME", currentPath, + MAX_PATH, iniFilePath.toStdWString().c_str()); + bool alreadyEnabled = + wcscmp(currentPath, localSavesDummy().toStdWString().c_str()) == 0; + + // Get the current bUseMyGamesDirectory + WCHAR currentMyGames[MAX_PATH]; + GetPrivateProfileStringW(L"General", L"bUseMyGamesDirectory", L"SKIP_ME", + currentMyGames, MAX_PATH, + iniFilePath.toStdWString().c_str()); + + // Create the __MO_Saves directory if local saves are enabled and it doesn't exist + if (enable) { + QDir saves = localSavesDirectory(); + if (!saves.exists()) { + saves.mkdir("."); + } + } + + // Set the path to __MO_Saves if it's not already + if (enable && !alreadyEnabled) { + // If the path is not blank, save it to savepath.ini + if (wcscmp(currentPath, L"SKIP_ME") != 0) { + MOBase::WriteRegistryValue(L"General", L"sLocalSavePath", currentPath, + saveIni.toStdWString().c_str()); + } + if (wcscmp(currentMyGames, L"SKIP_ME") != 0) { + MOBase::WriteRegistryValue(L"General", L"bUseMyGamesDirectory", currentMyGames, + saveIni.toStdWString().c_str()); + } + MOBase::WriteRegistryValue(L"General", L"sLocalSavePath", + localSavesDummy().toStdWString().c_str(), + iniFilePath.toStdWString().c_str()); + MOBase::WriteRegistryValue(L"General", L"bUseMyGamesDirectory", L"1", + iniFilePath.toStdWString().c_str()); + } + + // Get rid of the local saves setting if it's still there + if (!enable && alreadyEnabled) { + // If savepath.ini exists, use it and delete it + if (QFile::exists(saveIni)) { + WCHAR savedPath[MAX_PATH]; + WCHAR savedMyGames[MAX_PATH]; + GetPrivateProfileStringW(L"General", L"sLocalSavePath", L"DELETE_ME", savedPath, + MAX_PATH, saveIni.toStdWString().c_str()); + GetPrivateProfileStringW(L"General", L"bUseMyGamesDirectory", L"DELETE_ME", + savedMyGames, MAX_PATH, saveIni.toStdWString().c_str()); + if (wcscmp(savedPath, L"DELETE_ME") != 0) { + MOBase::WriteRegistryValue(L"General", L"sLocalSavePath", savedPath, + iniFilePath.toStdWString().c_str()); + } else { + MOBase::WriteRegistryValue(L"General", L"sLocalSavePath", NULL, + iniFilePath.toStdWString().c_str()); + } + if (wcscmp(savedMyGames, L"DELETE_ME") != 0) { + MOBase::WriteRegistryValue(L"General", L"bUseMyGamesDirectory", savedMyGames, + iniFilePath.toStdWString().c_str()); + } else { + MOBase::WriteRegistryValue(L"General", L"bUseMyGamesDirectory", NULL, + iniFilePath.toStdWString().c_str()); + } + QFile::remove(saveIni); + } + // Otherwise just delete the setting + else { + MOBase::WriteRegistryValue(L"General", L"sLocalSavePath", NULL, + iniFilePath.toStdWString().c_str()); + MOBase::WriteRegistryValue(L"General", L"bUseMyGamesDirectory", NULL, + iniFilePath.toStdWString().c_str()); + } + } + + return enable != alreadyEnabled; +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.h b/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.h new file mode 100644 index 0000000..e5f2a92 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.h @@ -0,0 +1,54 @@ +/* +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef GAMEBRYOLOCALSAVEGAMES_H +#define GAMEBRYOLOCALSAVEGAMES_H + +#include <localsavegames.h> + +#include <QDir> +#include <QString> + +class GameGamebryo; + +class GamebryoLocalSavegames : public MOBase::LocalSavegames +{ + +public: + GamebryoLocalSavegames(const GameGamebryo* game, const QString& iniFileName); + + virtual MappingType mappings(const QDir& profileSaveDir) const override; + virtual bool prepareProfile(MOBase::IProfile* profile) override; + +protected: + // return the path from the local game directory to the local saves folder + // + // this is virtual so game plugins for complete game overhauld (Enderal, Nehrim, etc.) + // can override it properly + // + virtual QString localSavesDummy() const; + + QDir localSavesDirectory() const; + QDir localGameDirectory() const; + +private: + const GameGamebryo* m_Game; + QString m_IniFileName; +}; + +#endif // GAMEBRYOLOCALSAVEGAMES_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.cpp b/libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.cpp new file mode 100644 index 0000000..b5bace6 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.cpp @@ -0,0 +1,76 @@ +#include <ifiletree.h> + +#include "gamebryomoddatachecker.h" + +/** + * @return the list of possible folder names in data. + */ +auto GamebryoModDataChecker::possibleFolderNames() const -> const FileNameSet& +{ + static FileNameSet result{"fonts", + "interface", + "menus", + "meshes", + "music", + "scripts", + "shaders", + "sound", + "strings", + "textures", + "trees", + "video", + "facegen", + "materials", + "skse", + "obse", + "mwse", + "nvse", + "fose", + "f4se", + "distantlod", + "asi", + "SkyProc Patchers", + "Tools", + "MCM", + "icons", + "bookart", + "distantland", + "mits", + "splash", + "dllplugins", + "CalienteTools", + "NetScriptFramework", + "shadersfx"}; + return result; +} + +/** + * @return the extensions of possible files in data. + */ +auto GamebryoModDataChecker::possibleFileExtensions() const -> const FileNameSet& +{ + static FileNameSet result{"esp", "esm", "esl", "bsa", "ba2", "modgroups", "ini"}; + return result; +} + +GamebryoModDataChecker::GamebryoModDataChecker(const GameGamebryo* game) : m_Game(game) +{} + +GamebryoModDataChecker::CheckReturn GamebryoModDataChecker::dataLooksValid( + std::shared_ptr<const MOBase::IFileTree> fileTree) const +{ + auto& folders = possibleFolderNames(); + auto& suffixes = possibleFileExtensions(); + for (auto entry : *fileTree) { + if (entry->isDir()) { + if (folders.count(entry->name()) > 0) { + return CheckReturn::VALID; + } + } else { + if (suffixes.count(entry->suffix()) > 0) { + return CheckReturn::VALID; + } + } + } + return CheckReturn::INVALID; +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.h b/libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.h new file mode 100644 index 0000000..e0b0733 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.h @@ -0,0 +1,46 @@ +#ifndef GAMEBRYO_MODATACHECKER_H +#define GAMEBRYO_MODATACHECKER_H + +#include <ifiletree.h> +#include <moddatachecker.h> + +class GameGamebryo; + +/** + * @brief ModDataChecker for GameBryo games that look at folder and files in the "data" + * directory. + * + * The default implementation is game-agnostic and uses the list of folders and file + * extensions that were used before the ModDataChecker feature was added. It is possible + * to inherit the class to provide custom list of folders or filenames. + */ +class GamebryoModDataChecker : public MOBase::ModDataChecker +{ +public: + /** + * @brief Construct a new mod-data checker for GameBryo games. + */ + GamebryoModDataChecker(const GameGamebryo* game); + + virtual CheckReturn + dataLooksValid(std::shared_ptr<const MOBase::IFileTree> fileTree) const override; + +protected: + GameGamebryo const* const m_Game; + + using FileNameSet = std::set<QString, MOBase::FileNameComparator>; + + const GameGamebryo* game() const { return m_Game; } + + /** + * @return the list of possible folder names in data. + */ + virtual const FileNameSet& possibleFolderNames() const; + + /** + * @return the extensions of possible files in data. + */ + virtual const FileNameSet& possibleFileExtensions() const; +}; + +#endif // GAMEBRYO_MODATACHECKER_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.cpp b/libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.cpp new file mode 100644 index 0000000..85b66b0 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.cpp @@ -0,0 +1,116 @@ +#include "gamebryomoddatacontent.h" + +#include <igamefeatures.h> +#include <scriptextender.h> + +GamebryoModDataContent::GamebryoModDataContent( + MOBase::IGameFeatures const* gameFeatures) + : m_GameFeatures(gameFeatures), m_Enabled(CONTENT_MODGROUP + 1, true) +{} + +std::vector<GamebryoModDataContent::Content> +GamebryoModDataContent::getAllContents() const +{ + static std::vector<Content> GAMEBRYO_CONTENTS{ + {CONTENT_PLUGIN, QT_TR_NOOP("Plugins (ESP/ESM/ESL)"), ":/MO/gui/content/plugin"}, + {CONTENT_OPTIONAL, QT_TR_NOOP("Optional Plugins"), "", true}, + {CONTENT_INTERFACE, QT_TR_NOOP("Interface"), ":/MO/gui/content/interface"}, + {CONTENT_MESH, QT_TR_NOOP("Meshes"), ":/MO/gui/content/mesh"}, + {CONTENT_BSA, QT_TR_NOOP("Bethesda Archive"), ":/MO/gui/content/bsa"}, + {CONTENT_SCRIPT, QT_TR_NOOP("Scripts (Papyrus)"), ":/MO/gui/content/script"}, + {CONTENT_SKSE, QT_TR_NOOP("Script Extender Plugin"), ":/MO/gui/content/skse"}, + {CONTENT_SKSE_FILES, QT_TR_NOOP("Script Extender Files"), "", true}, + {CONTENT_SKYPROC, QT_TR_NOOP("SkyProc Patcher"), ":/MO/gui/content/skyproc"}, + {CONTENT_SOUND, QT_TR_NOOP("Sound or Music"), ":/MO/gui/content/sound"}, + {CONTENT_TEXTURE, QT_TR_NOOP("Textures"), ":/MO/gui/content/texture"}, + {CONTENT_MCM, QT_TR_NOOP("MCM Configuration"), ":/MO/gui/content/menu"}, + {CONTENT_INI, QT_TR_NOOP("INI Files"), ":/MO/gui/content/inifile"}, + {CONTENT_FACEGEN, QT_TR_NOOP("FaceGen Data"), ":/MO/gui/content/facegen"}, + {CONTENT_MODGROUP, QT_TR_NOOP("ModGroup Files"), ":/MO/gui/content/modgroup"}}; + + // Copy the list of enabled contents: + std::vector<Content> contents; + std::copy_if(std::begin(GAMEBRYO_CONTENTS), std::end(GAMEBRYO_CONTENTS), + std::back_inserter(contents), [this](auto e) { + return m_Enabled[e.id()]; + }); + return contents; +} + +std::vector<int> GamebryoModDataContent::getContentsFor( + std::shared_ptr<const MOBase::IFileTree> fileTree) const +{ + std::vector<int> contents; + + for (auto e : *fileTree) { + if (e->isFile()) { + auto suffix = e->suffix().toLower(); + if (m_Enabled[CONTENT_PLUGIN] && + (suffix == "esp" || suffix == "esm" || suffix == "esl")) { + contents.push_back(CONTENT_PLUGIN); + } else if (m_Enabled[CONTENT_BSA] && (suffix == "bsa" || suffix == "ba2")) { + contents.push_back(CONTENT_BSA); + } else if (m_Enabled[CONTENT_INI] && suffix == "ini" && + e->compare("meta.ini") != 0) { + contents.push_back(CONTENT_INI); + } else if (m_Enabled[CONTENT_MODGROUP] && suffix == "modgroups") { + contents.push_back(CONTENT_MODGROUP); + } + } else { + if (m_Enabled[CONTENT_TEXTURE] && + (e->compare("textures") == 0 || e->compare("icons") == 0 || + e->compare("bookart") == 0)) { + contents.push_back(CONTENT_TEXTURE); + } else if (m_Enabled[CONTENT_MESH] && e->compare("meshes") == 0) { + contents.push_back(CONTENT_MESH); + } else if (m_Enabled[CONTENT_INTERFACE] && + (e->compare("interface") == 0 || e->compare("menus") == 0)) { + contents.push_back(CONTENT_INTERFACE); + } else if (m_Enabled[CONTENT_SOUND] && e->compare("music") == 0 || + e->compare("sound") == 0) { + contents.push_back(CONTENT_SOUND); + } else if (m_Enabled[CONTENT_SCRIPT] && e->compare("scripts") == 0) { + contents.push_back(CONTENT_SCRIPT); + } else if (m_Enabled[CONTENT_SKYPROC] && e->compare("SkyProc Patchers") == 0) { + contents.push_back(CONTENT_SKYPROC); + } else if (m_Enabled[CONTENT_MCM] && e->compare("MCM") == 0) { + contents.push_back(CONTENT_MCM); + } else if (m_Enabled[CONTENT_OPTIONAL] && e->compare("Optional") == 0 && + e->astree()->size() > 0) { + contents.push_back(CONTENT_OPTIONAL); + } + } + } + + if (m_Enabled[CONTENT_FACEGEN]) { + auto e1 = fileTree->findDirectory("meshes/actors/character/facegendata"); + if (e1) { + contents.push_back(CONTENT_FACEGEN); + } else { + auto e2 = fileTree->findDirectory("textures/actors/character/facegendata"); + if (e2) { + contents.push_back(CONTENT_FACEGEN); + } + } + } + + auto extender = m_GameFeatures->gameFeature<MOBase::ScriptExtender>(); + if (extender != nullptr) { + auto e = fileTree->findDirectory(extender->PluginPath()); + if (e) { + if (m_Enabled[CONTENT_SKSE_FILES]) { + contents.push_back(CONTENT_SKSE_FILES); + } + if (m_Enabled[CONTENT_SKSE]) { + for (auto f : *e) { + if (f->hasSuffix("dll")) { + contents.push_back(CONTENT_SKSE); + break; + } + } + } + } + } + + return contents; +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.h b/libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.h new file mode 100644 index 0000000..be02331 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.h @@ -0,0 +1,75 @@ +#ifndef GAMEBRYO_MODDATACONTENT_H +#define GAMEBRYO_MODDATACONTENT_H + +#include <ifiletree.h> +#include <moddatacontent.h> + +namespace MOBase +{ +class IGameFeatures; +} + +/** + * @brief ModDataContent for GameBryo games. + * + */ +class GamebryoModDataContent : public MOBase::ModDataContent +{ +protected: + /** + * Note: These are used to index m_Enabled so should have standard + * enum values, not custom ones. + */ + enum EContent + { + CONTENT_PLUGIN, + CONTENT_OPTIONAL, + CONTENT_TEXTURE, + CONTENT_MESH, + CONTENT_BSA, + CONTENT_INTERFACE, + CONTENT_SOUND, + CONTENT_SCRIPT, + CONTENT_SKSE, + CONTENT_SKSE_FILES, + CONTENT_SKYPROC, + CONTENT_MCM, + CONTENT_INI, + CONTENT_FACEGEN, + CONTENT_MODGROUP + }; + + /** + * This is the first value that can be used for game-specific contents. + */ + constexpr static auto CONTENT_NEXT_VALUE = CONTENT_MODGROUP + 1; + +public: + /** + * + */ + GamebryoModDataContent(const MOBase::IGameFeatures* gameFeatures); + + /** + * @return the list of all possible contents for the corresponding game. + */ + virtual std::vector<Content> getAllContents() const override; + + /** + * @brief Retrieve the list of contents in the given tree. + * + * @param fileTree The tree corresponding to the mod to retrieve contents for. + * + * @return the IDs of the content in the given tree. + */ + virtual std::vector<int> + getContentsFor(std::shared_ptr<const MOBase::IFileTree> fileTree) const override; + +protected: + MOBase::IGameFeatures const* const m_GameFeatures; + + // List of enabled contents: + std::vector<bool> m_Enabled; +}; + +#endif // GAMEBRYO_MODDATACONTENT_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryosavegame.cpp b/libs/game_gamebryo/src/gamebryo/gamebryosavegame.cpp new file mode 100644 index 0000000..596b19b --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryosavegame.cpp @@ -0,0 +1,606 @@ +#include "gamebryosavegame.h" + +#include "iplugingame.h" +#include "log.h" +#include "scriptextender.h" + +#include <QDate> +#include <QFile> +#include <QFileInfo> +#include <QScopedArrayPointer> +#include <QTime> + +#include <Windows.h> +#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) +{ + unsigned long width; + read(width); + unsigned long height; + read(height); + return readImage(width, height, scale, alpha); +} + +QImage GamebryoSaveGame::FileWrapper::readImage(unsigned long width, + unsigned long 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(); +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryosavegame.h b/libs/game_gamebryo/src/gamebryo/gamebryosavegame.h new file mode 100644 index 0000000..44d0584 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryosavegame.h @@ -0,0 +1,240 @@ +#ifndef GAMEBRYOSAVEGAME_H +#define GAMEBRYOSAVEGAME_H + +#include "isavegame.h" +#include "memoizedlock.h" + +#include <QDateTime> +#include <QFile> +#include <QImage> +#include <QString> +#include <QStringList> + +#include <stddef.h> +#include <stdexcept> + +struct _SYSTEMTIME; + +namespace MOBase +{ +class IPluginGame; +} // namespace MOBase + +class GameGamebryo; + +class GamebryoSaveGame : public MOBase::ISaveGame +{ +public: + GamebryoSaveGame(QString const& file, GameGamebryo const* game, + bool const lightEnabled = false, bool const mediumEnabled = false); + + virtual ~GamebryoSaveGame(); + +public: // ISaveGame interface + virtual QString getFilepath() const override; + virtual QDateTime getCreationTime() const override; + virtual QString getName() const override; + virtual QString getSaveGroupIdentifier() const override; + virtual QStringList allFiles() const override; + +public: + bool hasScriptExtenderFile() const; + + // Simple getters + virtual QString getPCName() const { return m_PCName; } + virtual unsigned short getPCLevel() const { return m_PCLevel; } + virtual QString getPCLocation() const { return m_PCLocation; } + virtual unsigned long getSaveNumber() const { return m_SaveNumber; } + + QStringList const& getPlugins() const { return m_DataFields.value()->Plugins; } + QStringList const& getMediumPlugins() const + { + return m_DataFields.value()->MediumPlugins; + } + QStringList const& getLightPlugins() const + { + return m_DataFields.value()->LightPlugins; + } + QImage const& getScreenshot() const { return m_DataFields.value()->Screenshot; } + + bool isMediumEnabled() const { return m_MediumEnabled; } + + bool isLightEnabled() const { return m_LightEnabled; } + + enum class StringType + { + TYPE_BZSTRING, + TYPE_BSTRING, + TYPE_WSTRING + }; + + enum class StringFormat + { + UTF8, + LOCAL8BIT + }; + +protected: + friend class FileWrapper; + + class FileWrapper + { + public: + /** + * @brief Construct the save file information. + * + * @param filepath The path to the save file. + * @params expected Expecte bytes at start of file. + * + **/ + FileWrapper(QString const& filepath, QString const& expected); + + /** Set this for save games that have a marker at the end of each + * field. Specifically fallout + **/ + void setHasFieldMarkers(bool); + + /** Set bz string mode (1 byte length, null terminated) + **/ + void setPluginString(StringType); + + /** Set string format (utf-8, windows local 8 bit strings) + **/ + void setPluginStringFormat(StringFormat); + + template <typename T> + void skip(int count = 1) + { + if (!m_File.seek(m_File.pos() + count * sizeof(T))) { + throw std::runtime_error("unexpected end of file"); + } + } + + template <typename T> + void read(T& value) + { + int read = m_File.read(reinterpret_cast<char*>(&value), sizeof(T)); + if (read != sizeof(T)) { + throw std::runtime_error("unexpected end of file"); + } + if (m_HasFieldMarkers) { + skip<char>(); + } + } + + template <> + void read<QString>(QString& value); + + void seek(unsigned long pos) + { + if (!m_File.seek(pos)) { + throw std::runtime_error("unexpected end of file"); + } + } + + void read(void* buff, std::size_t length); + + /* Reads RGB image from save + * Assumes picture dimentions come immediately before the save + */ + QImage readImage(int scale = 0, bool alpha = false); + + /* Reads RGB image from save */ + QImage readImage(unsigned long width, unsigned long height, int scale = 0, + bool alpha = false); + + /* Sets the compression type. */ + void setCompressionType(uint16_t type); + + /* uncompress the begining of the compressed block */ + bool openCompressedData(int bytesToIgnore = 0); + + /* read the next compressed block */ + bool readNextChunk(); + + /* frees the uncompressed block */ + void closeCompressedData(); + + /* Read the save game version in the compressed block */ + uint8_t readChar(int bytesToIgnore = 0); + + uint16_t readShort(int bytesToIgnore = 0); + + uint32_t readInt(int bytesToIgnore = 0); + + uint64_t readLong(int bytesToIgnore = 0); + + float_t readFloat(int bytesToIgnore = 0); + + /* Read the plugin list */ + QStringList readPlugins(int bytesToIgnore = 0, int extraData = 0, + const QStringList& corePlugins = {}); + + /* Read the light plugin list */ + QStringList readLightPlugins(int bytesToIgnore = 0, int extraData = 0, + const QStringList& corePlugins = {}); + + /* Read the medium plugin list */ + QStringList readMediumPlugins(int bytesToIgnore = 0, int extraData = 0, + const QStringList& corePlugins = {}); + + void close(); + + private: + QFile m_File; + uint64_t m_NextChunk; + uint64_t m_UncompressedSize; + bool m_HasFieldMarkers; + StringType m_PluginString; + StringFormat m_PluginStringFormat; + QDataStream* m_Data; + uint16_t m_CompressionType = 0; + + private: + template <typename T> + void readQDataStream(QDataStream& data, T& value); + + void readQDataStream(QDataStream& data, void* buff, std::size_t length); + + void skipQDataStream(QDataStream& data, std::size_t length); + + QStringList readPluginData(uint32_t count, int extraData, + const QStringList corePlugins); + }; + + void setCreationTime(_SYSTEMTIME const& time); + + GameGamebryo const* m_Game; + bool m_MediumEnabled; + bool m_LightEnabled; + + QString m_FileName; + QString m_PCName; + unsigned short m_PCLevel; + QString m_PCLocation; + unsigned long m_SaveNumber; + QDateTime m_CreationTime; + + // Those three fields are usually much slower to fetch than + // the other, so we do not fetch them if not needed. + // + // This is virtual so child class can add fields if those are + // hard to access. + struct DataFields + { + QStringList Plugins; + QStringList LightPlugins; + QStringList MediumPlugins; + QImage Screenshot; + + // We need this constructor. + DataFields() {} + virtual ~DataFields() {} + }; + MOBase::MemoizedLocked<std::unique_ptr<DataFields>> m_DataFields; + + // Fetch the field. + virtual std::unique_ptr<DataFields> fetchDataFields() const = 0; +}; + +#endif // GAMEBRYOSAVEGAME_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.cpp b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.cpp new file mode 100644 index 0000000..6d1289a --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.cpp @@ -0,0 +1,100 @@ +#include "gamebryosavegameinfo.h" + +#include "gamebryosavegame.h" +#include "gamebryosavegameinfowidget.h" +#include "gamegamebryo.h" +#include "imodinterface.h" +#include "imoinfo.h" +#include "iplugingame.h" +#include "ipluginlist.h" + +#include <QDir> +#include <QString> +#include <QStringList> + +GamebryoSaveGameInfo::GamebryoSaveGameInfo(GameGamebryo const* game) : m_Game(game) {} + +GamebryoSaveGameInfo::~GamebryoSaveGameInfo() {} + +GamebryoSaveGameInfo::MissingAssets +GamebryoSaveGameInfo::getMissingAssets(MOBase::ISaveGame const& save) const +{ + GamebryoSaveGame const& gamebryoSave = dynamic_cast<GamebryoSaveGame const&>(save); + MOBase::IOrganizer* organizerCore = m_Game->m_Organizer; + + // collect the list of missing plugins + MissingAssets missingAssets; + + for (QString const& pluginName : gamebryoSave.getPlugins()) { + switch (organizerCore->pluginList()->state(pluginName)) { + case MOBase::IPluginList::STATE_INACTIVE: + missingAssets[pluginName] = + ProvidingModules{organizerCore->pluginList()->origin(pluginName)}; + break; + case MOBase::IPluginList::STATE_MISSING: + missingAssets[pluginName] = ProvidingModules(); + break; + } + } + + for (QString const& pluginName : gamebryoSave.getLightPlugins()) { + switch (organizerCore->pluginList()->state(pluginName)) { + case MOBase::IPluginList::STATE_INACTIVE: + missingAssets[pluginName] = + ProvidingModules{organizerCore->pluginList()->origin(pluginName)}; + break; + case MOBase::IPluginList::STATE_MISSING: + missingAssets[pluginName] = ProvidingModules(); + break; + } + } + + // Find out any other mods that might contain the esp/esm + QStringList espFilter({"*.esp", "*.esl", "*.esm"}); + + QString dataDir(organizerCore->managedGame()->dataDirectory().absolutePath()); + + // Search normal mods. A note: This will also find mods in data. + for (QString const& mod : organizerCore->modList()->allModsByProfilePriority()) { + MOBase::IModInterface* modInfo = organizerCore->modList()->getMod(mod); + QStringList esps = QDir(modInfo->absolutePath()).entryList(espFilter); + for (QString const& esp : esps) { + MissingAssets::iterator iter = missingAssets.find(esp); + if (modInfo->absolutePath() == dataDir) { + // We have to prune esps that reside in the data directory, otherwise + // you get all the unmanaged mods listed as potential candidates for + // enabling + if (modInfo->name() != organizerCore->pluginList()->origin(esp)) { + continue; + } + } + if (iter != missingAssets.end()) { + if (!iter->contains(modInfo->name())) { + iter->push_back(modInfo->name()); + } + } + } + } + + // search in overwrite + { + QDir overwriteDir(organizerCore->overwritePath()); + QStringList esps = overwriteDir.entryList(espFilter); + for (const QString& esp : esps) { + MissingAssets::iterator iter = missingAssets.find(esp); + if (iter != missingAssets.end()) { + if (!iter->contains("<overwrite>")) { + iter->push_back("<overwrite>"); + } + } + } + } + + return missingAssets; +} + +MOBase::ISaveGameInfoWidget* +GamebryoSaveGameInfo::getSaveGameWidget(QWidget* parent) const +{ + return new GamebryoSaveGameInfoWidget(this, parent); +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.h b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.h new file mode 100644 index 0000000..e214293 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.h @@ -0,0 +1,23 @@ +#ifndef GAMEBRYOSAVEGAMEINFO_H +#define GAMEBRYOSAVEGAMEINFO_H + +#include "savegameinfo.h" + +class GameGamebryo; + +class GamebryoSaveGameInfo : public MOBase::SaveGameInfo +{ +public: + GamebryoSaveGameInfo(GameGamebryo const* game); + ~GamebryoSaveGameInfo(); + + virtual MissingAssets getMissingAssets(MOBase::ISaveGame const& save) const override; + + virtual MOBase::ISaveGameInfoWidget* getSaveGameWidget(QWidget*) const override; + +protected: + friend class GamebryoSaveGameInfoWidget; + GameGamebryo const* m_Game; +}; + +#endif // GAMEBRYOSAVEGAMEINFO_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.cpp b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.cpp new file mode 100644 index 0000000..8c95dad --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.cpp @@ -0,0 +1,199 @@ +#include "gamebryosavegameinfowidget.h" +#include "ui_gamebryosavegameinfowidget.h" + +#include "gamebryosavegame.h" +#include "gamebryosavegameinfo.h" +#include "gamegamebryo.h" +#include "imoinfo.h" +#include "ipluginlist.h" + +#include <QDate> +#include <QDateTime> +#include <QFont> +#include <QFrame> +#include <QLabel> +#include <QLayout> +#include <QLayoutItem> +#include <QPixmap> +#include <QString> +#include <QStyle> +#include <QTime> +#include <QVBoxLayout> + +#include <Qt> +#include <QtGlobal> + +#include <memory> + +GamebryoSaveGameInfoWidget::GamebryoSaveGameInfoWidget(GamebryoSaveGameInfo const* info, + QWidget* parent) + : MOBase::ISaveGameInfoWidget(parent), ui(new Ui::GamebryoSaveGameInfoWidget), + m_Info(info) +{ + ui->setupUi(this); + this->setWindowFlags(Qt::ToolTip | Qt::BypassGraphicsProxyWidget); + setWindowOpacity(style()->styleHint(QStyle::SH_ToolTipLabel_Opacity, 0, this) / + qreal(255.0)); + ui->gameFrame->setStyleSheet("background-color: transparent;"); + + QVBoxLayout* gameLayout = new QVBoxLayout(); + gameLayout->setContentsMargins(0, 0, 0, 0); + gameLayout->setSpacing(2); + ui->gameFrame->setLayout(gameLayout); +} + +GamebryoSaveGameInfoWidget::~GamebryoSaveGameInfoWidget() +{ + delete ui; +} + +void GamebryoSaveGameInfoWidget::setSave(MOBase::ISaveGame const& save) +{ + auto& gamebryoSave = dynamic_cast<GamebryoSaveGame const&>(save); + ui->saveNumLabel->setText(QString("%1").arg(gamebryoSave.getSaveNumber())); + ui->characterLabel->setText(gamebryoSave.getPCName()); + ui->locationLabel->setText(gamebryoSave.getPCLocation()); + ui->levelLabel->setText(QString("%1").arg(gamebryoSave.getPCLevel())); + // This somewhat contorted code is because on my system at least, the + // old way of doing this appears to give short date and long time. + QDateTime t = gamebryoSave.getCreationTime().toLocalTime(); + ui->dateLabel->setText( + QLocale::system().toString(t.date(), QLocale::FormatType::ShortFormat) + " " + + QLocale::system().toString(t.time())); + ui->screenshotLabel->setPixmap(QPixmap::fromImage(gamebryoSave.getScreenshot())); + if (ui->gameFrame->layout() != nullptr) { + QLayoutItem* item = nullptr; + while ((item = ui->gameFrame->layout()->takeAt(0)) != nullptr) { + delete item->widget(); + delete item; + } + ui->gameFrame->layout()->setSizeConstraint(QLayout::SetFixedSize); + } + + // Resize box to new content + this->resize(0, 0); + + QLayout* layout = ui->gameFrame->layout(); + if (gamebryoSave.hasScriptExtenderFile()) { + QLabel* scriptExtender = new QLabel(tr("Has Script Extender Data")); + QFont headerFont = scriptExtender->font(); + headerFont.setBold(true); + layout->addWidget(scriptExtender); + } + QLabel* header = new QLabel(tr("Missing ESPs")); + QFont headerFont = header->font(); + QFont contentFont = headerFont; + headerFont.setItalic(true); + contentFont.setBold(true); + contentFont.setPointSize(7); + header->setFont(headerFont); + layout->addWidget(header); + int count = 0; + MOBase::IPluginList* pluginList = m_Info->m_Game->m_Organizer->pluginList(); + for (QString const& pluginName : gamebryoSave.getPlugins()) { + if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) { + continue; + } + + ++count; + + if (count > 7) { + break; + } + + QLabel* pluginLabel = new QLabel(pluginName); + pluginLabel->setIndent(10); + pluginLabel->setFont(contentFont); + layout->addWidget(pluginLabel); + } + if (count > 7) { + QLabel* dotDotLabel = new QLabel("..."); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (count == 0) { + QLabel* dotDotLabel = new QLabel(tr("None")); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (gamebryoSave.isMediumEnabled()) { + QLabel* headerEsh = new QLabel(tr("Missing ESHs")); + QFont headerEshFont = headerEsh->font(); + QFont contentEshFont = headerEshFont; + headerEshFont.setItalic(true); + contentEshFont.setBold(true); + contentEshFont.setPointSize(7); + headerEsh->setFont(headerEshFont); + layout->addWidget(headerEsh); + int countEsh = 0; + for (QString const& pluginName : gamebryoSave.getMediumPlugins()) { + if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) { + continue; + } + + ++countEsh; + + if (countEsh > 7) { + break; + } + + QLabel* pluginLabel = new QLabel(pluginName); + pluginLabel->setIndent(10); + pluginLabel->setFont(contentFont); + layout->addWidget(pluginLabel); + } + if (countEsh > 7) { + QLabel* dotDotLabel = new QLabel("..."); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (countEsh == 0) { + QLabel* dotDotLabel = new QLabel(tr("None")); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + } + if (gamebryoSave.isLightEnabled()) { + QLabel* headerEsl = new QLabel(tr("Missing ESLs")); + QFont headerEslFont = headerEsl->font(); + QFont contentEslFont = headerEslFont; + headerEslFont.setItalic(true); + contentEslFont.setBold(true); + contentEslFont.setPointSize(7); + headerEsl->setFont(headerEslFont); + layout->addWidget(headerEsl); + int countEsl = 0; + for (QString const& pluginName : gamebryoSave.getLightPlugins()) { + if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) { + continue; + } + + ++countEsl; + + if (countEsl > 7) { + break; + } + + QLabel* pluginLabel = new QLabel(pluginName); + pluginLabel->setIndent(10); + pluginLabel->setFont(contentFont); + layout->addWidget(pluginLabel); + } + if (countEsl > 7) { + QLabel* dotDotLabel = new QLabel("..."); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (countEsl == 0) { + QLabel* dotDotLabel = new QLabel(tr("None")); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + } +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.h b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.h new file mode 100644 index 0000000..6642863 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.h @@ -0,0 +1,30 @@ +#ifndef GAMEBRYOSAVEGAMEINFOWIDGET_H +#define GAMEBRYOSAVEGAMEINFOWIDGET_H + +#include "isavegameinfowidget.h" + +#include <QObject> + +class GamebryoSaveGameInfo; + +namespace Ui +{ +class GamebryoSaveGameInfoWidget; +} + +class GamebryoSaveGameInfoWidget : public MOBase::ISaveGameInfoWidget +{ + Q_OBJECT + +public: + GamebryoSaveGameInfoWidget(GamebryoSaveGameInfo const* info, QWidget* parent); + ~GamebryoSaveGameInfoWidget(); + + virtual void setSave(MOBase::ISaveGame const&) override; + +private: + Ui::GamebryoSaveGameInfoWidget* ui; + GamebryoSaveGameInfo const* m_Info; +}; + +#endif // GAMEBRYOSAVEGAMEINFOWIDGET_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.ui b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.ui new file mode 100644 index 0000000..ea45d10 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.ui @@ -0,0 +1,209 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>GamebryoSaveGameInfoWidget</class> + <widget class="QWidget" name="GamebryoSaveGameInfoWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string/> + </property> + <property name="toolTip"> + <string notr="true"/> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QFormLayout" name="formLayout"> + <property name="fieldGrowthPolicy"> + <enum>QFormLayout::AllNonFixedFieldsGrow</enum> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="font"> + <font> + <italic>true</italic> + </font> + </property> + <property name="text"> + <string>Save #</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="font"> + <font> + <italic>true</italic> + </font> + </property> + <property name="text"> + <string>Character</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_4"> + <property name="font"> + <font> + <italic>true</italic> + </font> + </property> + <property name="text"> + <string>Level</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_3"> + <property name="font"> + <font> + <italic>true</italic> + </font> + </property> + <property name="text"> + <string>Location</string> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_5"> + <property name="font"> + <font> + <italic>true</italic> + </font> + </property> + <property name="text"> + <string>Date</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="saveNumLabel"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string notr="true"/> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="characterLabel"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string notr="true"/> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="levelLabel"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLabel" name="locationLabel"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string notr="true"/> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLabel" name="dateLabel"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QFrame" name="gameFrame"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="screenshotLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>16777215</height> + </size> + </property> + <property name="autoFillBackground"> + <bool>false</bool> + </property> + <property name="text"> + <string notr="true"/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.cpp b/libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.cpp new file mode 100644 index 0000000..e3f709a --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.cpp @@ -0,0 +1,46 @@ +#include "gamebryoscriptextender.h" + +#include "gamegamebryo.h" + +#include "utility.h" + +#include <QDir> +#include <QString> + +GamebryoScriptExtender::GamebryoScriptExtender(const GameGamebryo* game) : m_Game(game) +{} + +GamebryoScriptExtender::~GamebryoScriptExtender() {} + +QString GamebryoScriptExtender::loaderName() const +{ + return BinaryName(); +} + +QString GamebryoScriptExtender::loaderPath() const +{ + return m_Game->gameDirectory().absoluteFilePath(loaderName()); +} + +QString GamebryoScriptExtender::savegameExtension() const +{ + return m_Game->savegameSEExtension(); +} + +bool GamebryoScriptExtender::isInstalled() const +{ + // A note: It is possibly also OK if xxse_steam_loader.dll exists, but it's + // not clear why that would exist and the exe not if you'd installed it per + // instructions, and it'd mess up NCC installs a treat. + return m_Game->gameDirectory().exists(loaderName()); +} + +QString GamebryoScriptExtender::getExtenderVersion() const +{ + return MOBase::getFileVersion(m_Game->gameDirectory().absoluteFilePath(loaderName())); +} + +WORD GamebryoScriptExtender::getArch() const +{ + return m_Game->getArch(loaderName()); +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.h b/libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.h new file mode 100644 index 0000000..7ab4f1d --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.h @@ -0,0 +1,31 @@ +#ifndef GAMEBRYOSCRIPTEXTENDER_H +#define GAMEBRYOSCRIPTEXTENDER_H + +#include "scriptextender.h" + +class GameGamebryo; + +class GamebryoScriptExtender : public MOBase::ScriptExtender +{ +public: + GamebryoScriptExtender(GameGamebryo const* game); + + virtual ~GamebryoScriptExtender(); + + virtual QString loaderName() const override; + + virtual QString loaderPath() const override; + + virtual QString savegameExtension() const override; + + virtual bool isInstalled() const override; + + virtual QString getExtenderVersion() const override; + + virtual WORD getArch() const override; + +protected: + GameGamebryo const* const m_Game; +}; + +#endif // GAMEBRYOSCRIPTEXTENDER_H diff --git a/libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.cpp b/libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.cpp new file mode 100644 index 0000000..e5c7608 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.cpp @@ -0,0 +1,51 @@ +#include "gamebryounmanagedmods.h" +#include "gamegamebryo.h" +#include <pluginsetting.h> + +GamebryoUnmangedMods::GamebryoUnmangedMods(const GameGamebryo* game) : m_Game(game) {} + +GamebryoUnmangedMods::~GamebryoUnmangedMods() {} + +QStringList GamebryoUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList dlcPlugins = m_Game->DLCPlugins(); + QStringList mainPlugins = m_Game->primaryPlugins(); + + QDir dataDir(m_Game->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!mainPlugins.contains(fileName, Qt::CaseInsensitive) && + (!onlyOfficial || dlcPlugins.contains(fileName, Qt::CaseInsensitive))) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + + return result; +} + +QString GamebryoUnmangedMods::displayName(const QString& modName) const +{ + return modName; +} + +QFileInfo GamebryoUnmangedMods::referenceFile(const QString& modName) const +{ + QFileInfoList files = + m_Game->dataDirectory().entryInfoList(QStringList() << modName + ".es*"); + if (files.size() > 0) { + return files.at(0); + } else { + return QFileInfo(); + } +} + +QStringList GamebryoUnmangedMods::secondaryFiles(const QString& modName) const +{ + QStringList archives; + QDir dataDir = m_Game->dataDirectory(); + for (const QString& archiveName : dataDir.entryList({modName + "*.bsa"})) { + archives.append(dataDir.absoluteFilePath(archiveName)); + } + return archives; +} diff --git a/libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.h b/libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.h new file mode 100644 index 0000000..38e2ba5 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.h @@ -0,0 +1,26 @@ +#ifndef GAMEBRYOUNMANAGEDMODS_H +#define GAMEBRYOUNMANAGEDMODS_H + +#include <unmanagedmods.h> + +class GameGamebryo; + +class GamebryoUnmangedMods : public MOBase::UnmanagedMods +{ +public: + GamebryoUnmangedMods(const GameGamebryo* game); + ~GamebryoUnmangedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; + virtual QString displayName(const QString& modName) const override; + virtual QFileInfo referenceFile(const QString& modName) const override; + virtual QStringList secondaryFiles(const QString& modName) const override; + +protected: + const GameGamebryo* game() const { return m_Game; } + +private: + const GameGamebryo* m_Game; +}; + +#endif // GAMEBRYOUNMANAGEDMODS_H diff --git a/libs/game_gamebryo/src/gamebryo/gamegamebryo.cpp b/libs/game_gamebryo/src/gamebryo/gamegamebryo.cpp new file mode 100644 index 0000000..347c2d8 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamegamebryo.cpp @@ -0,0 +1,478 @@ +#include "gamegamebryo.h" + +#include "bsainvalidation.h" +#include "dataarchives.h" +#include "gamebryomoddatacontent.h" +#include "gamebryosavegame.h" +#include "gameplugins.h" +#include "iprofile.h" +#include "log.h" +#include "registry.h" +#include "savegameinfo.h" +#include "scopeguard.h" +#include "scriptextender.h" +#include "utility.h" +#include "vdf_parser.h" + +#include <QDir> +#include <QDirIterator> +#include <QFile> +#include <QFileInfo> +#include <QIcon> +#include <QJsonDocument> +#include <QJsonValue> + +#include <QtDebug> +#include <QtGlobal> + +#include <Knownfolders.h> +#include <Shlobj.h> +#include <Windows.h> +#include <winreg.h> +#include <winver.h> + +#include <optional> +#include <string> +#include <vector> + +GameGamebryo::GameGamebryo() {} + +void GameGamebryo::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath(gameName()); +} + +bool GameGamebryo::init(MOBase::IOrganizer* moInfo) +{ + m_Organizer = moInfo; + m_Organizer->onAboutToRun([this](const auto& binary) { + return prepareIni(binary); + }); + return true; +} + +bool GameGamebryo::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +QIcon GameGamebryo::gameIcon() const +{ + return MOBase::iconForExecutable(gameDirectory().absoluteFilePath(binaryName())); +} + +QDir GameGamebryo::gameDirectory() const +{ + return QDir(m_GamePath); +} + +QDir GameGamebryo::dataDirectory() const +{ + return gameDirectory().absoluteFilePath("data"); +} + +void GameGamebryo::setGamePath(const QString& path) +{ + m_GamePath = path; +} + +QDir GameGamebryo::documentsDirectory() const +{ + return m_MyGamesPath; +} + +QDir GameGamebryo::savesDirectory() const +{ + return QDir(myGamesPath() + "/Saves"); +} + +std::vector<std::shared_ptr<const MOBase::ISaveGame>> +GameGamebryo::listSaves(QDir folder) const +{ + QStringList filters; + filters << QString("*.") + savegameExtension(); + + std::vector<std::shared_ptr<const MOBase::ISaveGame>> saves; + for (auto info : folder.entryInfoList(filters, QDir::Files)) { + try { + saves.push_back(makeSaveGame(info.filePath())); + } catch (std::exception& e) { + MOBase::log::error("{}", e.what()); + continue; + } + } + + return saves; +} + +void GameGamebryo::setGameVariant(const QString& variant) +{ + m_GameVariant = variant; +} + +QString GameGamebryo::binaryName() const +{ + return gameShortName() + ".exe"; +} + +MOBase::IPluginGame::LoadOrderMechanism GameGamebryo::loadOrderMechanism() const +{ + return LoadOrderMechanism::FileTime; +} + +MOBase::IPluginGame::SortMechanism GameGamebryo::sortMechanism() const +{ + return SortMechanism::LOOT; +} + +bool GameGamebryo::looksValid(QDir const& path) const +{ + // Check for <prog>.exe for now. + return path.exists(binaryName()); +} + +QString GameGamebryo::gameVersion() const +{ + // We try the file version, but if it looks invalid (starts with the fallback + // version), we look the product version instead. If the product version is + // not empty, we use it. + QString binaryAbsPath = gameDirectory().absoluteFilePath(binaryName()); + QString version = MOBase::getFileVersion(binaryAbsPath); + if (version.startsWith(FALLBACK_GAME_VERSION)) { + QString pversion = MOBase::getProductVersion(binaryAbsPath); + if (!pversion.isEmpty()) { + version = pversion; + } + } + return version; +} + +QString GameGamebryo::getLauncherName() const +{ + return gameShortName() + "Launcher.exe"; +} + +WORD GameGamebryo::getArch(QString const& program) const +{ + WORD arch = 0; + // This *really* needs to be factored out + std::wstring app_name = + L"\\\\?\\" + + QDir::toNativeSeparators(this->gameDirectory().absoluteFilePath(program)) + .toStdWString(); + + WIN32_FIND_DATAW FindFileData; + HANDLE hFind = ::FindFirstFileW(app_name.c_str(), &FindFileData); + + // exit if the binary was not found + if (hFind == INVALID_HANDLE_VALUE) + return arch; + + HANDLE hFile = INVALID_HANDLE_VALUE; + HANDLE hMapping = INVALID_HANDLE_VALUE; + LPVOID addrHeader = nullptr; + PIMAGE_NT_HEADERS peHdr = nullptr; + + hFile = CreateFileW(app_name.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); + if (hFile == INVALID_HANDLE_VALUE) + goto cleanup; + + hMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, + program.toStdWString().c_str()); + if (hMapping == INVALID_HANDLE_VALUE) + goto cleanup; + + addrHeader = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0); + if (addrHeader == NULL) + goto cleanup; // couldn't memory map the file + + peHdr = ImageNtHeader(addrHeader); + if (peHdr == NULL) + goto cleanup; // couldn't read the header + + arch = peHdr->FileHeader.Machine; + +cleanup: // release all of our handles + FindClose(hFind); + if (hFile != INVALID_HANDLE_VALUE) + CloseHandle(hFile); + if (hMapping != INVALID_HANDLE_VALUE) + CloseHandle(hMapping); + return arch; +} + +QFileInfo GameGamebryo::findInGameFolder(const QString& relativePath) const +{ + return QFileInfo(m_GamePath + "/" + relativePath); +} + +QString GameGamebryo::identifyGamePath() const +{ + QString path = "Software\\Bethesda Softworks\\" + gameShortName(); + return findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +} + +bool GameGamebryo::prepareIni(const QString& exec) +{ + MOBase::IProfile* profile = m_Organizer->profile(); + + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : documentsDirectory().absolutePath(); + + if (!iniFiles().isEmpty()) { + + QString profileIni = basePath + "/" + iniFiles()[0]; + + WCHAR setting[512]; + if (!GetPrivateProfileStringW(L"Launcher", L"bEnableFileSelection", L"0", setting, + 512, profileIni.toStdWString().c_str()) || + wcstol(setting, nullptr, 10) != 1) { + MOBase::WriteRegistryValue(L"Launcher", L"bEnableFileSelection", L"1", + profileIni.toStdWString().c_str()); + } + } + + return true; +} + +QString GameGamebryo::selectedVariant() const +{ + return m_GameVariant; +} + +QString GameGamebryo::myGamesPath() const +{ + return m_MyGamesPath; +} + +/*static*/ QString GameGamebryo::getLootPath() +{ + return findInRegistry(HKEY_LOCAL_MACHINE, L"Software\\LOOT", L"Installed Path") + + "/Loot.exe"; +} + +QString GameGamebryo::localAppFolder() +{ + QString result = getKnownFolderPath(FOLDERID_LocalAppData, false); + if (result.isEmpty()) { + // fallback: try the registry + result = getSpecialPath("Local AppData"); + } + return result; +} + +void GameGamebryo::copyToProfile(QString const& sourcePath, + QDir const& destinationDirectory, + QString const& sourceFileName) +{ + copyToProfile(sourcePath, destinationDirectory, sourceFileName, sourceFileName); +} + +void GameGamebryo::copyToProfile(QString const& sourcePath, + QDir const& destinationDirectory, + QString const& sourceFileName, + QString const& destinationFileName) +{ + QString filePath = destinationDirectory.absoluteFilePath(destinationFileName); + if (!QFileInfo(filePath).exists()) { + if (!MOBase::shellCopy(sourcePath + "/" + sourceFileName, filePath)) { + // if copy file fails, create the file empty + QFile(filePath).open(QIODevice::WriteOnly); + } + } +} + +MappingType GameGamebryo::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameShortName() + "/" + profileFile, + false}); + } + + return result; +} + +std::unique_ptr<BYTE[]> GameGamebryo::getRegValue(HKEY key, LPCWSTR path, LPCWSTR value, + DWORD flags, LPDWORD type = nullptr) +{ + DWORD size = 0; + HKEY subKey; + LONG res = ::RegOpenKeyExW(key, path, 0, KEY_QUERY_VALUE | KEY_WOW64_32KEY, &subKey); + if (res != ERROR_SUCCESS) { + res = ::RegOpenKeyExW(key, path, 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &subKey); + if (res != ERROR_SUCCESS) + return std::unique_ptr<BYTE[]>(); + } + res = ::RegGetValueW(subKey, L"", value, flags, type, nullptr, &size); + if (res == ERROR_FILE_NOT_FOUND || res == ERROR_UNSUPPORTED_TYPE) { + return std::unique_ptr<BYTE[]>(); + } + if (res != ERROR_SUCCESS && res != ERROR_MORE_DATA) { + throw MOBase::MyException( + QObject::tr("failed to query registry path (preflight): %1").arg(res, 0, 16)); + } + + std::unique_ptr<BYTE[]> result(new BYTE[size]); + res = ::RegGetValueW(subKey, L"", value, flags, type, result.get(), &size); + + if (res != ERROR_SUCCESS) { + throw MOBase::MyException( + QObject::tr("failed to query registry path (read): %1").arg(res, 0, 16)); + } + + return result; +} + +QString GameGamebryo::findInRegistry(HKEY baseKey, LPCWSTR path, LPCWSTR value) +{ + std::unique_ptr<BYTE[]> buffer = + getRegValue(baseKey, path, value, RRF_RT_REG_SZ | RRF_NOEXPAND); + + return QString::fromUtf16(reinterpret_cast<const ushort*>(buffer.get())); +} + +QString GameGamebryo::getKnownFolderPath(REFKNOWNFOLDERID folderId, bool useDefault) +{ + PWSTR path = nullptr; + ON_BLOCK_EXIT([&]() { + if (path != nullptr) + ::CoTaskMemFree(path); + }); + + if (::SHGetKnownFolderPath(folderId, useDefault ? KF_FLAG_DEFAULT_PATH : 0, NULL, + &path) == S_OK) { + return QDir::fromNativeSeparators(QString::fromWCharArray(path)); + } else { + return QString(); + } +} + +QString GameGamebryo::getSpecialPath(const QString& name) +{ + QString base = findInRegistry( + HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders", + name.toStdWString().c_str()); + + WCHAR temp[MAX_PATH]; + if (::ExpandEnvironmentStringsW(base.toStdWString().c_str(), temp, MAX_PATH) != 0) { + return QString::fromWCharArray(temp); + } else { + return base; + } +} + +QString GameGamebryo::determineMyGamesPath(const QString& gameName) +{ + const QString pattern = "%1/My Games/" + gameName; + + auto tryDir = [&](const QString& dir) -> std::optional<QString> { + if (dir.isEmpty()) { + return {}; + } + + const auto path = pattern.arg(dir); + if (!QFileInfo(path).exists()) { + return {}; + } + + return path; + }; + + // a) this is the way it should work. get the configured My Documents directory + if (auto d = tryDir(getKnownFolderPath(FOLDERID_Documents, false))) { + return *d; + } + + // b) if there is no <game> directory there, look in the default directory + if (auto d = tryDir(getKnownFolderPath(FOLDERID_Documents, true))) { + return *d; + } + + // c) finally, look in the registry. This is discouraged + if (auto d = tryDir(getSpecialPath("Personal"))) { + return *d; + } + + return {}; +} + +QString GameGamebryo::parseEpicGamesLocation(const QStringList& manifests) +{ + // Use the registry entry to find the EGL Data dir first, just in case something + // changes + QString manifestDir = findInRegistry( + HKEY_LOCAL_MACHINE, L"Software\\Epic Games\\EpicGamesLauncher", L"AppDataPath"); + if (manifestDir.isEmpty()) + manifestDir = getKnownFolderPath(FOLDERID_ProgramData, false) + + "\\Epic\\EpicGamesLauncher\\Data\\"; + manifestDir += "Manifests"; + QDir epicManifests(manifestDir, "*.item", + QDir::SortFlags(QDir::Name | QDir::IgnoreCase), QDir::Files); + if (epicManifests.exists()) { + QDirIterator it(epicManifests); + while (it.hasNext()) { + QString manifestFile = it.next(); + QFile manifest(manifestFile); + + if (!manifest.open(QIODevice::ReadOnly)) { + qWarning("Couldn't open Epic Games manifest file."); + continue; + } + + QByteArray manifestData = manifest.readAll(); + + QJsonDocument manifestJson(QJsonDocument::fromJson(manifestData)); + + if (manifests.contains(manifestJson["AppName"].toString())) { + return manifestJson["InstallLocation"].toString(); + } + } + } + return ""; +} + +QString GameGamebryo::parseSteamLocation(const QString& appid, + const QString& directoryName) +{ + QString path = "Software\\Valve\\Steam"; + QString steamLocation = + findInRegistry(HKEY_CURRENT_USER, path.toStdWString().c_str(), L"SteamPath"); + if (!steamLocation.isEmpty()) { + QString steamLibraryLocation; + QString steamLibraries(steamLocation + "\\" + "config" + "\\" + + "libraryfolders.vdf"); + if (QFile(steamLibraries).exists()) { + std::ifstream file(steamLibraries.toStdString()); + auto root = tyti::vdf::read(file); + for (auto child : root.childs) { + tyti::vdf::object* library = child.second.get(); + auto apps = library->childs["apps"]; + if (apps->attribs.contains(appid.toStdString())) { + steamLibraryLocation = QString::fromStdString(library->attribs["path"]); + break; + } + } + } + if (!steamLibraryLocation.isEmpty()) { + QString gameLocation = steamLibraryLocation + "\\" + "steamapps" + "\\" + + "common" + "\\" + directoryName; + if (QDir(gameLocation).exists()) + return gameLocation; + } + } + return ""; +} + +void GameGamebryo::registerFeature(std::shared_ptr<MOBase::GameFeature> feature) +{ + // priority does not matter, this is a game plugin so will get lowest priority in MO2 + m_Organizer->gameFeatures()->registerFeature(this, feature, 0, true); +} diff --git a/libs/game_gamebryo/src/gamebryo/gamegamebryo.h b/libs/game_gamebryo/src/gamebryo/gamegamebryo.h new file mode 100644 index 0000000..2ff07db --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/gamegamebryo.h @@ -0,0 +1,151 @@ +#ifndef GAMEGAMEBRYO_H +#define GAMEGAMEBRYO_H + +#include "iplugingame.h" + +class BSAInvalidation; +class DataArchives; +class LocalSavegames; +class SaveGameInfo; +class BSAInvalidation; +class LocalSavegames; +class ScriptExtender; +class GamePlugins; +class UnmanagedMods; + +#include <QObject> +#include <QString> +#include <ShlObj.h> +#include <dbghelp.h> +#include <ipluginfilemapper.h> +#include <iplugingame.h> +#include <memory> + +#include "gamebryosavegame.h" +#include "igamefeatures.h" + +class GameGamebryo : public MOBase::IPluginGame, public MOBase::IPluginFileMapper +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame MOBase::IPluginFileMapper) + + friend class GamebryoScriptExtender; + friend class GamebryoSaveGameInfo; + friend class GamebryoSaveGameInfoWidget; + friend class GamebryoSaveGame; + + /** + * Some Bethesda games do not have a valid file version but a valid product + * version. If the file version starts with FALLBACK_GAME_VERSION, the product + * version will be tried. + */ + static constexpr const char* FALLBACK_GAME_VERSION = "1.0.0"; + +public: + GameGamebryo(); + + void detectGame() override; + bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + // getName + // initializeProfile + virtual std::vector<std::shared_ptr<const MOBase::ISaveGame>> + listSaves(QDir folder) const override; + + virtual bool isInstalled() const override; + virtual QIcon gameIcon() const override; + virtual QDir gameDirectory() const override; + virtual QDir dataDirectory() const override; + // secondaryDataDirectories + virtual void setGamePath(const QString& path) override; + virtual QDir documentsDirectory() const override; + virtual QDir savesDirectory() const override; + // executables + // steamAPPId + // primaryPlugins + // enabledPlugins + // gameVariants + virtual void setGameVariant(const QString& variant) override; + virtual QString binaryName() const override; + // gameShortName + // primarySources + // validShortNames + // iniFiles + // DLCPlugins + // CCPlugins + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual SortMechanism sortMechanism() const override; + // nexusModOrganizerID + // nexusGameID + virtual bool looksValid(QDir const&) const override; + virtual QString gameVersion() const override; + virtual QString getLauncherName() const override; + +public: // IPluginFileMapper interface + virtual MappingType mappings() const; + +public: // Other (e.g. for game features) + QString myGamesPath() const; + +protected: + // Retrieve the saves extension for the game. + virtual QString savegameExtension() const = 0; + virtual QString savegameSEExtension() const = 0; + + // Create a save game. + virtual std::shared_ptr<const GamebryoSaveGame> + makeSaveGame(QString filepath) const = 0; + + QFileInfo findInGameFolder(const QString& relativePath) const; + QString selectedVariant() const; + WORD getArch(QString const& program) const; + + static QString localAppFolder(); + // Arguably this shouldn't really be here but every gamebryo program seems to + // use it + static QString getLootPath(); + + // This function is not terribly well named as it copies exactly where it's told + // to, irrespective of whether it's in the profile... + static void copyToProfile(const QString& sourcePath, const QDir& destinationDirectory, + const QString& sourceFileName); + + static void copyToProfile(const QString& sourcePath, const QDir& destinationDirectory, + const QString& sourceFileName, + const QString& destinationFileName); + + virtual QString identifyGamePath() const; + + virtual bool prepareIni(const QString& exec); + + static std::unique_ptr<BYTE[]> getRegValue(HKEY key, LPCWSTR path, LPCWSTR value, + DWORD flags, LPDWORD type); + + static QString findInRegistry(HKEY baseKey, LPCWSTR path, LPCWSTR value); + + static QString getKnownFolderPath(REFKNOWNFOLDERID folderId, bool useDefault); + + static QString getSpecialPath(const QString& name); + + static QString determineMyGamesPath(const QString& gameName); + + static QString parseEpicGamesLocation(const QStringList& manifests); + + static QString parseSteamLocation(const QString& appid, const QString& directoryName); + +protected: + void registerFeature(std::shared_ptr<MOBase::GameFeature> feature); + +protected: + // to access organizer for game features, avoid having to pass it to all saves since + // we already pass the game + friend class GamebryoSaveGame; + + QString m_GamePath; + QString m_MyGamesPath; + QString m_GameVariant; + MOBase::IOrganizer* m_Organizer; +}; + +#endif // GAMEGAMEBRYO_H diff --git a/libs/game_gamebryo/src/gamebryo/vdf_parser.h b/libs/game_gamebryo/src/gamebryo/vdf_parser.h new file mode 100644 index 0000000..32d4d27 --- /dev/null +++ b/libs/game_gamebryo/src/gamebryo/vdf_parser.h @@ -0,0 +1,739 @@ +// MIT License +// +// Copyright(c) 2016 Matthias Moeller +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef __TYTI_STEAM_VDF_PARSER_H__ +#define __TYTI_STEAM_VDF_PARSER_H__ + +#include <algorithm> +#include <fstream> +#include <functional> +#include <iterator> +#include <map> +#include <memory> +#include <unordered_map> +#include <unordered_set> +#include <utility> +#include <vector> + +#include <exception> +#include <system_error> + +// for wstring support +#include <locale> +#include <string> + +// internal +#include <stack> + +// VS < 2015 has only partial C++11 support +#if defined(_MSC_VER) && _MSC_VER < 1900 +#ifndef CONSTEXPR +#define CONSTEXPR +#endif + +#ifndef NOEXCEPT +#define NOEXCEPT +#endif +#else +#ifndef CONSTEXPR +#define CONSTEXPR constexpr +#define TYTI_UNDEF_CONSTEXPR +#endif + +#ifndef NOEXCEPT +#define NOEXCEPT noexcept +#define TYTI_UNDEF_NOEXCEPT +#endif + +#endif + +namespace tyti +{ +namespace vdf +{ + namespace detail + { + /////////////////////////////////////////////////////////////////////////// + // Helper functions selecting the right encoding (char/wchar_T) + /////////////////////////////////////////////////////////////////////////// + + template <typename T> + struct literal_macro_help + { + static CONSTEXPR const char* result(const char* c, const wchar_t*) NOEXCEPT + { + return c; + } + static CONSTEXPR const char result(const char c, const wchar_t) NOEXCEPT + { + return c; + } + }; + + template <> + struct literal_macro_help<wchar_t> + { + static CONSTEXPR const wchar_t* result(const char*, const wchar_t* wc) NOEXCEPT + { + return wc; + } + static CONSTEXPR const wchar_t result(const char, const wchar_t wc) NOEXCEPT + { + return wc; + } + }; +#define TYTI_L(type, text) vdf::detail::literal_macro_help<type>::result(text, L##text) + + inline std::string string_converter(const std::string& w) NOEXCEPT + { + return w; + } + + // utility wrapper to adapt locale-bound facets for wstring/wbuffer convert + // from cppreference + template <class Facet> + struct deletable_facet : Facet + { + template <class... Args> + deletable_facet(Args&&... args) : Facet(std::forward<Args>(args)...) + {} + ~deletable_facet() {} + }; + + inline std::string string_converter(const std::wstring& w) // todo: use us-locale + { + std::wstring_convert<deletable_facet<std::codecvt<wchar_t, char, std::mbstate_t>>> + conv1; + return conv1.to_bytes(w); + } + + /////////////////////////////////////////////////////////////////////////// + // Writer helper functions + /////////////////////////////////////////////////////////////////////////// + + template <typename charT> + class tabs + { + const size_t t; + + public: + explicit CONSTEXPR tabs(size_t i) NOEXCEPT : t(i) {} + std::basic_string<charT> print() const + { + return std::basic_string<charT>(t, TYTI_L(charT, '\t')); + } + inline CONSTEXPR tabs operator+(size_t i) const NOEXCEPT { return tabs(t + i); } + }; + + template <typename oStreamT> + oStreamT& operator<<(oStreamT& s, const tabs<typename oStreamT::char_type> t) + { + s << t.print(); + return s; + } + } // end namespace detail + + /////////////////////////////////////////////////////////////////////////// + // Interface + /////////////////////////////////////////////////////////////////////////// + + /// custom objects and their corresponding write functions + + /// basic object node. Every object has a name and can contains attributes saved as + /// key_value pairs or childrens + template <typename CharT> + struct basic_object + { + typedef CharT char_type; + std::basic_string<char_type> name; + std::unordered_map<std::basic_string<char_type>, std::basic_string<char_type>> + attribs; + std::unordered_map<std::basic_string<char_type>, + std::shared_ptr<basic_object<char_type>>> + childs; + + void add_attribute(std::basic_string<char_type> key, + std::basic_string<char_type> value) + { + attribs.emplace(std::move(key), std::move(value)); + } + void add_child(std::unique_ptr<basic_object<char_type>> child) + { + std::shared_ptr<basic_object<char_type>> obj{child.release()}; + childs.emplace(obj->name, obj); + } + void set_name(std::basic_string<char_type> n) { name = std::move(n); } + }; + + template <typename CharT> + struct basic_multikey_object + { + typedef CharT char_type; + std::basic_string<char_type> name; + std::unordered_multimap<std::basic_string<char_type>, std::basic_string<char_type>> + attribs; + std::unordered_multimap<std::basic_string<char_type>, + std::shared_ptr<basic_multikey_object<char_type>>> + childs; + + void add_attribute(std::basic_string<char_type> key, + std::basic_string<char_type> value) + { + attribs.emplace(std::move(key), std::move(value)); + } + void add_child(std::unique_ptr<basic_multikey_object<char_type>> child) + { + std::shared_ptr<basic_multikey_object<char_type>> obj{child.release()}; + childs.emplace(obj->name, obj); + } + void set_name(std::basic_string<char_type> n) { name = std::move(n); } + }; + + typedef basic_object<char> object; + typedef basic_object<wchar_t> wobject; + typedef basic_multikey_object<char> multikey_object; + typedef basic_multikey_object<wchar_t> wmultikey_object; + + struct Options + { + bool strip_escape_symbols; + bool ignore_all_platform_conditionals; + bool ignore_includes; + + Options() + : strip_escape_symbols(true), ignore_all_platform_conditionals(false), + ignore_includes(false) + {} + }; + + // forward decls + // forward decl + template <typename OutputT, typename iStreamT> + OutputT read(iStreamT& inStream, const Options& opt = Options{}); + + /** \brief writes given object tree in vdf format to given stream. + Output is prettyfied, using tabs + */ + template <typename oStreamT, typename T> + void write(oStreamT& s, const T& r, + const detail::tabs<typename oStreamT::char_type> tab = + detail::tabs<typename oStreamT::char_type>(0)) + { + typedef typename oStreamT::char_type charT; + using namespace detail; + s << tab << TYTI_L(charT, '"') << r.name << TYTI_L(charT, "\"\n") << tab + << TYTI_L(charT, "{\n"); + for (const auto& i : r.attribs) + s << tab + 1 << TYTI_L(charT, '"') << i.first << TYTI_L(charT, "\"\t\t\"") + << i.second << TYTI_L(charT, "\"\n"); + for (const auto& i : r.childs) + if (i.second) + write(s, *i.second, tab + 1); + s << tab << TYTI_L(charT, "}\n"); + } + + namespace detail + { + template <typename iStreamT> + std::basic_string<typename iStreamT::char_type> read_file(iStreamT& inStream) + { + // cache the file + typedef typename iStreamT::char_type charT; + std::basic_string<charT> str; + inStream.seekg(0, std::ios::end); + str.resize(static_cast<size_t>(inStream.tellg())); + if (str.empty()) + return str; + + inStream.seekg(0, std::ios::beg); + inStream.read(&str[0], str.size()); + return str; + } + + /** \brief Read VDF formatted sequences defined by the range [first, last). + If the file is mailformatted, parser will try to read it until it can. + @param first begin iterator + @param end end iterator + @param exclude_files list of files which cant be included anymore. + prevents circular includes + + can thow: + - "std::runtime_error" if a parsing error occured + - "std::bad_alloc" if not enough memory coup be allocated + */ + template <typename OutputT, typename IterT> + std::vector<std::unique_ptr<OutputT>> + read_internal(IterT first, const IterT last, + std::unordered_set<std::basic_string< + typename std::iterator_traits<IterT>::value_type>>& exclude_files, + const Options& opt) + { + static_assert(std::is_default_constructible<OutputT>::value, + "Output Type must be default constructible (provide constructor " + "without arguments)"); + static_assert(std::is_move_constructible<OutputT>::value, + "Output Type must be move constructible"); + + typedef typename std::iterator_traits<IterT>::value_type charT; + + const std::basic_string<charT> comment_end_str = TYTI_L(charT, "*/"); + const std::basic_string<charT> whitespaces = TYTI_L(charT, " \n\v\f\r\t"); + +#ifdef WIN32 + std::function<bool(const std::basic_string<charT>&)> is_platform_str = + [](const std::basic_string<charT>& in) { + return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$WINDOWS"); + }; +#elif __APPLE__ + // WIN32 stands for pc in general + std::function<bool(const std::basic_string<charT>&)> is_platform_str = + [](const std::basic_string<charT>& in) { + return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$POSIX") || + in == TYTI_L(charT, "$OSX"); + }; + +#elif __linux__ + // WIN32 stands for pc in general + std::function<bool(const std::basic_string<charT>&)> is_platform_str = + [](const std::basic_string<charT>& in) { + return in == TYTI_L(charT, "$WIN32") || in == TYTI_L(charT, "$POSIX") || + in == TYTI_L(charT, "$LINUX"); + }; +#else + std::function<bool(const std::basic_string<charT>&)> is_platform_str = + [](const std::basic_string<charT>& in) { + return false; + }; +#endif + + if (opt.ignore_all_platform_conditionals) + is_platform_str = [](const std::basic_string<charT>&) { + return false; + }; + + // function for skipping a comment block + // iter: iterator poition to the position after a '/' + auto skip_comments = [&comment_end_str](IterT iter, const IterT& last) -> IterT { + ++iter; + if (iter != last) { + if (*iter == TYTI_L(charT, '/')) { + // line comment, skip whole line + iter = std::find(iter + 1, last, TYTI_L(charT, '\n')); + } + + if (*iter == '*') { + // block comment, skip until next occurance of "*\" + iter = std::search(iter + 1, last, std::begin(comment_end_str), + std::end(comment_end_str)); + iter += 2; + } + } + return iter; + }; + + auto end_quote = [](IterT iter, const IterT& last) -> IterT { + const auto begin = iter; + auto last_esc = iter; + do { + ++iter; + iter = std::find(iter, last, TYTI_L(charT, '\"')); + if (iter == last) + break; + + last_esc = std::prev(iter); + while (last_esc != begin && *last_esc == '\\') + --last_esc; + } while (!(std::distance(last_esc, iter) % 2)); + if (iter == last) + throw std::runtime_error{"quote was opened but not closed."}; + return iter; + }; + + auto end_word = [&whitespaces](IterT iter, const IterT& last) -> IterT { + const auto begin = iter; + auto last_esc = iter; + do { + ++iter; + iter = std::find_first_of(iter, last, std::begin(whitespaces), + std::end(whitespaces)); + if (iter == last) + break; + + last_esc = std::prev(iter); + while (last_esc != begin && *last_esc == '\\') + --last_esc; + } while (!(std::distance(last_esc, iter) % 2)); + // if (iter == last) + // throw std::runtime_error{ "word wasnt properly ended" }; + return iter; + }; + + auto skip_whitespaces = [&whitespaces](IterT iter, const IterT& last) -> IterT { + iter = std::find_if_not(iter, last, [&whitespaces](charT c) { + // return true if whitespace + return std::any_of(std::begin(whitespaces), std::end(whitespaces), + [c](charT pc) { + return pc == c; + }); + }); + return iter; + }; + + std::function<void(std::basic_string<charT>&)> strip_escape_symbols = + [](std::basic_string<charT>& s) { + auto quote_searcher = [&s](size_t pos) { + return s.find(TYTI_L(charT, "\\\""), pos); + }; + auto p = quote_searcher(0); + while (p != s.npos) { + s.replace(p, 2, TYTI_L(charT, "\"")); + p = quote_searcher(p); + } + auto searcher = [&s](size_t pos) { + return s.find(TYTI_L(charT, "\\\\"), pos); + }; + p = searcher(0); + while (p != s.npos) { + s.replace(p, 2, TYTI_L(charT, "\\")); + p = searcher(p); + } + }; + + if (!opt.strip_escape_symbols) + strip_escape_symbols = [](std::basic_string<charT>&) {}; + + auto conditional_fullfilled = [&skip_whitespaces, + &is_platform_str](IterT& iter, const IterT& last) { + iter = skip_whitespaces(iter, last); + if (*iter == '[') { + ++iter; + const auto end = std::find(iter, last, ']'); + const bool negate = *iter == '!'; + if (negate) + ++iter; + auto conditional = std::basic_string<charT>(iter, end); + + const bool is_platform = is_platform_str(conditional); + iter = end + 1; + + return static_cast<bool>(is_platform ^ negate); + } + return true; + }; + + // read header + // first, quoted name + std::unique_ptr<OutputT> curObj = nullptr; + std::vector<std::unique_ptr<OutputT>> roots; + std::stack<std::unique_ptr<OutputT>> lvls; + auto curIter = first; + + while (curIter != last && *curIter != '\0') { + // find first starting attrib/child, or ending + curIter = skip_whitespaces(curIter, last); + if (curIter == last || *curIter == '\0') + break; + if (*curIter == TYTI_L(charT, '/')) { + curIter = skip_comments(curIter, last); + } else if (*curIter != TYTI_L(charT, '}')) { + + // get key + const auto keyEnd = (*curIter == TYTI_L(charT, '\"')) + ? end_quote(curIter, last) + : end_word(curIter, last); + if (*curIter == TYTI_L(charT, '\"')) + ++curIter; + std::basic_string<charT> key(curIter, keyEnd); + strip_escape_symbols(key); + curIter = keyEnd + ((*keyEnd == TYTI_L(charT, '\"')) ? 1 : 0); + + curIter = skip_whitespaces(curIter, last); + + auto conditional = conditional_fullfilled(curIter, last); + if (!conditional) + continue; + + while (*curIter == TYTI_L(charT, '/')) { + + curIter = skip_comments(curIter, last); + if (curIter == last || *curIter == '}') + throw std::runtime_error{"key declared, but no value"}; + curIter = skip_whitespaces(curIter, last); + if (curIter == last || *curIter == '}') + throw std::runtime_error{"key declared, but no value"}; + } + // get value + if (*curIter != '{') { + const auto valueEnd = (*curIter == TYTI_L(charT, '\"')) + ? end_quote(curIter, last) + : end_word(curIter, last); + if (*curIter == TYTI_L(charT, '\"')) + ++curIter; + + auto value = std::basic_string<charT>(curIter, valueEnd); + strip_escape_symbols(value); + curIter = valueEnd + ((*valueEnd == TYTI_L(charT, '\"')) ? 1 : 0); + + auto conditional = conditional_fullfilled(curIter, last); + if (!conditional) + continue; + + // process value + if (key != TYTI_L(charT, "#include") && key != TYTI_L(charT, "#base")) { + if (curObj) { + curObj->add_attribute(std::move(key), std::move(value)); + } else { + throw std::runtime_error{"unexpected key without object"}; + } + } else { + if (!opt.ignore_includes && + exclude_files.find(value) == exclude_files.end()) { + exclude_files.insert(value); + std::basic_ifstream<charT> i(detail::string_converter(value)); + auto str = read_file(i); + auto file_objs = + read_internal<OutputT>(str.begin(), str.end(), exclude_files, opt); + for (auto& n : file_objs) { + if (curObj) + curObj->add_child(std::move(n)); + else + roots.push_back(std::move(n)); + } + exclude_files.erase(value); + } + } + } else if (*curIter == '{') { + if (curObj) + lvls.push(std::move(curObj)); + curObj = std::make_unique<OutputT>(); + curObj->set_name(std::move(key)); + ++curIter; + } + } + // end of new object + else if (curObj && *curIter == TYTI_L(charT, '}')) { + if (!lvls.empty()) { + // get object before + std::unique_ptr<OutputT> prev{std::move(lvls.top())}; + lvls.pop(); + + // add finished obj to obj before and release it from processing + prev->add_child(std::move(curObj)); + curObj = std::move(prev); + } else { + roots.push_back(std::move(curObj)); + curObj.reset(); + } + ++curIter; + } else { + throw std::runtime_error{"unexpected '}'"}; + } + } + if (curObj != nullptr || !lvls.empty()) { + throw std::runtime_error{"object is not closed with '}'"}; + } + + return roots; + } + + } // namespace detail + + /** \brief Read VDF formatted sequences defined by the range [first, last). + If the file is mailformatted, parser will try to read it until it can. + @param first begin iterator + @param end end iterator + + can thow: + - "std::runtime_error" if a parsing error occured + - "std::bad_alloc" if not enough memory coup be allocated + */ + template <typename OutputT, typename IterT> + OutputT read(IterT first, const IterT last, const Options& opt = Options{}) + { + auto exclude_files = std::unordered_set< + std::basic_string<typename std::iterator_traits<IterT>::value_type>>{}; + auto roots = detail::read_internal<OutputT>(first, last, exclude_files, opt); + + OutputT result; + if (roots.size() > 1) { + for (auto& i : roots) + result.add_child(std::move(i)); + } else if (roots.size() == 1) + result = std::move(*roots[0]); + + return result; + } + + /** \brief Read VDF formatted sequences defined by the range [first, last). + If the file is mailformatted, parser will try to read it until it can. + @param first begin iterator + @param end end iterator + @param ec output bool. 0 if ok, otherwise, holds an system error code + + Possible error codes: + std::errc::protocol_error: file is mailformatted + std::errc::not_enough_memory: not enough space + std::errc::invalid_argument: iterators throws e.g. out of range + */ + template <typename OutputT, typename IterT> + OutputT read(IterT first, IterT last, std::error_code& ec, + const Options& opt = Options{}) NOEXCEPT + + { + ec.clear(); + OutputT r{}; + try { + r = read<OutputT>(first, last, opt); + } catch (std::runtime_error&) { + ec = std::make_error_code(std::errc::protocol_error); + } catch (std::bad_alloc&) { + ec = std::make_error_code(std::errc::not_enough_memory); + } catch (...) { + ec = std::make_error_code(std::errc::invalid_argument); + } + return r; + } + + /** \brief Read VDF formatted sequences defined by the range [first, last). + If the file is mailformatted, parser will try to read it until it can. + @param first begin iterator + @param end end iterator + @param ok output bool. true, if parser successed, false, if parser failed + */ + template <typename OutputT, typename IterT> + OutputT read(IterT first, const IterT last, bool* ok, + const Options& opt = Options{}) NOEXCEPT + { + std::error_code ec; + auto r = read<OutputT>(first, last, ec, opt); + if (ok) + *ok = !ec; + return r; + } + + template <typename IterT> + inline auto read(IterT first, const IterT last, bool* ok, + const Options& opt = Options{}) NOEXCEPT + ->basic_object<typename std::iterator_traits<IterT>::value_type> + { + return read<basic_object<typename std::iterator_traits<IterT>::value_type>>( + first, last, ok, opt); + } + + template <typename IterT> + inline auto read(IterT first, IterT last, std::error_code& ec, + const Options& opt = Options{}) NOEXCEPT + ->basic_object<typename std::iterator_traits<IterT>::value_type> + { + return read<basic_object<typename std::iterator_traits<IterT>::value_type>>( + first, last, ec, opt); + } + + template <typename IterT> + inline auto read(IterT first, const IterT last, const Options& opt = Options{}) + -> basic_object<typename std::iterator_traits<IterT>::value_type> + { + return read<basic_object<typename std::iterator_traits<IterT>::value_type>>( + first, last, opt); + } + + /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf + formatted data. throws "std::bad_alloc" if file buffer could not be allocated + */ + template <typename OutputT, typename iStreamT> + OutputT read(iStreamT& inStream, std::error_code& ec, const Options& opt = Options{}) + { + // cache the file + typedef typename iStreamT::char_type charT; + std::basic_string<charT> str = detail::read_file(inStream); + + // parse it + return read<OutputT>(str.begin(), str.end(), ec, opt); + } + + template <typename iStreamT> + inline basic_object<typename iStreamT::char_type> + read(iStreamT& inStream, std::error_code& ec, const Options& opt = Options{}) + { + return read<basic_object<typename iStreamT::char_type>>(inStream, ec, opt); + } + + /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf + formatted data. throws "std::bad_alloc" if file buffer could not be allocated ok == + false, if a parsing error occured + */ + template <typename OutputT, typename iStreamT> + OutputT read(iStreamT& inStream, bool* ok, const Options& opt = Options{}) + { + std::error_code ec; + const auto r = read<OutputT>(inStream, ec, opt); + if (ok) + *ok = !ec; + return r; + } + + template <typename iStreamT> + inline basic_object<typename iStreamT::char_type> read(iStreamT& inStream, bool* ok, + const Options& opt = Options{}) + { + return read<basic_object<typename iStreamT::char_type>>(inStream, ok, opt); + } + + /** \brief Loads a stream (e.g. filestream) into the memory and parses the vdf + formatted data. throws "std::bad_alloc" if file buffer could not be allocated + throws "std::runtime_error" if a parsing error occured + */ + template <typename OutputT, typename iStreamT> + OutputT read(iStreamT& inStream, const Options& opt) + { + + // cache the file + typedef typename iStreamT::char_type charT; + std::basic_string<charT> str = detail::read_file(inStream); + // parse it + return read<OutputT>(str.begin(), str.end(), opt); + } + + template <typename iStreamT> + inline basic_object<typename iStreamT::char_type> read(iStreamT& inStream, + const Options& opt = Options{}) + { + return read<basic_object<typename iStreamT::char_type>>(inStream, opt); + } + +} // namespace vdf +} // namespace tyti +#ifndef TYTI_NO_L_UNDEF +#undef TYTI_L +#endif + +#ifdef TYTI_UNDEF_CONSTEXPR +#undef CONSTEXPR +#undef TYTI_NO_L_UNDEF +#endif + +#ifdef TYTI_UNDEF_NOTHROW +#undef NOTHROW +#undef TYTI_UNDEF_NOTHROW +#endif + +#endif //__TYTI_STEAM_VDF_PARSER_H__ |
