aboutsummaryrefslogtreecommitdiff
path: root/libs/game_bethesda/src/games/skyrim
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_bethesda/src/games/skyrim
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_bethesda/src/games/skyrim')
-rw-r--r--libs/game_bethesda/src/games/skyrim/CMakeLists.txt22
-rw-r--r--libs/game_bethesda/src/games/skyrim/game_skyrim_en.ts17
-rw-r--r--libs/game_bethesda/src/games/skyrim/gameskyrim.cpp263
-rw-r--r--libs/game_bethesda/src/games/skyrim/gameskyrim.h55
-rw-r--r--libs/game_bethesda/src/games/skyrim/gameskyrim.json1
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp16
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.h20
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp42
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimdataarchives.h23
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimgameplugins.cpp123
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimgameplugins.h23
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimmoddatachecker.h50
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimmoddatacontent.h14
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimsavegame.cpp105
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimsavegame.h28
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimscriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/skyrim/skyrimscriptextender.h17
17 files changed, 837 insertions, 0 deletions
diff --git a/libs/game_bethesda/src/games/skyrim/CMakeLists.txt b/libs/game_bethesda/src/games/skyrim/CMakeLists.txt
new file mode 100644
index 0000000..1fba8d5
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/CMakeLists.txt
@@ -0,0 +1,22 @@
+cmake_minimum_required(VERSION 3.16)
+
+add_library(game_skyrim SHARED
+ gameskyrim.cpp
+ gameskyrim.h
+ skyrimbsainvalidation.cpp
+ skyrimbsainvalidation.h
+ skyrimdataarchives.cpp
+ skyrimdataarchives.h
+ skyrimgameplugins.cpp
+ skyrimgameplugins.h
+ skyrimmoddatachecker.h
+ skyrimmoddatacontent.h
+ skyrimsavegame.cpp
+ skyrimsavegame.h
+ skyrimscriptextender.cpp
+ skyrimscriptextender.h
+)
+mo2_configure_plugin(game_skyrim NO_SOURCES WARNINGS 4)
+mo2_default_source_group()
+target_link_libraries(game_skyrim PRIVATE game_gamebryo)
+mo2_install_plugin(game_skyrim)
diff --git a/libs/game_bethesda/src/games/skyrim/game_skyrim_en.ts b/libs/game_bethesda/src/games/skyrim/game_skyrim_en.ts
new file mode 100644
index 0000000..1ba702c
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/game_skyrim_en.ts
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameSkyrim</name>
+ <message>
+ <location filename="gameskyrim.cpp" line="92"/>
+ <source>Skyrim Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gameskyrim.cpp" line="102"/>
+ <source>Adds support for the game Skyrim</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/libs/game_bethesda/src/games/skyrim/gameskyrim.cpp b/libs/game_bethesda/src/games/skyrim/gameskyrim.cpp
new file mode 100644
index 0000000..4a0053e
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/gameskyrim.cpp
@@ -0,0 +1,263 @@
+#include "gameskyrim.h"
+
+#include "skyrimbsainvalidation.h"
+#include "skyrimdataarchives.h"
+#include "skyrimgameplugins.h"
+#include "skyrimmoddatachecker.h"
+#include "skyrimmoddatacontent.h"
+#include "skyrimsavegame.h"
+#include "skyrimscriptextender.h"
+
+#include "executableinfo.h"
+#include "pluginsetting.h"
+
+#include <gamebryogameplugins.h>
+#include <gamebryolocalsavegames.h>
+#include <gamebryosavegameinfo.h>
+#include <gamebryounmanagedmods.h>
+
+#include <QCoreApplication>
+#include <QDebug>
+#include <QFileInfo>
+
+#include <QtDebug>
+
+#ifdef _WIN32
+#include <Windows.h>
+#include <winver.h>
+#endif
+
+#include <exception>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+using namespace MOBase;
+
+GameSkyrim::GameSkyrim() {}
+
+bool GameSkyrim::init(IOrganizer* moInfo)
+{
+ if (!GameGamebryo::init(moInfo)) {
+ return false;
+ }
+
+ auto dataArchives = std::make_shared<SkyrimDataArchives>(this);
+ registerFeature(std::make_shared<SkyrimScriptExtender>(this));
+ registerFeature(dataArchives);
+ registerFeature(std::make_shared<SkyrimBSAInvalidation>(dataArchives.get(), this));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "skyrim.ini"));
+ registerFeature(std::make_shared<SkyrimModDataChecker>(this));
+ registerFeature(std::make_shared<SkyrimModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<SkyrimGamePlugins>(moInfo));
+ registerFeature(std::make_shared<GamebryoUnmangedMods>(this));
+
+ return true;
+}
+
+QString GameSkyrim::gameName() const
+{
+ return "Skyrim";
+}
+
+QList<ExecutableInfo> GameSkyrim::executables() const
+{
+ return QList<ExecutableInfo>()
+ << ExecutableInfo("SKSE",
+ findInGameFolder(m_Organizer->gameFeatures()
+ ->gameFeature<MOBase::ScriptExtender>()
+ ->loaderName()))
+ << ExecutableInfo("SBW", findInGameFolder("SBW.exe"))
+ << ExecutableInfo("Skyrim", findInGameFolder(binaryName()))
+ << ExecutableInfo("Skyrim Launcher", findInGameFolder(getLauncherName()))
+ << ExecutableInfo("BOSS", findInGameFolder("BOSS/BOSS.exe"))
+ << ExecutableInfo("LOOT", QFileInfo(getLootPath()))
+ .withArgument("--game=\"Skyrim\"")
+ << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe"))
+ .withSteamAppId("202480");
+}
+
+QList<ExecutableForcedLoadSetting> GameSkyrim::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+QString GameSkyrim::name() const
+{
+ return "Skyrim Support Plugin";
+}
+
+QString GameSkyrim::localizedName() const
+{
+ return tr("Skyrim Support Plugin");
+}
+
+QString GameSkyrim::author() const
+{
+ return "Tannin & MO2 Team";
+}
+
+QString GameSkyrim::description() const
+{
+ return tr("Adds support for the game Skyrim");
+}
+
+MOBase::VersionInfo GameSkyrim::version() const
+{
+ return VersionInfo(1, 6, 1, VersionInfo::RELEASE_FINAL);
+}
+
+QList<PluginSetting> GameSkyrim::settings() const
+{
+ QList<PluginSetting> results;
+ results.push_back(
+ PluginSetting("sse_downloads", "allow Skyrim SE downloads", QVariant(false)));
+ return results;
+}
+
+void GameSkyrim::initializeProfile(const QDir& path, ProfileSettings settings) const
+{
+ if (settings.testFlag(IPluginGame::MODS)) {
+ copyToProfile(localAppFolder() + "/Skyrim", path, "plugins.txt");
+ }
+
+ if (settings.testFlag(IPluginGame::CONFIGURATION)) {
+ if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) ||
+ !QFileInfo(myGamesPath() + "/skyrim.ini").exists()) {
+ copyToProfile(gameDirectory().absolutePath(), path, "skyrim_default.ini",
+ "skyrim.ini");
+ } else {
+ copyToProfile(myGamesPath(), path, "skyrim.ini");
+ }
+
+ copyToProfile(myGamesPath(), path, "skyrimprefs.ini");
+ }
+}
+
+QString GameSkyrim::savegameExtension() const
+{
+ return "ess";
+}
+
+QString GameSkyrim::savegameSEExtension() const
+{
+ return "skse";
+}
+
+std::shared_ptr<const GamebryoSaveGame> GameSkyrim::makeSaveGame(QString filepath) const
+{
+ return std::make_shared<const SkyrimSaveGame>(filepath, this);
+}
+
+QString GameSkyrim::steamAPPId() const
+{
+ return "72850";
+}
+
+QStringList GameSkyrim::primaryPlugins() const
+{
+ return {"skyrim.esm", "update.esm"};
+}
+
+QString GameSkyrim::binaryName() const
+{
+ return "TESV.exe";
+}
+
+QString GameSkyrim::gameShortName() const
+{
+ return "Skyrim";
+}
+
+QString GameSkyrim::gameNexusName() const
+{
+ return "skyrim";
+}
+
+QStringList GameSkyrim::validShortNames() const
+{
+ QStringList results;
+ if (m_Organizer->pluginSetting(name(), "sse_downloads").toBool()) {
+ results.push_back("SkyrimSE");
+ }
+ return results;
+}
+
+QStringList GameSkyrim::iniFiles() const
+{
+ return {"skyrim.ini", "skyrimprefs.ini"};
+}
+
+QStringList GameSkyrim::DLCPlugins() const
+{
+ return {"Dawnguard.esm",
+ "Dragonborn.esm",
+ "HearthFires.esm",
+ "HighResTexturePack01.esp",
+ "HighResTexturePack02.esp",
+ "HighResTexturePack03.esp"};
+}
+
+namespace
+{
+// Note: This is ripped off from shared/util. And in an upcoming move, the fomod
+// installer requires something similar. I suspect I should abstract this out
+// into gamebryo (or lower level)
+
+#ifdef _WIN32
+VS_FIXEDFILEINFO GetFileVersion(const std::wstring& fileName)
+{
+ DWORD handle = 0UL;
+ DWORD size = ::GetFileVersionInfoSizeW(fileName.c_str(), &handle);
+ if (size == 0) {
+ throw std::runtime_error("failed to determine file version info size");
+ }
+
+ std::vector<char> buffer(size);
+ handle = 0UL;
+ if (!::GetFileVersionInfoW(fileName.c_str(), handle, size, buffer.data())) {
+ throw std::runtime_error("failed to determine file version info");
+ }
+
+ void* versionInfoPtr = nullptr;
+ UINT versionInfoLength = 0;
+ if (!::VerQueryValue(buffer.data(), L"\\", &versionInfoPtr, &versionInfoLength)) {
+ throw std::runtime_error("failed to determine file version");
+ }
+
+ return *static_cast<VS_FIXEDFILEINFO*>(versionInfoPtr);
+}
+#endif
+
+} // namespace
+
+IPluginGame::LoadOrderMechanism GameSkyrim::loadOrderMechanism() const
+{
+#ifdef _WIN32
+ try {
+ std::wstring fileName =
+ gameDirectory().absoluteFilePath(binaryName()).toStdWString().c_str();
+ VS_FIXEDFILEINFO versionInfo = ::GetFileVersion(fileName);
+ if ((versionInfo.dwFileVersionMS > 0x10004) || // version >= 1.5.x?
+ ((versionInfo.dwFileVersionMS == 0x10004) &&
+ (versionInfo.dwFileVersionLS >= 0x1A0000))) { // version >= ?.4.26
+ return LoadOrderMechanism::PluginsTxt;
+ }
+ } catch (const std::exception& e) {
+ qCritical() << "TESV.exe is invalid: " << e.what();
+ }
+#endif
+ return LoadOrderMechanism::FileTime;
+}
+
+int GameSkyrim::nexusModOrganizerID() const
+{
+ return 0;
+}
+
+int GameSkyrim::nexusGameID() const
+{
+ return 110;
+}
diff --git a/libs/game_bethesda/src/games/skyrim/gameskyrim.h b/libs/game_bethesda/src/games/skyrim/gameskyrim.h
new file mode 100644
index 0000000..2fbc2c4
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/gameskyrim.h
@@ -0,0 +1,55 @@
+#ifndef GAMESKYRIM_H
+#define GAMESKYRIM_H
+
+#include "gamegamebryo.h"
+
+#include <QObject>
+#include <QtGlobal>
+
+class GameSkyrim : public GameGamebryo
+{
+ Q_OBJECT
+#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
+ Q_PLUGIN_METADATA(IID "org.tannin.GameSkyrim" FILE "gameskyrim.json")
+#endif
+
+public:
+ GameSkyrim();
+
+ virtual bool init(MOBase::IOrganizer* moInfo) override;
+
+public: // IPluginGame interface
+ virtual QString gameName() const override;
+ virtual QList<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ executableForcedLoads() const override;
+ virtual void initializeProfile(const QDir& path,
+ ProfileSettings settings) const override;
+ virtual QString steamAPPId() const override;
+ virtual QStringList primaryPlugins() const override;
+ virtual QString binaryName() const override;
+ virtual QString gameShortName() const override;
+ virtual QString gameNexusName() const override;
+ virtual QStringList validShortNames() const override;
+ virtual QStringList iniFiles() const override;
+ virtual QStringList DLCPlugins() const override;
+ virtual LoadOrderMechanism loadOrderMechanism() const override;
+ virtual int nexusModOrganizerID() const override;
+ virtual int nexusGameID() const override;
+
+public: // IPlugin interface
+ virtual QString name() const override;
+ virtual QString localizedName() const override;
+ virtual QString author() const override;
+ virtual QString description() const override;
+ virtual MOBase::VersionInfo version() const override;
+ virtual QList<MOBase::PluginSetting> settings() const override;
+
+protected:
+ virtual QString savegameExtension() const override;
+ virtual QString savegameSEExtension() const override;
+ virtual std::shared_ptr<const GamebryoSaveGame>
+ makeSaveGame(QString filepath) const override;
+};
+
+#endif // GAMESKYRIM_H
diff --git a/libs/game_bethesda/src/games/skyrim/gameskyrim.json b/libs/game_bethesda/src/games/skyrim/gameskyrim.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/gameskyrim.json
@@ -0,0 +1 @@
+{}
diff --git a/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp b/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp
new file mode 100644
index 0000000..e056f10
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp
@@ -0,0 +1,16 @@
+#include "skyrimbsainvalidation.h"
+
+SkyrimBSAInvalidation::SkyrimBSAInvalidation(MOBase::DataArchives* dataArchives,
+ MOBase::IPluginGame const* game)
+ : GamebryoBSAInvalidation(dataArchives, "skyrim.ini", game)
+{}
+
+QString SkyrimBSAInvalidation::invalidationBSAName() const
+{
+ return "Skyrim - Invalidation.bsa";
+}
+
+unsigned long SkyrimBSAInvalidation::bsaVersion() const
+{
+ return 0x68;
+}
diff --git a/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.h b/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.h
new file mode 100644
index 0000000..4937fe9
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.h
@@ -0,0 +1,20 @@
+#ifndef SKYRIMBSAINVALIDATION_H
+#define SKYRIMBSAINVALIDATION_H
+
+#include "gamebryobsainvalidation.h"
+#include "skyrimdataarchives.h"
+
+#include <memory>
+
+class SkyrimBSAInvalidation : public GamebryoBSAInvalidation
+{
+public:
+ SkyrimBSAInvalidation(MOBase::DataArchives* dataArchives,
+ MOBase::IPluginGame const* game);
+
+private:
+ virtual QString invalidationBSAName() const override;
+ virtual unsigned long bsaVersion() const override;
+};
+
+#endif // SKYRIMBSAINVALIDATION_H
diff --git a/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp b/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp
new file mode 100644
index 0000000..6422175
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp
@@ -0,0 +1,42 @@
+#include "skyrimdataarchives.h"
+#include <utility.h>
+
+QStringList SkyrimDataArchives::vanillaArchives() const
+{
+ return {"Skyrim - Misc.bsa", "Skyrim - Shaders.bsa",
+ "Skyrim - Textures.bsa", "HighResTexturePack01.bsa",
+ "HighResTexturePack02.bsa", "HighResTexturePack03.bsa",
+ "Skyrim - Interface.bsa", "Skyrim - Animations.bsa",
+ "Skyrim - Meshes.bsa", "Skyrim - Sounds.bsa",
+ "Skyrim - Voices.bsa", "Skyrim - VoicesExtra.bsa"};
+}
+
+QStringList SkyrimDataArchives::archives(const MOBase::IProfile* profile) const
+{
+ QStringList result;
+
+ QString iniFile = profile->localSettingsEnabled()
+ ? QDir(profile->absolutePath()).absoluteFilePath("skyrim.ini")
+ : localGameDirectory().absoluteFilePath("skyrim.ini");
+ result.append(getArchivesFromKey(iniFile, "SResourceArchiveList"));
+ result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2"));
+
+ return result;
+}
+
+void SkyrimDataArchives::writeArchiveList(MOBase::IProfile* profile,
+ const QStringList& before)
+{
+ QString list = before.join(", ");
+
+ QString iniFile = profile->localSettingsEnabled()
+ ? QDir(profile->absolutePath()).absoluteFilePath("skyrim.ini")
+ : localGameDirectory().absoluteFilePath("skyrim.ini");
+ if (list.length() > 255) {
+ int splitIdx = list.lastIndexOf(",", 256);
+ setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx));
+ setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2));
+ } else {
+ setArchivesToKey(iniFile, "SResourceArchiveList", list);
+ }
+}
diff --git a/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.h b/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.h
new file mode 100644
index 0000000..b8c2ae0
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.h
@@ -0,0 +1,23 @@
+#ifndef SKYRIMDATAARCHIVES_H
+#define SKYRIMDATAARCHIVES_H
+
+#include <QDir>
+#include <QString>
+#include <QStringList>
+#include <gamebryodataarchives.h>
+#include <iprofile.h>
+
+class SkyrimDataArchives : public GamebryoDataArchives
+{
+public:
+ using GamebryoDataArchives::GamebryoDataArchives;
+
+ virtual QStringList vanillaArchives() const override;
+ virtual QStringList archives(const MOBase::IProfile* profile) const override;
+
+private:
+ virtual void writeArchiveList(MOBase::IProfile* profile,
+ const QStringList& before) override;
+};
+
+#endif // SKYRIMDATAARCHIVES_H
diff --git a/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.cpp b/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.cpp
new file mode 100644
index 0000000..88aac81
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.cpp
@@ -0,0 +1,123 @@
+#include "skyrimgameplugins.h"
+#include <ipluginlist.h>
+#include <report.h>
+#include <safewritefile.h>
+#include <scopeguard.h>
+
+#include <QDir>
+#include <QStringEncoder>
+#include <QStringList>
+
+using MOBase::IOrganizer;
+using MOBase::IPluginGame;
+using MOBase::IPluginList;
+using MOBase::reportError;
+using MOBase::SafeWriteFile;
+
+SkyrimGamePlugins::SkyrimGamePlugins(IOrganizer* organizer)
+ : GamebryoGamePlugins(organizer)
+{}
+
+void SkyrimGamePlugins::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 (pluginsIsNew && !loadOrderIsNew) {
+ // If the plugins is new but not loadorder, we must reparse the load order from the
+ // plugin files
+
+ // removed because returned loadorder was incorrect and did not account for plugins
+ // that were already disabled before.
+ /*QStringList loadOrder = readPluginList(pluginList);
+ pluginList->setLoadOrder(loadOrder);*/
+
+ // Fix me: we are ignoring order changes in plugins.txt favouring loadorder.txt in
+ // all cases (plugins.txt shuld have precedence)
+ QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath);
+ pluginList->setLoadOrder(loadOrder);
+ readPluginList(pluginList);
+ } else {
+ // 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);
+ }
+
+ m_LastRead = QDateTime::currentDateTime();
+}
+
+// TODO: return value is incorrect and should be ignored (it's not currently used
+QStringList SkyrimGamePlugins::readPluginList(MOBase::IPluginList* pluginList)
+{
+ QStringList plugins = pluginList->pluginNames();
+ QStringList primaryPlugins = organizer()->managedGame()->primaryPlugins();
+ QStringList loadOrder(plugins);
+
+ for (const QString& pluginName : primaryPlugins) {
+ if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) {
+ pluginList->setState(pluginName, IPluginList::STATE_ACTIVE);
+ }
+ }
+
+ // Do not sort the primary plugins. Their load order should be locked as defined in
+ // "primaryPlugins".
+ const QStringList pluginsClone(plugins);
+ for (QString plugin : pluginsClone) {
+ if (primaryPlugins.contains(plugin, Qt::CaseInsensitive))
+ plugins.removeAll(plugin);
+ }
+
+ // 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([&]() {
+ qDebug("close %s", qUtf8Printable(filePath));
+ 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;
+ }
+
+ if (pluginsTxtExists) {
+ 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 (pluginName.size() > 0) {
+ pluginList->setState(pluginName, IPluginList::STATE_ACTIVE);
+ plugins.removeAll(pluginName);
+ // we already have the old loadorder and we ignore the positions in plugins.txt
+ // (needs fix) loadOrder.append(pluginName);
+ }
+ }
+
+ file.close();
+
+ // we removed each plugin found in the file, so what's left are inactive mods
+ for (const QString& pluginName : plugins) {
+ pluginList->setState(pluginName, IPluginList::STATE_INACTIVE);
+ }
+ } else {
+ for (const QString& pluginName : plugins) {
+ pluginList->setState(pluginName, IPluginList::STATE_INACTIVE);
+ }
+ }
+
+ return loadOrder;
+}
diff --git a/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.h b/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.h
new file mode 100644
index 0000000..8795c91
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.h
@@ -0,0 +1,23 @@
+#ifndef _SKYRIMGAMEPLUGINS_H
+#define _SKYRIMGAMEPLUGINS_H
+
+#include <gamebryogameplugins.h>
+#include <imoinfo.h>
+#include <iplugingame.h>
+#include <map>
+
+class SkyrimGamePlugins : public GamebryoGamePlugins
+{
+public:
+ SkyrimGamePlugins(MOBase::IOrganizer* organizer);
+
+ virtual void readPluginLists(MOBase::IPluginList* pluginList) override;
+
+protected:
+ virtual QStringList readPluginList(MOBase::IPluginList* pluginList) override;
+
+private:
+ std::map<QString, QByteArray> m_LastSaveHash;
+};
+
+#endif // _SKYRIMSEGAMEPLUGINS_H
diff --git a/libs/game_bethesda/src/games/skyrim/skyrimmoddatachecker.h b/libs/game_bethesda/src/games/skyrim/skyrimmoddatachecker.h
new file mode 100644
index 0000000..4de6d6c
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/skyrimmoddatachecker.h
@@ -0,0 +1,50 @@
+#ifndef SKYRIM_MODATACHECKER_H
+#define SKYRIM_MODATACHECKER_H
+
+#include <gamebryomoddatachecker.h>
+
+class SkyrimModDataChecker : public GamebryoModDataChecker
+{
+public:
+ using GamebryoModDataChecker::GamebryoModDataChecker;
+
+protected:
+ virtual const FileNameSet& possibleFolderNames() const override
+ {
+ static FileNameSet result{"fonts",
+ "interface",
+ "menus",
+ "meshes",
+ "music",
+ "scripts",
+ "shaders",
+ "sound",
+ "strings",
+ "textures",
+ "trees",
+ "video",
+ "facegen",
+ "materials",
+ "skse",
+ "distantlod",
+ "asi",
+ "Tools",
+ "MCM",
+ "distantland",
+ "mits",
+ "dllplugins",
+ "SkyProc Patchers",
+ "CalienteTools",
+ "NetScriptFramework",
+ "shadersfx",
+ "Nemesis_Engine"};
+ return result;
+ }
+ virtual const FileNameSet& possibleFileExtensions() const override
+ {
+ static FileNameSet result{"esp", "esm", "bsa", "modgroups", "ini"};
+ return result;
+ }
+};
+
+#endif // SKYRIM_MODATACHECKER_H
diff --git a/libs/game_bethesda/src/games/skyrim/skyrimmoddatacontent.h b/libs/game_bethesda/src/games/skyrim/skyrimmoddatacontent.h
new file mode 100644
index 0000000..96ec5a6
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/skyrimmoddatacontent.h
@@ -0,0 +1,14 @@
+#ifndef SKYRIM_MODDATACONTENT_H
+#define SKYRIM_MODDATACONTENT_H
+
+#include <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+// Skyrim does not need any change from the default feature:
+class SkyrimModDataContent : public GamebryoModDataContent
+{
+public:
+ using GamebryoModDataContent::GamebryoModDataContent;
+};
+
+#endif // SKYRIM_MODDATACONTENT_H
diff --git a/libs/game_bethesda/src/games/skyrim/skyrimsavegame.cpp b/libs/game_bethesda/src/games/skyrim/skyrimsavegame.cpp
new file mode 100644
index 0000000..bd16036
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/skyrimsavegame.cpp
@@ -0,0 +1,105 @@
+#include "skyrimsavegame.h"
+
+#ifdef _WIN32
+#include <Windows.h>
+#else
+#include <QDateTime>
+
+// SYSTEMTIME is _SYSTEMTIME (defined in gamebryosavegame.h for Linux)
+using SYSTEMTIME = _SYSTEMTIME;
+
+// Portable FileTimeToSystemTime: converts Windows FILETIME to SYSTEMTIME
+static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st)
+{
+ const uint64_t ticks = (static_cast<uint64_t>(ft->dwHighDateTime) << 32) |
+ static_cast<uint64_t>(ft->dwLowDateTime);
+ const int64_t unixSecs = static_cast<int64_t>(ticks / 10000000ULL) - 11644473600LL;
+ const uint64_t remainderHns = ticks % 10000000ULL; // leftover 100ns units
+
+ const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC);
+ const QDate d = dt.date();
+ const QTime t = dt.time();
+
+ st->wYear = static_cast<uint16_t>(d.year());
+ st->wMonth = static_cast<uint16_t>(d.month());
+ st->wDayOfWeek = static_cast<uint16_t>(d.dayOfWeek() % 7); // Qt Mon=1..Sun=7 -> Win Sun=0..Sat=6
+ st->wDay = static_cast<uint16_t>(d.day());
+ st->wHour = static_cast<uint16_t>(t.hour());
+ st->wMinute = static_cast<uint16_t>(t.minute());
+ st->wSecond = static_cast<uint16_t>(t.second());
+ st->wMilliseconds = static_cast<uint16_t>(remainderHns / 10000);
+}
+#endif
+
+#include "gameskyrim.h"
+
+SkyrimSaveGame::SkyrimSaveGame(QString const& fileName, GameSkyrim const* game)
+ : GamebryoSaveGame(fileName, game)
+{
+ FileWrapper file(getFilepath(), "TESV_SAVEGAME");
+
+ FILETIME ftime;
+ fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, ftime);
+
+ // A file time is a 64-bit value that represents the number of 100-nanosecond
+ // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal
+ // Time (UTC). So we need to convert that to something useful
+ SYSTEMTIME ctime;
+ ::FileTimeToSystemTime(&ftime, &ctime);
+ setCreationTime(ctime);
+}
+
+void SkyrimSaveGame::fetchInformationFields(
+ FileWrapper& file, uint32_t& saveNumber, QString& playerName,
+ unsigned short& playerLevel, QString& playerLocation, FILETIME& creationTime) const
+{
+ file.skip<uint32_t>(); // header size
+ file.skip<uint32_t>(); // header version
+ file.read(saveNumber);
+
+ file.read(playerName);
+
+ uint32_t temp;
+ file.read(temp);
+ playerLevel = static_cast<unsigned short>(temp);
+
+ file.setPluginStringFormat(GamebryoSaveGame::StringFormat::LOCAL8BIT);
+ file.read(playerLocation);
+ file.setPluginStringFormat(GamebryoSaveGame::StringFormat::UTF8);
+
+ QString timeOfDay;
+ file.read(timeOfDay);
+
+ QString race;
+ file.read(race); // race name (i.e. BretonRace)
+
+ file.skip<unsigned short>(); // Player gender (0 = male)
+ file.skip<float>(2); // experience gathered, experience required
+
+ file.read(creationTime);
+}
+
+std::unique_ptr<GamebryoSaveGame::DataFields> SkyrimSaveGame::fetchDataFields() const
+{
+ FileWrapper file(getFilepath(), "TESV_SAVEGAME");
+ std::unique_ptr<DataFields> fields = std::make_unique<DataFields>();
+
+ {
+ QString dummyName, dummyLocation;
+ unsigned short dummyLevel;
+ uint32_t dummySaveNumber;
+ FILETIME dummyTime;
+
+ fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation,
+ dummyTime);
+ }
+
+ fields->Screenshot = file.readImage();
+
+ file.skip<unsigned char>(); // form version
+ file.skip<uint32_t>(); // plugin info size
+
+ fields->Plugins = file.readPlugins();
+
+ return fields;
+}
diff --git a/libs/game_bethesda/src/games/skyrim/skyrimsavegame.h b/libs/game_bethesda/src/games/skyrim/skyrimsavegame.h
new file mode 100644
index 0000000..2df96b2
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/skyrimsavegame.h
@@ -0,0 +1,28 @@
+#ifndef SKYRIMSAVEGAME_H
+#define SKYRIMSAVEGAME_H
+
+#include "gamebryosavegame.h"
+#include "windows_compat.h"
+
+namespace MOBase
+{
+class IPluginGame;
+}
+
+class GameSkyrim;
+
+class SkyrimSaveGame : public GamebryoSaveGame
+{
+public:
+ SkyrimSaveGame(QString const& fileName, GameSkyrim const* game);
+
+protected:
+ // Fetch easy-to-access information.
+ void fetchInformationFields(FileWrapper& wrapper, uint32_t& saveNumber,
+ QString& playerName, unsigned short& playerLevel,
+ QString& playerLocation, FILETIME& creationTime) const;
+
+ std::unique_ptr<DataFields> fetchDataFields() const override;
+};
+
+#endif // SKYRIMSAVEGAME_H
diff --git a/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.cpp b/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.cpp
new file mode 100644
index 0000000..38e17ed
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.cpp
@@ -0,0 +1,18 @@
+#include "skyrimscriptextender.h"
+
+#include <QString>
+#include <QStringList>
+
+SkyrimScriptExtender::SkyrimScriptExtender(GameGamebryo const* game)
+ : GamebryoScriptExtender(game)
+{}
+
+QString SkyrimScriptExtender::BinaryName() const
+{
+ return "skse_loader.exe";
+}
+
+QString SkyrimScriptExtender::PluginPath() const
+{
+ return "skse/plugins";
+}
diff --git a/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.h b/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.h
new file mode 100644
index 0000000..6e95249
--- /dev/null
+++ b/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.h
@@ -0,0 +1,17 @@
+#ifndef SKYRIMSCRIPTEXTENDER_H
+#define SKYRIMSCRIPTEXTENDER_H
+
+#include "gamebryoscriptextender.h"
+
+class GameGamebryo;
+
+class SkyrimScriptExtender : public GamebryoScriptExtender
+{
+public:
+ SkyrimScriptExtender(const GameGamebryo* game);
+
+ virtual QString BinaryName() const override;
+ virtual QString PluginPath() const override;
+};
+
+#endif // SKYRIMSCRIPTEXTENDER_H