aboutsummaryrefslogtreecommitdiff
path: root/libs/game_bethesda/src/games/enderal
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/enderal
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/enderal')
-rw-r--r--libs/game_bethesda/src/games/enderal/CMakeLists.txt24
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp16
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalbsainvalidation.h20
-rw-r--r--libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp42
-rw-r--r--libs/game_bethesda/src/games/enderal/enderaldataarchives.h23
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp123
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalgameplugins.h23
-rw-r--r--libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp24
-rw-r--r--libs/game_bethesda/src/games/enderal/enderallocalsavegames.h35
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalmoddatachecker.h49
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalmoddatacontent.h14
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalsavegame.cpp101
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalsavegame.h23
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp18
-rw-r--r--libs/game_bethesda/src/games/enderal/enderalscriptextender.h17
-rw-r--r--libs/game_bethesda/src/games/enderal/game_enderal_en.ts17
-rw-r--r--libs/game_bethesda/src/games/enderal/gameenderal.cpp267
-rw-r--r--libs/game_bethesda/src/games/enderal/gameenderal.h61
-rw-r--r--libs/game_bethesda/src/games/enderal/gameenderal.json1
19 files changed, 898 insertions, 0 deletions
diff --git a/libs/game_bethesda/src/games/enderal/CMakeLists.txt b/libs/game_bethesda/src/games/enderal/CMakeLists.txt
new file mode 100644
index 0000000..348429c
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/CMakeLists.txt
@@ -0,0 +1,24 @@
+cmake_minimum_required(VERSION 3.16)
+
+add_library(game_enderal SHARED
+ enderalbsainvalidation.cpp
+ enderalbsainvalidation.h
+ enderaldataarchives.cpp
+ enderaldataarchives.h
+ enderalgameplugins.cpp
+ enderalgameplugins.h
+ enderallocalsavegames.cpp
+ enderallocalsavegames.h
+ enderalmoddatachecker.h
+ enderalmoddatacontent.h
+ enderalsavegame.cpp
+ enderalsavegame.h
+ enderalscriptextender.cpp
+ enderalscriptextender.h
+ gameenderal.cpp
+ gameenderal.h
+)
+mo2_configure_plugin(game_enderal NO_SOURCES WARNINGS 4)
+mo2_default_source_group()
+target_link_libraries(game_enderal PRIVATE game_creation)
+mo2_install_plugin(game_enderal)
diff --git a/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp
new file mode 100644
index 0000000..5564075
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp
@@ -0,0 +1,16 @@
+#include "enderalbsainvalidation.h"
+
+EnderalBSAInvalidation::EnderalBSAInvalidation(MOBase::DataArchives* dataArchives,
+ MOBase::IPluginGame const* game)
+ : GamebryoBSAInvalidation(dataArchives, "enderal.ini", game)
+{}
+
+QString EnderalBSAInvalidation::invalidationBSAName() const
+{
+ return "Enderal - Invalidation.bsa";
+}
+
+unsigned long EnderalBSAInvalidation::bsaVersion() const
+{
+ return 0x68;
+}
diff --git a/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.h b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.h
new file mode 100644
index 0000000..721da3e
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.h
@@ -0,0 +1,20 @@
+#ifndef ENDERALBSAINVALIDATION_H
+#define ENDERALBSAINVALIDATION_H
+
+#include "enderaldataarchives.h"
+#include "gamebryobsainvalidation.h"
+
+#include <memory>
+
+class EnderalBSAInvalidation : public GamebryoBSAInvalidation
+{
+public:
+ EnderalBSAInvalidation(MOBase::DataArchives* dataArchives,
+ MOBase::IPluginGame const* game);
+
+private:
+ virtual QString invalidationBSAName() const override;
+ virtual unsigned long bsaVersion() const override;
+};
+
+#endif // ENDERALBSAINVALIDATION_H
diff --git a/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp b/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp
new file mode 100644
index 0000000..3ac4ee0
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp
@@ -0,0 +1,42 @@
+#include "enderaldataarchives.h"
+#include <utility.h>
+
+QStringList EnderalDataArchives::vanillaArchives() const
+{
+ return {"Skyrim - Misc.bsa", "Skyrim - Shaders.bsa", "Skyrim - Textures.bsa",
+ "Skyrim - Interface.bsa", "Skyrim - Animations.bsa", "Skyrim - Meshes.bsa",
+ "Skyrim - Sounds.bsa", "E - Meshes.bsa", "E - Music.bsa",
+ "E - Scripts.bsa", "E - Sounds.bsa", "E - Textures1.bsa",
+ "E - Textures2.bsa", "E - Textures3.bsa", "L - Textures.bsa",
+ "L - Voices.bsa"};
+}
+
+QStringList EnderalDataArchives::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"));
+ result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2"));
+
+ return result;
+}
+
+void EnderalDataArchives::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() > 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/enderal/enderaldataarchives.h b/libs/game_bethesda/src/games/enderal/enderaldataarchives.h
new file mode 100644
index 0000000..cea1a8b
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/enderaldataarchives.h
@@ -0,0 +1,23 @@
+#ifndef ENDERALDATAARCHIVES_H
+#define ENDERALDATAARCHIVES_H
+
+#include <QDir>
+#include <QString>
+#include <QStringList>
+#include <gamebryodataarchives.h>
+#include <iprofile.h>
+
+class EnderalDataArchives : 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 // ENDERALDATAARCHIVES_H
diff --git a/libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp b/libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp
new file mode 100644
index 0000000..cd8048e
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp
@@ -0,0 +1,123 @@
+#include "enderalgameplugins.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;
+
+EnderalGamePlugins::EnderalGamePlugins(IOrganizer* organizer)
+ : GamebryoGamePlugins(organizer)
+{}
+
+void EnderalGamePlugins::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 should 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 EnderalGamePlugins::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/enderal/enderalgameplugins.h b/libs/game_bethesda/src/games/enderal/enderalgameplugins.h
new file mode 100644
index 0000000..f78904d
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/enderalgameplugins.h
@@ -0,0 +1,23 @@
+#ifndef _ENDERALGAMEPLUGINS_H
+#define _ENDERALGAMEPLUGINS_H
+
+#include <gamebryogameplugins.h>
+#include <imoinfo.h>
+#include <iplugingame.h>
+#include <map>
+
+class EnderalGamePlugins : public GamebryoGamePlugins
+{
+public:
+ EnderalGamePlugins(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 // _ENDERALSEGAMEPLUGINS_H
diff --git a/libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp b/libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp
new file mode 100644
index 0000000..e01783a
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/enderallocalsavegames.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 "enderallocalsavegames.h"
+
+QString EnderalLocalSavegames::localSavesDummy() const
+{
+ return "..\\Enderal\\__MO_Saves\\";
+}
diff --git a/libs/game_bethesda/src/games/enderal/enderallocalsavegames.h b/libs/game_bethesda/src/games/enderal/enderallocalsavegames.h
new file mode 100644
index 0000000..79a8d43
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/enderallocalsavegames.h
@@ -0,0 +1,35 @@
+/*
+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 ENDERALLOCALSAVEGAMES_H
+#define ENDERALLOCALSAVEGAMES_H
+
+#include <gamebryolocalsavegames.h>
+
+#include <QString>
+
+class EnderalLocalSavegames : public GamebryoLocalSavegames
+{
+public:
+ using GamebryoLocalSavegames::GamebryoLocalSavegames;
+
+protected:
+ QString localSavesDummy() const override;
+};
+
+#endif // ENDERALLOCALSAVEGAMES_H
diff --git a/libs/game_bethesda/src/games/enderal/enderalmoddatachecker.h b/libs/game_bethesda/src/games/enderal/enderalmoddatachecker.h
new file mode 100644
index 0000000..55f8aac
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/enderalmoddatachecker.h
@@ -0,0 +1,49 @@
+#ifndef ENDERAL_MODATACHECKER_H
+#define ENDERAL_MODATACHECKER_H
+
+#include <gamebryomoddatachecker.h>
+
+class EnderalModDataChecker : 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"};
+ return result;
+ }
+ virtual const FileNameSet& possibleFileExtensions() const override
+ {
+ static FileNameSet result{"esp", "esm", "bsa", "modgroups", "ini"};
+ return result;
+ }
+};
+
+#endif // ENDERAL_MODATACHECKER_H
diff --git a/libs/game_bethesda/src/games/enderal/enderalmoddatacontent.h b/libs/game_bethesda/src/games/enderal/enderalmoddatacontent.h
new file mode 100644
index 0000000..47b45b9
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/enderalmoddatacontent.h
@@ -0,0 +1,14 @@
+#ifndef ENDERAL_MODDATACONTENT_H
+#define ENDERAL_MODDATACONTENT_H
+
+#include <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+// Like Skyrim, Enderal does not need any change from the default feature:
+class EnderalModDataContent : public GamebryoModDataContent
+{
+public:
+ using GamebryoModDataContent::GamebryoModDataContent;
+};
+
+#endif // ENDERAL_MODDATACONTENT_H
diff --git a/libs/game_bethesda/src/games/enderal/enderalsavegame.cpp b/libs/game_bethesda/src/games/enderal/enderalsavegame.cpp
new file mode 100644
index 0000000..cd9beb3
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/enderalsavegame.cpp
@@ -0,0 +1,101 @@
+#include "enderalsavegame.h"
+
+#ifdef _WIN32
+#include <Windows.h>
+#else
+#include <QDateTime>
+
+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
+
+#include "gameenderal.h"
+
+EnderalSaveGame::EnderalSaveGame(QString const& fileName, GameEnderal 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 EnderalSaveGame::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.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);
+}
+
+std::unique_ptr<GamebryoSaveGame::DataFields> EnderalSaveGame::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/enderal/enderalsavegame.h b/libs/game_bethesda/src/games/enderal/enderalsavegame.h
new file mode 100644
index 0000000..c29eb46
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/enderalsavegame.h
@@ -0,0 +1,23 @@
+#ifndef ENDERALSAVEGAME_H
+#define ENDERALSAVEGAME_H
+
+#include "gamebryosavegame.h"
+#include "windows_compat.h"
+
+class GameEnderal;
+
+class EnderalSaveGame : public GamebryoSaveGame
+{
+public:
+ EnderalSaveGame(QString const& fileName, GameEnderal 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 // ENDERALSAVEGAME_H
diff --git a/libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp b/libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp
new file mode 100644
index 0000000..ff3864b
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp
@@ -0,0 +1,18 @@
+#include "enderalscriptextender.h"
+
+#include <QString>
+#include <QStringList>
+
+EnderalScriptExtender::EnderalScriptExtender(GameGamebryo const* game)
+ : GamebryoScriptExtender(game)
+{}
+
+QString EnderalScriptExtender::BinaryName() const
+{
+ return "skse_loader.exe";
+}
+
+QString EnderalScriptExtender::PluginPath() const
+{
+ return "skse/plugins";
+}
diff --git a/libs/game_bethesda/src/games/enderal/enderalscriptextender.h b/libs/game_bethesda/src/games/enderal/enderalscriptextender.h
new file mode 100644
index 0000000..7d0d027
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/enderalscriptextender.h
@@ -0,0 +1,17 @@
+#ifndef ENDERALSCRIPTEXTENDER_H
+#define ENDERALSCRIPTEXTENDER_H
+
+#include "gamebryoscriptextender.h"
+
+class GameGamebryo;
+
+class EnderalScriptExtender : public GamebryoScriptExtender
+{
+public:
+ EnderalScriptExtender(const GameGamebryo* game);
+
+ virtual QString BinaryName() const override;
+ virtual QString PluginPath() const override;
+};
+
+#endif // ENDERALSCRIPTEXTENDER_H
diff --git a/libs/game_bethesda/src/games/enderal/game_enderal_en.ts b/libs/game_bethesda/src/games/enderal/game_enderal_en.ts
new file mode 100644
index 0000000..9dcac4d
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/game_enderal_en.ts
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameEnderal</name>
+ <message>
+ <location filename="gameenderal.cpp" line="93"/>
+ <source>Enderal Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gameenderal.cpp" line="103"/>
+ <source>Adds support for the game Enderal</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/libs/game_bethesda/src/games/enderal/gameenderal.cpp b/libs/game_bethesda/src/games/enderal/gameenderal.cpp
new file mode 100644
index 0000000..fb4e1d7
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/gameenderal.cpp
@@ -0,0 +1,267 @@
+#include "gameenderal.h"
+
+#include "enderalbsainvalidation.h"
+#include "enderaldataarchives.h"
+#include "enderalgameplugins.h"
+#include "enderallocalsavegames.h"
+#include "enderalmoddatachecker.h"
+#include "enderalmoddatacontent.h"
+#include "enderalsavegame.h"
+#include "enderalscriptextender.h"
+
+#include "executableinfo.h"
+#include "pluginsetting.h"
+#include "steamutility.h"
+#include "utility.h"
+
+#include <gamebryogameplugins.h>
+#include <gamebryosavegameinfo.h>
+#include <gamebryounmanagedmods.h>
+
+#include <QCoreApplication>
+#include <QDebug>
+#include <QFileInfo>
+
+#include <QIcon>
+#include <QtDebug>
+
+#ifdef _WIN32
+#include <Windows.h>
+#include <winver.h>
+#endif
+
+#include <exception>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+using namespace MOBase;
+
+GameEnderal::GameEnderal() {}
+
+bool GameEnderal::init(IOrganizer* moInfo)
+{
+ if (!GameGamebryo::init(moInfo)) {
+ return false;
+ }
+
+ auto dataArchives = std::make_shared<EnderalDataArchives>(this);
+ registerFeature(std::make_shared<EnderalScriptExtender>(this));
+ registerFeature(dataArchives);
+ registerFeature(std::make_shared<EnderalBSAInvalidation>(dataArchives.get(), this));
+ registerFeature(std::make_shared<GamebryoSaveGameInfo>(this));
+ registerFeature(std::make_shared<EnderalLocalSavegames>(this, "enderal.ini"));
+ registerFeature(std::make_shared<EnderalModDataChecker>(this));
+ registerFeature(std::make_shared<EnderalModDataContent>(moInfo->gameFeatures()));
+ registerFeature(std::make_shared<EnderalGamePlugins>(moInfo));
+ registerFeature(std::make_shared<GamebryoUnmangedMods>(this));
+
+ return true;
+}
+
+QString GameEnderal::gameName() const
+{
+ return "Enderal";
+}
+
+QList<ExecutableInfo> GameEnderal::executables() const
+{
+ return QList<ExecutableInfo>()
+ //<< ExecutableInfo("SKSE",
+ // findInGameFolder(feature<ScriptExtender>()->loaderName()))
+ //<< ExecutableInfo("SBW", findInGameFolder("SBW.exe"))
+ << ExecutableInfo("Enderal (SKSE)", findInGameFolder(binaryName()))
+ << ExecutableInfo("Enderal Launcher", findInGameFolder(getLauncherName()))
+ << ExecutableInfo("BOSS", findInGameFolder("BOSS/BOSS.exe"))
+ << ExecutableInfo("LOOT", QFileInfo(getLootPath()))
+ .withArgument("--game=\"Enderal\"")
+ << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe"))
+ .withSteamAppId("202480");
+}
+
+QList<ExecutableForcedLoadSetting> GameEnderal::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+QString GameEnderal::name() const
+{
+ return "Enderal Support Plugin";
+}
+
+QString GameEnderal::localizedName() const
+{
+ return tr("Enderal Support Plugin");
+}
+
+QString GameEnderal::author() const
+{
+ return "AL12 & MO2 Team";
+}
+
+QString GameEnderal::description() const
+{
+ return tr("Adds support for the game Enderal");
+}
+
+MOBase::VersionInfo GameEnderal::version() const
+{
+ return VersionInfo(1, 3, 1, VersionInfo::RELEASE_FINAL);
+}
+
+QList<PluginSetting> GameEnderal::settings() const
+{
+ QList<PluginSetting> results;
+ results.push_back(
+ PluginSetting("sse_downloads", "allow Skyrim SE downloads", QVariant(false)));
+ return results;
+}
+
+void GameEnderal::initializeProfile(const QDir& path, ProfileSettings settings) const
+{
+ if (settings.testFlag(IPluginGame::MODS)) {
+ copyToProfile(localAppFolder() + "/enderal", 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");
+ }
+ }
+}
+
+bool GameEnderal::looksValid(QDir const& path) const
+{
+ // we need to check both launcher and binary because the binary also exists for
+ // Skyrim and the launcher for Enderal SE
+ return path.exists(getLauncherName()) && path.exists(binaryName());
+}
+
+QIcon GameEnderal::gameIcon() const
+{
+ return MOBase::iconForExecutable(gameDirectory().absoluteFilePath(getLauncherName()));
+}
+
+QString GameEnderal::savegameExtension() const
+{
+ return "ess";
+}
+
+QString GameEnderal::savegameSEExtension() const
+{
+ return "skse";
+}
+
+std::shared_ptr<const GamebryoSaveGame>
+GameEnderal::makeSaveGame(QString filepath) const
+{
+ return std::make_shared<const EnderalSaveGame>(filepath, this);
+}
+
+QString GameEnderal::steamAPPId() const
+{
+ return "933480";
+}
+
+QStringList GameEnderal::primaryPlugins() const
+{
+ return {"Skyrim.esm", "Enderal - Forgotten Stories.esm", "Update.esm"};
+}
+
+QString GameEnderal::binaryName() const
+{
+ return "skse_loader.exe";
+}
+
+QString GameEnderal::getLauncherName() const
+{
+ return "Enderal Launcher.exe";
+}
+
+QString GameEnderal::gameShortName() const
+{
+ return "Enderal";
+}
+
+QString GameEnderal::gameNexusName() const
+{
+ return "enderal";
+}
+
+QStringList GameEnderal::primarySources() const
+{
+ return {"Skyrim"};
+}
+
+QStringList GameEnderal::validShortNames() const
+{
+ QStringList results;
+ results.push_back("Skyrim");
+ if (m_Organizer->pluginSetting(name(), "sse_downloads").toBool()) {
+ results.push_back("SkyrimSE");
+ }
+ return results;
+}
+
+QStringList GameEnderal::iniFiles() const
+{
+ return {"enderal.ini", "enderalprefs.ini"};
+}
+
+QStringList GameEnderal::DLCPlugins() const
+{
+ return {};
+}
+
+IPluginGame::LoadOrderMechanism GameEnderal::loadOrderMechanism() const
+{
+ return LoadOrderMechanism::PluginsTxt;
+}
+
+int GameEnderal::nexusModOrganizerID() const
+{
+ return 0;
+}
+
+int GameEnderal::nexusGameID() const
+{
+ return 2736;
+}
+
+QString GameEnderal::identifyGamePath() const
+{
+#ifdef _WIN32
+ QString path = "Software\\SureAI\\Enderal";
+ QString result;
+ try {
+ result =
+ findInRegistry(HKEY_CURRENT_USER, path.toStdWString().c_str(), L"Install_Path");
+ } catch (MOBase::MyException) {
+ result = MOBase::findSteamGame("Enderal", "Data\\Enderal - Forgotten Stories.esm");
+ }
+ return result;
+#else
+ // Prefer exact Steam app-id resolution for Enderal LE.
+ QString result = parseSteamLocation(steamAPPId(), "Enderal");
+ if (!result.isEmpty() && looksValid(QDir(result))) {
+ return result;
+ }
+
+ result = MOBase::findSteamGame("Enderal", "Data/Enderal - Forgotten Stories.esm");
+ if (!result.isEmpty() && looksValid(QDir(result))) {
+ return result;
+ }
+
+ return GameGamebryo::identifyGamePath();
+#endif
+}
diff --git a/libs/game_bethesda/src/games/enderal/gameenderal.h b/libs/game_bethesda/src/games/enderal/gameenderal.h
new file mode 100644
index 0000000..fb0a377
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/gameenderal.h
@@ -0,0 +1,61 @@
+#ifndef GAMEENDERAL_H
+#define GAMEENDERAL_H
+
+#include "gamegamebryo.h"
+
+#include <QObject>
+#include <QtGlobal>
+
+class GameEnderal : public GameGamebryo
+{
+ Q_OBJECT
+#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
+ Q_PLUGIN_METADATA(IID "org.tannin.GameEnderal" FILE "gameenderal.json")
+#endif
+
+public:
+ GameEnderal();
+
+ 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 getLauncherName() const override;
+ virtual QString gameShortName() const override;
+ virtual QStringList validShortNames() const override;
+ virtual QString gameNexusName() const override;
+ virtual QStringList primarySources() 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;
+ virtual bool looksValid(QDir const&) const override;
+ virtual QIcon gameIcon() 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;
+
+ virtual QString identifyGamePath() const override;
+};
+
+#endif // GAMEENDERAL_H
diff --git a/libs/game_bethesda/src/games/enderal/gameenderal.json b/libs/game_bethesda/src/games/enderal/gameenderal.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/libs/game_bethesda/src/games/enderal/gameenderal.json
@@ -0,0 +1 @@
+{}