aboutsummaryrefslogtreecommitdiff
path: root/libs/game_gamebryo/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /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')
-rw-r--r--libs/game_gamebryo/src/creation/CMakeLists.txt10
-rw-r--r--libs/game_gamebryo/src/creation/creationgameplugins.cpp183
-rw-r--r--libs/game_gamebryo/src/creation/creationgameplugins.h22
-rw-r--r--libs/game_gamebryo/src/creation/game_creation_en.ts12
-rw-r--r--libs/game_gamebryo/src/gamebryo/CMakeLists.txt10
-rw-r--r--libs/game_gamebryo/src/gamebryo/dummybsa.cpp192
-rw-r--r--libs/game_gamebryo/src/gamebryo/dummybsa.h59
-rw-r--r--libs/game_gamebryo/src/gamebryo/game_gamebryo_en.ts170
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.cpp133
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryobsainvalidation.h36
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryodataarchives.cpp80
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryodataarchives.h37
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryogameplugins.cpp248
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryogameplugins.h38
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.cpp140
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.h54
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.cpp76
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryomoddatachecker.h46
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.cpp116
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryomoddatacontent.h75
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryosavegame.cpp606
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryosavegame.h240
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.cpp100
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryosavegameinfo.h23
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.cpp199
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.h30
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryosavegameinfowidget.ui209
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.cpp46
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryoscriptextender.h31
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.cpp51
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryounmanagedmods.h26
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamegamebryo.cpp478
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamegamebryo.h151
-rw-r--r--libs/game_gamebryo/src/gamebryo/vdf_parser.h739
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 &apos;%2&apos; 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__