diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /libs/game_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')
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 @@ +{} |
