aboutsummaryrefslogtreecommitdiff
path: root/libs/game_bethesda/src/games/enderalse
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/enderalse
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/enderalse')
-rw-r--r--libs/game_bethesda/src/games/enderalse/CMakeLists.txt24
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp64
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsedataarchives.h26
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp82
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsegameplugins.h19
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp24
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.h17
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsemoddatachecker.h31
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsemoddatacontent.h21
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp148
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsesavegame.h28
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalsescriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp29
-rw-r--r--libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.h16
-rw-r--r--libs/game_bethesda/src/games/enderalse/game_enderalse_en.ts25
-rw-r--r--libs/game_bethesda/src/games/enderalse/gameenderalse.cpp347
-rw-r--r--libs/game_bethesda/src/games/enderalse/gameenderalse.h75
-rw-r--r--libs/game_bethesda/src/games/enderalse/gameenderalse.json1
19 files changed, 1012 insertions, 0 deletions
diff --git a/libs/game_bethesda/src/games/enderalse/CMakeLists.txt b/libs/game_bethesda/src/games/enderalse/CMakeLists.txt
new file mode 100644
index 0000000..1e33fbf
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/CMakeLists.txt
@@ -0,0 +1,24 @@
+cmake_minimum_required(VERSION 3.16)
+
+add_library(game_enderalse SHARED
+ enderalsedataarchives.cpp
+ enderalsedataarchives.h
+ enderalsegameplugins.cpp
+ enderalsegameplugins.h
+ enderalselocalsavegames.cpp
+ enderalselocalsavegames.h
+ enderalsemoddatachecker.h
+ enderalsemoddatacontent.h
+ enderalsesavegame.cpp
+ enderalsesavegame.h
+ enderalsescriptextender.cpp
+ enderalsescriptextender.h
+ enderalseunmanagedmods.cpp
+ enderalseunmanagedmods.h
+ gameenderalse.cpp
+ gameenderalse.h
+)
+mo2_configure_plugin(game_enderalse NO_SOURCES WARNINGS 4)
+mo2_default_source_group()
+target_link_libraries(game_enderalse PRIVATE game_creation)
+mo2_install_plugin(game_enderalse)
diff --git a/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp b/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp
new file mode 100644
index 0000000..606c688
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp
@@ -0,0 +1,64 @@
+#include "enderalsedataarchives.h"
+
+#include "iprofile.h"
+#include <utility.h>
+
+QStringList EnderalSEDataArchives::vanillaArchives() const
+{
+ return {"Skyrim - Textures0.bsa",
+ "Skyrim - Textures1.bsa",
+ "Skyrim - Textures2.bsa",
+ "Skyrim - Textures3.bsa",
+ "Skyrim - Textures4.bsa",
+ "Skyrim - Textures5.bsa",
+ "Skyrim - Textures6.bsa",
+ "Skyrim - Textures7.bsa",
+ "Skyrim - Textures8.bsa",
+ "Skyrim - Meshes0.bsa",
+ "Skyrim - Meshes1.bsa",
+ "Skyrim - Voices_en0.bsa",
+ "Skyrim - Sounds.bsa",
+ "Skyrim - Interface.bsa",
+ "Skyrim - Animations.bsa",
+ "Skyrim - Shaders.bsa",
+ "Skyrim - Misc.bsa",
+ "E - Meshes.bsa",
+ "E - SE.bsa",
+ "E - Scripts.bsa",
+ "E - Sounds.bsa",
+ "E - Textures1.bsa",
+ "E - Textures2.bsa",
+ "E - Textures3.bsa",
+ "L - Textures.bsa",
+ "L - Voices.bsa"};
+}
+
+QStringList EnderalSEDataArchives::archives(const MOBase::IProfile* profile) const
+{
+ QStringList result;
+
+ QString iniFile = profile->localSettingsEnabled()
+ ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini")
+ : localGameDirectory().absoluteFilePath("enderal.ini");
+ result.append(getArchivesFromKey(iniFile, "SResourceArchiveList", 512));
+ result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2", 512));
+
+ return result;
+}
+
+void EnderalSEDataArchives::writeArchiveList(MOBase::IProfile* profile,
+ const QStringList& before)
+{
+ QString list = before.join(",");
+
+ QString iniFile = profile->localSettingsEnabled()
+ ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini")
+ : localGameDirectory().absoluteFilePath("enderal.ini");
+ if (list.length() > 511) {
+ int splitIdx = list.lastIndexOf(",", 512);
+ 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/enderalse/enderalsedataarchives.h b/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.h
new file mode 100644
index 0000000..4c51e93
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.h
@@ -0,0 +1,26 @@
+#ifndef ENDERALSEDATAARCHIVES_H
+#define ENDERALSEDATAARCHIVES_H
+
+#include "gamebryodataarchives.h"
+#include <QDir>
+#include <QStringList>
+
+namespace MOBase
+{
+class IProfile;
+}
+
+class EnderalSEDataArchives : 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 // _SKYRIMSEDATAARCHIVES_H
diff --git a/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp b/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp
new file mode 100644
index 0000000..d7be8ce
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp
@@ -0,0 +1,82 @@
+#include "enderalsegameplugins.h"
+
+#include <ipluginlist.h>
+#include <log.h>
+#include <report.h>
+#include <safewritefile.h>
+
+#include <QStringEncoder>
+
+using namespace MOBase;
+
+void EnderalSEGamePlugins::writePluginList(const MOBase::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()));
+
+ // we need to force some plugins because those are not force-loaded
+ // by the game but are considered primary plugins for users
+ file->write("*Enderal - Forgotten Stories.esm\r\n");
+ file->write("*SkyUI_SE.esp\r\n");
+
+ // 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();
+}
diff --git a/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.h b/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.h
new file mode 100644
index 0000000..700a074
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.h
@@ -0,0 +1,19 @@
+#ifndef ENDERALSEGAMEPLUGINS_H
+#define ENDERALSEGAMEPLUGINS_H
+
+#include <creationgameplugins.h>
+#include <imoinfo.h>
+#include <iplugingame.h>
+#include <map>
+
+class EnderalSEGamePlugins : public CreationGamePlugins
+{
+public:
+ using CreationGamePlugins::CreationGamePlugins;
+
+protected:
+ void writePluginList(const MOBase::IPluginList* pluginList,
+ const QString& filePath) override;
+};
+
+#endif // ENDERALSEGAMEPLUGINS_H
diff --git a/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp b/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp
new file mode 100644
index 0000000..fafa739
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp
@@ -0,0 +1,24 @@
+/*
+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 "enderalselocalsavegames.h"
+
+QString EnderalSELocalSavegames::localSavesDummy() const
+{
+ return "..\\Enderal Special Edition\\__MO_Saves\\";
+}
diff --git a/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.h b/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.h
new file mode 100644
index 0000000..f0d14ad
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.h
@@ -0,0 +1,17 @@
+#ifndef ENDERALSELOCALSAVEGAMES_H
+#define ENDERALSELOCALSAVEGAMES_H
+
+#include <gamebryolocalsavegames.h>
+
+#include <QString>
+
+class EnderalSELocalSavegames : public GamebryoLocalSavegames
+{
+public:
+ using GamebryoLocalSavegames::GamebryoLocalSavegames;
+
+protected:
+ QString localSavesDummy() const override;
+};
+
+#endif // ENDERALSELOCALSAVEGAMES_H
diff --git a/libs/game_bethesda/src/games/enderalse/enderalsemoddatachecker.h b/libs/game_bethesda/src/games/enderalse/enderalsemoddatachecker.h
new file mode 100644
index 0000000..2336819
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/enderalsemoddatachecker.h
@@ -0,0 +1,31 @@
+#ifndef ENDERALSE_MODATACHECKER_H
+#define ENDERALSE_MODATACHECKER_H
+
+#include <gamebryomoddatachecker.h>
+
+class EnderalSEModDataChecker : 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", "CalienteTools", "NetScriptFramework",
+ "shadersfx", "Nemesis_Engine"};
+ return result;
+ }
+ virtual const FileNameSet& possibleFileExtensions() const override
+ {
+ static FileNameSet result{"esp", "esm", "esl", "bsa", "modgroups", "ini"};
+ return result;
+ }
+};
+
+#endif // SKYRIMSE_MODATACHECKER_H
diff --git a/libs/game_bethesda/src/games/enderalse/enderalsemoddatacontent.h b/libs/game_bethesda/src/games/enderalse/enderalsemoddatacontent.h
new file mode 100644
index 0000000..a9cce03
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/enderalsemoddatacontent.h
@@ -0,0 +1,21 @@
+#ifndef ENDERALSE_MODDATACONTENT_H
+#define ENDERALSE_MODDATACONTENT_H
+
+#include <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+class EnderalSEModDataContent : public GamebryoModDataContent
+{
+public:
+ /**
+ *
+ */
+ EnderalSEModDataContent(MOBase::IGameFeatures* gameFeatures)
+ : GamebryoModDataContent(gameFeatures)
+ {
+ // Just need to disable some contents:
+ m_Enabled[CONTENT_SKYPROC] = false;
+ }
+};
+
+#endif // SKYRIMSE_MODDATACONTENT_H
diff --git a/libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp b/libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp
new file mode 100644
index 0000000..2662505
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp
@@ -0,0 +1,148 @@
+#include "enderalsesavegame.h"
+
+#ifdef _WIN32
+#include <Windows.h>
+#else
+#include <QDateTime>
+
+union _ULARGE_INTEGER {
+ struct {
+ uint32_t LowPart;
+ uint32_t HighPart;
+ };
+ uint64_t QuadPart;
+};
+
+using SYSTEMTIME = _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;
+
+ 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);
+ 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
+
+EnderalSESaveGame::EnderalSESaveGame(QString const& fileName, GameEnderalSE const* game)
+ : GamebryoSaveGame(fileName, game, true)
+{
+ FileWrapper file(fileName, "TESV_SAVEGAME"); // 10bytes
+
+ uint32_t version;
+ FILETIME ftime;
+ fetchInformationFields(file, version, m_PCName, m_PCLevel, m_PCLocation, m_SaveNumber,
+ 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
+
+ // For some reason, the file time is off by about 6 hours.
+ // So we need to subtract those 6 hours from the filetime.
+ _ULARGE_INTEGER time;
+ time.LowPart = ftime.dwLowDateTime;
+ time.HighPart = ftime.dwHighDateTime;
+ time.QuadPart -= 2.16e11;
+ ftime.dwHighDateTime = time.HighPart;
+ ftime.dwLowDateTime = time.LowPart;
+
+ SYSTEMTIME ctime;
+ ::FileTimeToSystemTime(&ftime, &ctime);
+
+ setCreationTime(ctime);
+}
+
+void EnderalSESaveGame::fetchInformationFields(
+ FileWrapper& file, uint32_t& version, QString& playerName,
+ unsigned short& playerLevel, QString& playerLocation, uint32_t& saveNumber,
+ FILETIME& creationTime) const
+{
+ uint32_t headerSize;
+ file.read(headerSize); // header size "TESV_SAVEGAME"
+ file.read(version);
+ file.read(saveNumber);
+ file.read(playerName);
+
+ uint32_t temp;
+ file.read(temp);
+ playerLevel = static_cast<unsigned short>(temp);
+ file.read(playerLocation);
+
+ 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); // filetime
+}
+
+std::unique_ptr<GamebryoSaveGame::DataFields> EnderalSESaveGame::fetchDataFields() const
+{
+ FileWrapper file(getFilepath(), "TESV_SAVEGAME"); // 10bytes
+
+ uint32_t version = 0;
+ {
+ QString dummyName, dummyLocation;
+ unsigned short dummyLevel;
+ uint32_t dummySaveNumber;
+ FILETIME dummyTime;
+
+ fetchInformationFields(file, version, dummyName, dummyLevel, dummyLocation,
+ dummySaveNumber, dummyTime);
+ }
+
+ std::unique_ptr<DataFields> fields = std::make_unique<DataFields>();
+
+ uint32_t width;
+ uint32_t height;
+ file.read(width);
+ file.read(height);
+
+ bool alpha = false;
+
+ // compatibility between LE and SE:
+ // SE has an additional uin16_t for compression
+ // SE uses an alpha channel, whereas LE does not
+ if (version == 12) {
+ uint16_t compressionType;
+ file.read(compressionType);
+ file.setCompressionType(compressionType);
+ alpha = true;
+ }
+
+ fields->Screenshot = file.readImage(width, height, 320, alpha);
+
+ file.openCompressedData();
+
+ uint8_t saveGameVersion = file.readChar();
+ uint8_t pluginInfoSize = file.readChar();
+ uint16_t other = file.readShort(); // Unknown
+
+ fields->Plugins = file.readPlugins(1); // Just empty data
+
+ if (saveGameVersion >= 78) {
+ fields->LightPlugins = file.readLightPlugins();
+ }
+
+ file.closeCompressedData();
+
+ return fields;
+}
diff --git a/libs/game_bethesda/src/games/enderalse/enderalsesavegame.h b/libs/game_bethesda/src/games/enderalse/enderalsesavegame.h
new file mode 100644
index 0000000..1363142
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/enderalsesavegame.h
@@ -0,0 +1,28 @@
+#ifndef ENDERALSESAVEGAME_H
+#define ENDERALSESAVEGAME_H
+
+#include "gamebryosavegame.h"
+#include "gameenderalse.h"
+#include "windows_compat.h"
+
+namespace MOBase
+{
+class IPluginGame;
+}
+
+class EnderalSESaveGame : public GamebryoSaveGame
+{
+public:
+ EnderalSESaveGame(QString const& fileName, GameEnderalSE const* game);
+
+protected:
+ // Fetch easy-to-access information.
+ void fetchInformationFields(FileWrapper& wrapper, uint32_t& version,
+ QString& playerName, unsigned short& playerLevel,
+ QString& playerLocation, uint32_t& saveNumber,
+ FILETIME& creationTime) const;
+
+ std::unique_ptr<DataFields> fetchDataFields() const override;
+};
+
+#endif // _SKYRIMSESAVEGAME_H
diff --git a/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp b/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp
new file mode 100644
index 0000000..f7a3113
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp
@@ -0,0 +1,18 @@
+#include "enderalsescriptextender.h"
+
+#include <QString>
+#include <QStringList>
+
+EnderalSEScriptExtender::EnderalSEScriptExtender(GameGamebryo const* game)
+ : GamebryoScriptExtender(game)
+{}
+
+QString EnderalSEScriptExtender::BinaryName() const
+{
+ return "skse64_loader.exe";
+}
+
+QString EnderalSEScriptExtender::PluginPath() const
+{
+ return "skse/plugins";
+}
diff --git a/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.h b/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.h
new file mode 100644
index 0000000..79edaf2
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.h
@@ -0,0 +1,17 @@
+#ifndef ENDERALSESCRIPTEXTENDER_H
+#define ENDERALESCRIPTEXTENDER_H
+
+#include "gamebryoscriptextender.h"
+
+class GameGamebryo;
+
+class EnderalSEScriptExtender : public GamebryoScriptExtender
+{
+public:
+ EnderalSEScriptExtender(GameGamebryo const* game);
+
+ virtual QString BinaryName() const override;
+ virtual QString PluginPath() const override;
+};
+
+#endif // _SKYRIMSESCRIPTEXTENDER_H
diff --git a/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp b/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp
new file mode 100644
index 0000000..9f11b44
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp
@@ -0,0 +1,29 @@
+#include "enderalseunmanagedmods.h"
+
+EnderalSEUnmangedMods::EnderalSEUnmangedMods(const GameGamebryo* game)
+ : GamebryoUnmangedMods(game)
+{}
+
+EnderalSEUnmangedMods::~EnderalSEUnmangedMods() {}
+
+QStringList EnderalSEUnmangedMods::mods(bool onlyOfficial) const
+{
+ QStringList result;
+
+ QStringList pluginList = game()->primaryPlugins();
+ QStringList otherPlugins = game()->DLCPlugins();
+ otherPlugins.append(game()->CCPlugins());
+ for (QString plugin : otherPlugins) {
+ pluginList.removeAll(plugin);
+ }
+ QDir dataDir(game()->dataDirectory());
+ for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) {
+ if (!pluginList.contains(fileName, Qt::CaseInsensitive)) {
+ if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) {
+ result.append(fileName.chopped(4)); // trims the extension off
+ }
+ }
+ }
+
+ return result;
+}
diff --git a/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.h b/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.h
new file mode 100644
index 0000000..6eda502
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.h
@@ -0,0 +1,16 @@
+#ifndef ENDERALSEUNMANAGEDMODS_H
+#define ENDERALSEUNMANAGEDMODS_H
+
+#include "gamebryounmanagedmods.h"
+#include <gamegamebryo.h>
+
+class EnderalSEUnmangedMods : public GamebryoUnmangedMods
+{
+public:
+ EnderalSEUnmangedMods(const GameGamebryo* game);
+ ~EnderalSEUnmangedMods();
+
+ virtual QStringList mods(bool onlyOfficial) const override;
+};
+
+#endif // _SKYRIMSEUNMANAGEDMODS_H
diff --git a/libs/game_bethesda/src/games/enderalse/game_enderalse_en.ts b/libs/game_bethesda/src/games/enderalse/game_enderalse_en.ts
new file mode 100644
index 0000000..4c8b5a2
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/game_enderalse_en.ts
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameEnderalSE</name>
+ <message>
+ <location filename="gameenderalse.cpp" line="185"/>
+ <source>Enderal Special Edition Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gameenderalse.cpp" line="195"/>
+ <source>Adds support for the game Enderal Special Edition.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message>
+ <location filename="enderalsegameplugins.cpp" line="75"/>
+ <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_bethesda/src/games/enderalse/gameenderalse.cpp b/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp
new file mode 100644
index 0000000..26587b8
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp
@@ -0,0 +1,347 @@
+#include "gameenderalse.h"
+
+#include "enderalsedataarchives.h"
+#include "enderalsegameplugins.h"
+#include "enderalselocalsavegames.h"
+#include "enderalsemoddatachecker.h"
+#include "enderalsemoddatacontent.h"
+#include "enderalsesavegame.h"
+#include "enderalsescriptextender.h"
+#include "enderalseunmanagedmods.h"
+#include "steamutility.h"
+
+#include "versioninfo.h"
+#include <executableinfo.h>
+#include <gamebryosavegameinfo.h>
+#include <ipluginlist.h>
+#include <pluginsetting.h>
+#include <utility.h>
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QFileInfo>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+#include "scopeguard.h"
+#include <memory>
+
+using namespace MOBase;
+
+GameEnderalSE::GameEnderalSE() {}
+
+void GameEnderalSE::setVariant(QString variant)
+{
+ m_GameVariant = variant;
+}
+
+void GameEnderalSE::checkVariants()
+{
+ QFileInfo gog_dll(m_GamePath + "\\Galaxy64.dll");
+ if (gog_dll.exists())
+ setVariant("GOG");
+ else
+ setVariant("Steam");
+}
+
+QDir GameEnderalSE::documentsDirectory() const
+{
+ return m_MyGamesPath;
+}
+
+void GameEnderalSE::detectGame()
+{
+ m_GamePath = identifyGamePath();
+ checkVariants();
+ m_MyGamesPath = determineMyGamesPath(gameDirectoryName());
+}
+
+QString GameEnderalSE::identifyGamePath() const
+{
+#ifdef _WIN32
+ QMap<QString, QString> paths = {
+ {"Software\\Bethesda Softworks\\" + gameName(), "Installed Path"},
+ {"Software\\GOG.com\\Games\\1708684988", "path"},
+ };
+ QString result;
+ try {
+ for (auto& path : paths.toStdMap()) {
+ result = findInRegistry(HKEY_LOCAL_MACHINE, path.first.toStdWString().c_str(),
+ path.second.toStdWString().c_str());
+ if (!result.isEmpty())
+ break;
+ }
+ } catch (MOBase::MyException) {
+ result = MOBase::findSteamGame("Enderal Special Edition",
+ "Data\\Enderal - Forgotten Stories.esm");
+ }
+ return result;
+#else
+ // Prefer exact Steam app-id resolution for Enderal SE.
+ QString result = parseSteamLocation("976620", "Enderal Special Edition");
+ if (!result.isEmpty() && looksValid(QDir(result))) {
+ return result;
+ }
+
+ result = MOBase::findSteamGame("Enderal Special Edition",
+ "Data/Enderal - Forgotten Stories.esm");
+ if (!result.isEmpty() && looksValid(QDir(result))) {
+ return result;
+ }
+
+ return GameGamebryo::identifyGamePath();
+#endif
+}
+
+void GameEnderalSE::setGamePath(const QString& path)
+{
+ m_GamePath = path;
+ checkVariants();
+ m_MyGamesPath = determineMyGamesPath(gameDirectoryName());
+}
+
+QDir GameEnderalSE::savesDirectory() const
+{
+ return QDir(m_MyGamesPath + "/Saves");
+}
+
+bool GameEnderalSE::isInstalled() const
+{
+ return !m_GamePath.isEmpty();
+}
+
+bool GameEnderalSE::init(IOrganizer* moInfo)
+{
+ if (!GameGamebryo::init(moInfo)) {
+ return false;
+ }
+
+ auto dataArchives = std::make_shared<EnderalSEDataArchives>(this);
+ registerFeature(std::make_shared<EnderalSEScriptExtender>(this));
+ registerFeature(dataArchives);
+ registerFeature(std::make_shared<EnderalSELocalSavegames>(this, "enderal.ini"));
+ registerFeature(std::make_shared<EnderalSEModDataChecker>(this));
+ registerFeature(std::make_shared<EnderalSEModDataContent>(moInfo->gameFeatures()));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<EnderalSEGamePlugins>(moInfo));
+ registerFeature(std::make_shared<EnderalSEUnmangedMods>(this));
+
+ return true;
+}
+
+QString GameEnderalSE::gameName() const
+{
+ return "Enderal Special Edition";
+}
+
+QString GameEnderalSE::gameDirectoryName() const
+{
+ if (selectedVariant() == "GOG")
+ return "Enderal Special Edition GOG";
+ else
+ return "Enderal Special Edition";
+}
+
+QIcon GameEnderalSE::gameIcon() const
+{
+ return MOBase::iconForExecutable(gameDirectory().absoluteFilePath(getLauncherName()));
+}
+
+QList<ExecutableInfo> GameEnderalSE::executables() const
+{
+ return QList<ExecutableInfo>()
+ << ExecutableInfo("Enderal Special Edition (SKSE)",
+ findInGameFolder(m_Organizer->gameFeatures()
+ ->gameFeature<MOBase::ScriptExtender>()
+ ->loaderName()))
+ << ExecutableInfo("Enderal Special Edition Launcher",
+ findInGameFolder(getLauncherName()))
+ << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe"))
+ .withSteamAppId("1946180")
+ << ExecutableInfo("LOOT", QFileInfo(getLootPath()))
+ .withArgument("--game=\"Enderal Special Edition\"");
+}
+
+QList<ExecutableForcedLoadSetting> GameEnderalSE::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+QString GameEnderalSE::binaryName() const
+{
+ return "skse64_loader.exe";
+}
+
+QString GameEnderalSE::getLauncherName() const
+{
+ return "Enderal Launcher.exe";
+}
+
+bool GameEnderalSE::looksValid(const QDir& folder) const
+{
+ // we need to check both launcher and binary because the binary also exists for
+ // Skyrim SE and the launcher for Enderal LE
+ return folder.exists(getLauncherName()) && folder.exists(binaryName());
+}
+
+QFileInfo GameEnderalSE::findInGameFolder(const QString& relativePath) const
+{
+ return QFileInfo(m_GamePath + "/" + relativePath);
+}
+
+QString GameEnderalSE::name() const
+{
+ return "Enderal Special Edition Support Plugin";
+}
+
+QString GameEnderalSE::localizedName() const
+{
+ return tr("Enderal Special Edition Support Plugin");
+}
+
+QString GameEnderalSE::author() const
+{
+ return "Archost, ZachHaber & MO2 Team";
+}
+
+QString GameEnderalSE::description() const
+{
+ return tr("Adds support for the game Enderal Special Edition.");
+}
+
+MOBase::VersionInfo GameEnderalSE::version() const
+{
+ return VersionInfo(1, 2, 0, VersionInfo::RELEASE_FINAL);
+}
+
+QList<PluginSetting> GameEnderalSE::settings() const
+{
+ return QList<PluginSetting>();
+}
+
+void GameEnderalSE::initializeProfile(const QDir& path, ProfileSettings settings) const
+{
+ if (settings.testFlag(IPluginGame::MODS)) {
+ copyToProfile(localAppFolder() + gameDirectoryName(), path, "plugins.txt");
+ }
+
+ if (settings.testFlag(IPluginGame::CONFIGURATION)) {
+ if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) ||
+ !QFileInfo(myGamesPath() + "/Enderal.ini").exists()) {
+
+ // there is no default ini, actually they are going to put them in for us!
+ copyToProfile(gameDirectory().absolutePath(), path, "enderal_default.ini",
+ "Enderal.ini");
+ copyToProfile(gameDirectory().absolutePath(), path, "enderalprefs_default.ini",
+ "EnderalPrefs.ini");
+ } else {
+ copyToProfile(myGamesPath(), path, "Enderal.ini");
+ copyToProfile(myGamesPath(), path, "EnderalPrefs.ini");
+ }
+ }
+}
+
+QString GameEnderalSE::savegameExtension() const
+{
+ return "ess";
+}
+
+QString GameEnderalSE::savegameSEExtension() const
+{
+ return "skse";
+}
+
+std::shared_ptr<const GamebryoSaveGame>
+GameEnderalSE::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const EnderalSESaveGame>(filePath, this);
+}
+
+QString GameEnderalSE::steamAPPId() const
+{
+ if (selectedVariant() == "Steam")
+ return "976620";
+ return "";
+}
+
+QStringList GameEnderalSE::primaryPlugins() const
+{
+ return {"skyrim.esm", "update.esm", "dawnguard.esm", "hearthfires.esm",
+ "dragonborn.esm",
+
+ // these two plugins are considered "primary" for users but are not
+ // automatically loaded by the game so we need to force-write them
+ // to the plugin list
+ "enderal - forgotten stories.esm", "skyui_se.esp"};
+}
+
+QStringList GameEnderalSE::DLCPlugins() const
+{
+ return {};
+}
+
+QStringList GameEnderalSE::gameVariants() const
+{
+ return {"Steam", "GOG"};
+}
+
+QString GameEnderalSE::gameShortName() const
+{
+ return "EnderalSE";
+}
+
+QStringList GameEnderalSE::validShortNames() const
+{
+ return {"Skyrim", "SkyrimSE", "Enderal"};
+}
+
+QString GameEnderalSE::gameNexusName() const
+{
+ return "enderalspecialedition";
+}
+
+QStringList GameEnderalSE::iniFiles() const
+{
+ return {"Enderal.ini", "EnderalPrefs.ini"};
+}
+
+QStringList GameEnderalSE::CCPlugins() const
+{
+ return {};
+}
+
+IPluginGame::LoadOrderMechanism GameEnderalSE::loadOrderMechanism() const
+{
+ return IPluginGame::LoadOrderMechanism::PluginsTxt;
+}
+
+int GameEnderalSE::nexusModOrganizerID() const
+{
+ return 0;
+}
+
+int GameEnderalSE::nexusGameID() const
+{
+ return 3685;
+}
+
+QDir GameEnderalSE::gameDirectory() const
+{
+ return QDir(m_GamePath);
+}
+
+// Not to delete all the spaces...
+MappingType GameEnderalSE::mappings() const
+{
+ MappingType result;
+
+ for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) {
+ result.push_back({m_Organizer->profilePath() + "/" + profileFile,
+ localAppFolder() + "/" + gameDirectoryName() + "/" + profileFile,
+ false});
+ }
+
+ return result;
+}
diff --git a/libs/game_bethesda/src/games/enderalse/gameenderalse.h b/libs/game_bethesda/src/games/enderalse/gameenderalse.h
new file mode 100644
index 0000000..754951d
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/gameenderalse.h
@@ -0,0 +1,75 @@
+#ifndef GAMEENDERALSE_H
+#define GAMEENDERALSE_H
+
+#include "gamegamebryo.h"
+
+#include <QObject>
+#include <QtGlobal>
+
+class GameEnderalSE : public GameGamebryo
+{
+ Q_OBJECT
+
+ Q_PLUGIN_METADATA(IID "com.soundcontactstudio.GameEnderalSE" FILE
+ "gameenderalse.json")
+
+public:
+ GameEnderalSE();
+
+ virtual bool init(MOBase::IOrganizer* moInfo) override;
+
+public: // IPluginGame interface
+ virtual void detectGame() override;
+ virtual QString gameName() const override;
+ virtual QIcon gameIcon() 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 QStringList gameVariants() const override;
+ virtual QString binaryName() const override;
+ virtual QString getLauncherName() const override;
+ virtual bool looksValid(const QDir& folder) 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 QStringList CCPlugins() const override;
+ virtual LoadOrderMechanism loadOrderMechanism() const override;
+ virtual int nexusModOrganizerID() const override;
+ virtual int nexusGameID() const override;
+
+ virtual bool isInstalled() const override;
+ virtual void setGamePath(const QString& path) override;
+ virtual QDir gameDirectory() 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;
+ virtual MappingType mappings() const override;
+
+protected:
+ std::shared_ptr<const GamebryoSaveGame> makeSaveGame(QString filePath) const override;
+ QString savegameExtension() const override;
+ QString savegameSEExtension() const override;
+
+ QString gameDirectoryName() const;
+ QDir documentsDirectory() const;
+ QDir savesDirectory() const;
+ QFileInfo findInGameFolder(const QString& relativePath) const;
+
+ void checkVariants();
+ void setVariant(QString variant);
+
+ virtual QString identifyGamePath() const override;
+};
+
+#endif // _GAMESKYRIMSE_H
diff --git a/libs/game_bethesda/src/games/enderalse/gameenderalse.json b/libs/game_bethesda/src/games/enderalse/gameenderalse.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderalse/gameenderalse.json
@@ -0,0 +1 @@
+{}