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/starfield | |
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/starfield')
19 files changed, 1375 insertions, 0 deletions
diff --git a/libs/game_bethesda/src/games/starfield/CMakeLists.txt b/libs/game_bethesda/src/games/starfield/CMakeLists.txt new file mode 100644 index 0000000..e6b4204 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(game_starfield SHARED + gamestarfield.cpp + gamestarfield.h + starfieldbsainvalidation.cpp + starfieldbsainvalidation.h + starfielddataarchives.cpp + starfielddataarchives.h + starfieldgameplugins.cpp + starfieldgameplugins.h + starfieldmoddatachecker.h + starfieldmoddatacontent.h + starfieldsavegame.cpp + starfieldsavegame.h + starfieldscriptextender.cpp + starfieldscriptextender.h + starfieldunmanagedmods.cpp + starfieldunmanagedmods.h +) +mo2_configure_plugin(game_starfield NO_SOURCES WARNINGS 4) +mo2_default_source_group() +target_link_libraries(game_starfield PRIVATE game_creation) +mo2_install_plugin(game_starfield) diff --git a/libs/game_bethesda/src/games/starfield/game_starfield_en.ts b/libs/game_bethesda/src/games/starfield/game_starfield_en.ts new file mode 100644 index 0000000..4118e93 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/game_starfield_en.ts @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="en_US"> +<context> + <name>GameStarfield</name> + <message> + <location filename="gamestarfield.cpp" line="116"/> + <source>Starfield Support Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamestarfield.cpp" line="126"/> + <source>Adds support for the game Starfield.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamestarfield.cpp" line="139"/> + <source>Show a warning when ESP plugins are enabled in the load order.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamestarfield.cpp" line="142"/> + <source>Show a warning when plugins.txt management is invalid.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamestarfield.cpp" line="401"/> + <source>You have active ESP plugins in Starfield</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamestarfield.cpp" line="403"/> + <source>sTestFile entries are present</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamestarfield.cpp" line="413"/> + <source><p>ESP plugins are not ideal for Starfield. In addition to being unable to sort them alongside ESM or master-flagged plugins, certain record references are always kept loaded by the game. This consumes unnecessary resources and limits the game's ability to load what it needs.</p><p>Ideally, plugins should be saved as ESM files upon release. It can also be released as an ESL plugin, however there are additional concerns with the way light plugins are currently handled and should only be used when absolutely certain about what you're doing.</p><p>Notably, xEdit does not currently support saving ESP files.</p><h4>Current ESPs:</h4><p>%1</p></source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gamestarfield.cpp" line="427"/> + <source><p>You have plugin managment enabled but you still have sTestFile settings in your StarfieldCustom.ini. These must be removed or the game will not read the plugins.txt file. Management is still disabled.</p></source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>QObject</name> + <message> + <location filename="starfieldunmanagedmods.cpp" line="70"/> + <source>ContentCatalog.txt appears to be corrupt: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>StarfieldModDataContent</name> + <message> + <location filename="starfieldmoddatacontent.h" line="27"/> + <source>Materials</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="starfieldmoddatacontent.h" line="28"/> + <source>Geometries</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="starfieldmoddatacontent.h" line="29"/> + <source>Video</source> + <translation type="unfinished"></translation> + </message> +</context> +</TS> diff --git a/libs/game_bethesda/src/games/starfield/gamestarfield.cpp b/libs/game_bethesda/src/games/starfield/gamestarfield.cpp new file mode 100644 index 0000000..c6c211a --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/gamestarfield.cpp @@ -0,0 +1,438 @@ +#include "gamestarfield.h" + +#include "ipluginlist.h" + +#include "starfieldbsainvalidation.h" +#include "starfielddataarchives.h" +#include "starfieldgameplugins.h" +#include "starfieldmoddatachecker.h" +#include "starfieldmoddatacontent.h" +#include "starfieldsavegame.h" +#include "starfieldscriptextender.h" +#include "starfieldunmanagedmods.h" + +#include "versioninfo.h" +#include <executableinfo.h> +#include <gamebryolocalsavegames.h> +#include <gamebryosavegameinfo.h> +#include <pluginsetting.h> + +#include <QCoreApplication> +#include <QDesktopServices> +#include <QDir> +#include <QFileInfo> +#include <QList> +#include <QObject> +#include <QString> +#include <QStringList> + +#include <memory> + +#include "utility.h" + +using namespace MOBase; + +const unsigned int GameStarfield::PROBLEM_ESP; +const unsigned int GameStarfield::PROBLEM_TEST_FILE; + +GameStarfield::GameStarfield() {} + +bool GameStarfield::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared<StarfieldDataArchives>(this); + registerFeature(std::make_shared<StarfieldScriptExtender>(this)); + registerFeature(dataArchives); + registerFeature( + std::make_shared<GamebryoLocalSavegames>(this, "StarfieldCustom.ini")); + registerFeature(std::make_shared<StarfieldModDataChecker>(this)); + registerFeature( + std::make_shared<StarfieldModDataContent>(m_Organizer->gameFeatures())); + registerFeature(std::make_shared<GamebryoSaveGameInfo>(this)); + registerFeature(std::make_shared<StarfieldGamePlugins>(moInfo)); + registerFeature(std::make_shared<StarfieldUnmanagedMods>(this, localAppFolder())); + registerFeature(std::make_shared<StarfieldBSAInvalidation>(dataArchives.get(), this)); + + return true; +} + +QString GameStarfield::gameName() const +{ + return "Starfield"; +} + +void GameStarfield::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath("Starfield"); +} + +QString GameStarfield::identifyGamePath() const +{ + return parseSteamLocation(steamAPPId(), gameName()); +} + +QDir GameStarfield::dataDirectory() const +{ + QDir dataDir = documentsDirectory().absoluteFilePath("Data"); + if (!dataDir.exists()) + dataDir.mkdir(dataDir.path()); + return documentsDirectory().absoluteFilePath("Data"); +} + +QMap<QString, QDir> GameStarfield::secondaryDataDirectories() const +{ + QMap<QString, QDir> directories; + directories.insert("game_data", gameDirectory().absoluteFilePath("Data")); + return directories; +} + +QList<ExecutableInfo> GameStarfield::executables() const +{ + return QList<ExecutableInfo>() + << ExecutableInfo("SFSE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature<MOBase::ScriptExtender>() + ->loaderName())) + << ExecutableInfo("Starfield", findInGameFolder(binaryName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("2722710") + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Starfield\""); +} + +QList<ExecutableForcedLoadSetting> GameStarfield::executableForcedLoads() const +{ + return QList<ExecutableForcedLoadSetting>(); +} + +QString GameStarfield::name() const +{ + return "Starfield Support Plugin"; +} + +QString GameStarfield::localizedName() const +{ + return tr("Starfield Support Plugin"); +} + +QString GameStarfield::author() const +{ + return "Silarn"; +} + +QString GameStarfield::description() const +{ + return tr("Adds support for the game Starfield."); +} + +MOBase::VersionInfo GameStarfield::version() const +{ + return VersionInfo(1, 1, 0, VersionInfo::RELEASE_FINAL); +} + +QList<PluginSetting> GameStarfield::settings() const +{ + return QList<PluginSetting>() + << PluginSetting( + "enable_esp_warning", + tr("Show a warning when ESP plugins are enabled in the load order."), + true) + << PluginSetting("enable_management_warnings", + tr("Show a warning when plugins.txt management is invalid."), + true); +} + +MappingType GameStarfield::mappings() const +{ + MappingType result; + if (testFilePlugins().isEmpty()) { + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameShortName() + "/" + profileFile, + false}); + } + } + return result; +} + +void GameStarfield::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Starfield", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + copyToProfile(myGamesPath(), path, "StarfieldPrefs.ini"); + copyToProfile(myGamesPath(), path, "StarfieldCustom.ini"); + } +} + +QString GameStarfield::savegameExtension() const +{ + return "sfs"; +} + +QString GameStarfield::savegameSEExtension() const +{ + return "sfse"; +} + +std::shared_ptr<const GamebryoSaveGame> +GameStarfield::makeSaveGame(QString filePath) const +{ + return std::make_shared<const StarfieldSaveGame>(filePath, this); +} + +QString GameStarfield::steamAPPId() const +{ + return "1716740"; +} + +QStringList GameStarfield::testFilePlugins() const +{ + QStringList plugins; + if (m_Organizer != nullptr && m_Organizer->profile() != nullptr) { + QString customIni( + m_Organizer->profile()->absoluteIniFilePath("StarfieldCustom.ini")); + if (QFile(customIni).exists()) { + for (int i = 1; i <= 10; ++i) { + QString setting("sTestFile"); + setting += std::to_string(i); + QString plugin = GameGamebryo::readIniValue(customIni, "General", setting, ""); + if (!plugin.isEmpty() && !plugins.contains(plugin, Qt::CaseInsensitive)) { + plugins.append(plugin); + } + } + } + } + return plugins; +} + +QStringList GameStarfield::primaryPlugins() const +{ + QStringList plugins = {"Starfield.esm", "Constellation.esm", + "ShatteredSpace.esm", "OldMars.esm", + "SFBGS003.esm", "SFBGS004.esm", + "SFBGS006.esm", "SFBGS007.esm", + "SFBGS008.esm", "BlueprintShips-Starfield.esm"}; + + for (auto plugin : CCCPlugins()) { + if (!plugins.contains(plugin, Qt::CaseInsensitive)) { + plugins.append(plugin); + } + } + + auto testPlugins = testFilePlugins(); + if (loadOrderMechanism() == LoadOrderMechanism::None) { + plugins << enabledPlugins(); + plugins << testPlugins; + } + + plugins.removeDuplicates(); + + return plugins; +} + +QStringList GameStarfield::enabledPlugins() const +{ + return {}; +} + +QStringList GameStarfield::gameVariants() const +{ + return {"Regular"}; +} + +QString GameStarfield::gameShortName() const +{ + return "Starfield"; +} + +QString GameStarfield::gameNexusName() const +{ + return "starfield"; +} + +QStringList GameStarfield::iniFiles() const +{ + return {"StarfieldPrefs.ini", "StarfieldCustom.ini"}; +} + +bool GameStarfield::prepareIni(const QString& exec) +{ + return true; // no need to write to Starfield.ini +} + +QStringList GameStarfield::DLCPlugins() const +{ + return {"Constellation.esm", "ShatteredSpace.esm"}; +} + +QStringList GameStarfield::CCCPlugins() const +{ + // While the CCC file appears to be mostly legacy, we need to parse it since the game + // will still read it and there are some compatibility reason to use it for + // force-loading the core game plugins. + QStringList plugins = {}; + if (!testFilePresent()) { + QFile myDocsCCCFile(myGamesPath() + "/Starfield.ccc"); + QFile gameCCCFile(gameDirectory().absoluteFilePath("Starfield.ccc")); + QFile* file; + if (myDocsCCCFile.exists()) { + file = &myDocsCCCFile; + } else { + file = &gameCCCFile; + } + if (file->open(QIODevice::ReadOnly)) { + if (file->size() > 0) { + while (!file->atEnd()) { + QByteArray line = file->readLine().trimmed(); + QString modName; + if ((line.size() > 0) && (line.at(0) != '#')) { + modName = QString::fromUtf8(line.constData()); + } + if (modName.size() > 0) { + plugins.append(modName); + } + } + } + } + } + return plugins; +} + +QStringList GameStarfield::CCPlugins() const +{ + QStringList plugins = {}; + std::shared_ptr<StarfieldUnmanagedMods> unmanagedMods = + std::static_pointer_cast<StarfieldUnmanagedMods>( + m_Organizer->gameFeatures()->gameFeature<MOBase::UnmanagedMods>()); + + // The ContentCatalog.txt appears to be the main repository where Starfiled stores + // info about the installed Creations. We parse this to correctly mark unmanaged mods + // as Creations. The StarfieldUnmanagedMods class handles parsing mod names and files. + if (unmanagedMods.get()) { + auto contentCatalog = unmanagedMods->parseContentCatalog(); + for (const auto& mod : contentCatalog) { + if (!plugins.contains(mod.first, Qt::CaseInsensitive)) { + plugins.append(mod.first); + } + } + } + return plugins; +} + +IPluginGame::SortMechanism GameStarfield::sortMechanism() const +{ + return IPluginGame::SortMechanism::LOOT; +} + +IPluginGame::LoadOrderMechanism GameStarfield::loadOrderMechanism() const +{ + if (!testFilePresent()) + return IPluginGame::LoadOrderMechanism::PluginsTxt; + return IPluginGame::LoadOrderMechanism::None; +} + +int GameStarfield::nexusModOrganizerID() const +{ + return 0; +} + +int GameStarfield::nexusGameID() const +{ + return 4187; +} + +// Start Diagnose +std::vector<unsigned int> GameStarfield::activeProblems() const +{ + std::vector<unsigned int> result; + if (m_Organizer->managedGame() == this) { + if (m_Organizer->pluginSetting(name(), "enable_esp_warning").toBool() && + activeESP()) + result.push_back(PROBLEM_ESP); + if (m_Organizer->pluginSetting(name(), "enable_management_warnings").toBool()) { + if (testFilePresent()) + result.push_back(PROBLEM_TEST_FILE); + } + } + return result; +} + +bool GameStarfield::activeESP() const +{ + m_Active_ESPs.clear(); + std::set<QString> enabledPlugins; + + QStringList esps = m_Organizer->findFiles("", [](const QString& fileName) -> bool { + return fileName.endsWith(".esp", FileNameComparator::CaseSensitivity); + }); + + for (const QString& esp : esps) { + QString baseName = QFileInfo(esp).fileName(); + if (m_Organizer->pluginList()->state(baseName) == IPluginList::STATE_ACTIVE) { + m_Active_ESPs.insert(baseName); + } + } + + if (!m_Active_ESPs.empty()) + return true; + return false; +} + +bool GameStarfield::testFilePresent() const +{ + if (!testFilePlugins().isEmpty()) + return true; + return false; +} + +QString GameStarfield::shortDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_ESP: + return tr("You have active ESP plugins in Starfield"); + case PROBLEM_TEST_FILE: + return tr("sTestFile entries are present"); + } + return ""; +} + +QString GameStarfield::fullDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_ESP: { + QString espInfo = SetJoin(m_Active_ESPs, ", "); + return tr("<p>ESP plugins are not ideal for Starfield. In addition to being unable " + "to sort them alongside ESM or master-flagged plugins, certain record " + "references are always kept loaded by the game. This consumes " + "unnecessary resources and limits the game's ability to load what it " + "needs.</p>" + "<p>Ideally, plugins should be saved as ESM files upon release. It can " + "also be released as an ESL plugin, however there are additional " + "concerns with the way light plugins are currently handled and should " + "only be used when absolutely certain about what you're doing.</p>" + "<p>Notably, xEdit does not currently support saving ESP files.</p>" + "<h4>Current ESPs:</h4><p>%1</p>") + .arg(espInfo); + } + case PROBLEM_TEST_FILE: { + return tr("<p>You have plugin managment enabled but you still have sTestFile " + "settings in your StarfieldCustom.ini. These must be removed or the game " + "will not read the plugins.txt file. Management is still disabled.</p>"); + } + } + return ""; +} + +bool GameStarfield::hasGuidedFix(unsigned int key) const +{ + return false; +} + +void GameStarfield::startGuidedFix(unsigned int key) const {} diff --git a/libs/game_bethesda/src/games/starfield/gamestarfield.h b/libs/game_bethesda/src/games/starfield/gamestarfield.h new file mode 100644 index 0000000..f9724c0 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/gamestarfield.h @@ -0,0 +1,83 @@ +#ifndef GAMESTARFIELD_H +#define GAMESTARFIELD_H + +#include "gamegamebryo.h" +#include "iplugindiagnose.h" + +#include <QObject> +#include <QtGlobal> + +class GameStarfield : public GameGamebryo, public MOBase::IPluginDiagnose +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame MOBase::IPluginDiagnose) + Q_PLUGIN_METADATA(IID "org.modorganizer.GameStarfield" FILE "gamestarfield.json") + +public: + GameStarfield(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: + QStringList testFilePlugins() const; + +public: // IPluginGame interface + virtual QString gameName() const override; + virtual void detectGame() override; + virtual QDir dataDirectory() const override; + virtual QMap<QString, QDir> secondaryDataDirectories() 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 enabledPlugins() const override; + virtual QStringList gameVariants() const override; + virtual QString gameShortName() const override; + virtual QString gameNexusName() const override; + virtual QStringList iniFiles() const override; + virtual bool prepareIni(const QString& exec) override; + virtual QStringList DLCPlugins() const override; + virtual QStringList CCPlugins() const override; + virtual SortMechanism sortMechanism() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + +public: // IPluginDiagnose interface + virtual std::vector<unsigned int> activeProblems() const override; + virtual QString shortDescription(unsigned int key) const override; + virtual QString fullDescription(unsigned int key) const override; + virtual bool hasGuidedFix(unsigned int key) const override; + virtual void startGuidedFix(unsigned int key) 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: + virtual QString identifyGamePath() const override; + std::shared_ptr<const GamebryoSaveGame> makeSaveGame(QString filePath) const override; + QString savegameExtension() const override; + QString savegameSEExtension() const override; + +private: + QStringList CCCPlugins() const; + bool activeESP() const; + bool testFilePresent() const; + +private: + static const unsigned int PROBLEM_ESP = 1; + static const unsigned int PROBLEM_TEST_FILE = 2; + + mutable std::set<QString> m_Active_ESPs; +}; + +#endif // GAMEStarfield_H diff --git a/libs/game_bethesda/src/games/starfield/gamestarfield.json b/libs/game_bethesda/src/games/starfield/gamestarfield.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/gamestarfield.json @@ -0,0 +1 @@ +{} diff --git a/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp b/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp new file mode 100644 index 0000000..f6e803b --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp @@ -0,0 +1,68 @@ +#include "starfieldbsainvalidation.h" + +#include "dummybsa.h" +#include "iplugingame.h" +#include "iprofile.h" +#include "registry.h" +#include <imoinfo.h> +#include <utility.h> + +#include "gamegamebryo.h" + +StarfieldBSAInvalidation::StarfieldBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "StarfieldCustom.ini", game) +{ + m_IniFileName = "StarfieldCustom.ini"; + m_Game = game; +} + +bool StarfieldBSAInvalidation::isInvalidationBSA(const QString& bsaName) +{ + return false; +} + +QString StarfieldBSAInvalidation::invalidationBSAName() const +{ + return ""; +} + +unsigned long StarfieldBSAInvalidation::bsaVersion() const +{ + return 0x68; +} + +bool StarfieldBSAInvalidation::prepareProfile(MOBase::IProfile* profile) +{ + bool dirty = false; + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : m_Game->documentsDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + + if (profile->invalidationActive(nullptr)) { + // write bInvalidateOlderFiles = 1, if needed + QString bInvalidateOlderFiles = GameGamebryo::readIniValue( + iniFilePath, "Archive", "bInvalidateOlderFiles", "0"); + if (bInvalidateOlderFiles.toLong() != 1) { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "bInvalidateOlderFiles", "1", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + QString sResourceDataDirsFinal = GameGamebryo::readIniValue( + iniFilePath, "Archive", "sResourceDataDirsFinal", "STRINGS\\"); + if (sResourceDataDirsFinal != "") { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "sResourceDataDirsFinal", "", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } + + return dirty; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.h b/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.h new file mode 100644 index 0000000..40ec355 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.h @@ -0,0 +1,33 @@ +#ifndef STARFIELDBSAINVALIDATION_H +#define STARFIELDBSAINVALIDATION_H + +#include "gamebryobsainvalidation.h" +#include "starfielddataarchives.h" +#include <bsainvalidation.h> +#include <dataarchives.h> + +#include <memory> + +namespace MOBase +{ +class IPluginGame; +} + +class StarfieldBSAInvalidation : public GamebryoBSAInvalidation +{ +public: + StarfieldBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game); + virtual bool isInvalidationBSA(const QString& bsaName) override; + virtual bool prepareProfile(MOBase::IProfile* profile) override; + +private: + virtual QString invalidationBSAName() const override; + virtual unsigned long bsaVersion() const override; + +private: + QString m_IniFileName; + MOBase::IPluginGame const* m_Game; +}; + +#endif // STARFIELDBSAINVALIDATION_H diff --git a/libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp b/libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp new file mode 100644 index 0000000..d7eadae --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp @@ -0,0 +1,98 @@ +#include "starfielddataarchives.h" + +#include "iprofile.h" +#include <utility.h> + +QStringList StarfieldDataArchives::vanillaArchives() const +{ + return {"Starfield - Animations.ba2", + "Starfield - DensityMaps.ba2", + "Starfield - FaceAnimation01.ba2", + "Starfield - FaceAnimation02.ba2", + "Starfield - FaceAnimation03.ba2", + "Starfield - FaceAnimation04.ba2", + "Starfield - FaceAnimationPatch.ba2", + "Starfield - FaceMeshes.ba2", + "Starfield - GeneratedTextures.ba2", + "Starfield - Interface.ba2", + "Starfield - Localization.ba2", + "Starfield - LODMeshes.ba2", + "Starfield - LODMeshesPatch.ba2", + "Starfield - LODTextures.ba2", + "Starfield - Materials.ba2", + "Starfield - Meshes01.ba2", + "Starfield - Meshes02.ba2", + "Starfield - MeshesPatch.ba2", + "Starfield - Misc.ba2", + "Starfield - Particles.ba2", + "Starfield - ParticlesTestData.ba2", + "Starfield - PlanetData.ba2", + "Starfield - Shaders.ba2", + "Starfield - ShadersBeta.ba2", + "Starfield - Terrain01.ba2", + "Starfield - Terrain02.ba2", + "Starfield - Terrain03.ba2", + "Starfield - Terrain04.ba2", + "Starfield - TerrainPatch.ba2", + "Starfield - Textures01.ba2", + "Starfield - Textures02.ba2", + "Starfield - Textures03.ba2", + "Starfield - Textures04.ba2", + "Starfield - Textures05.ba2", + "Starfield - Textures06.ba2", + "Starfield - Textures07.ba2", + "Starfield - Textures08.ba2", + "Starfield - Textures09.ba2", + "Starfield - Textures10.ba2", + "Starfield - Textures11.ba2", + "Starfield - TexturesPatch.ba2", + "Starfield - Voices01.ba2", + "Starfield - Voices02.ba2", + "Starfield - VoicesPatch.ba2", + "Starfield - WwiseSounds01.ba2", + "Starfield - WwiseSounds02.ba2", + "Starfield - WwiseSounds03.ba2", + "Starfield - WwiseSounds04.ba2", + "Starfield - WwiseSounds05.ba2", + "Starfield - WwiseSoundsPatch.ba2", + "Constellation - Localization.ba2", + "Constellation - Textures.ba2", + "OldMars - Localization.ba2", + "OldMars - Textures.ba2", + "BlueprintShips-Starfield - Localization.ba2"}; +} + +QStringList StarfieldDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString defaultIniFile = gameDirectory().absoluteFilePath("Starfield.ini"); + QString customIniFile = + profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("StarfieldCustom.ini") + : localGameDirectory().absoluteFilePath("StarfieldCustom.ini"); + QStringList archiveSettings = {"SResourceArchiveList", "sResourceIndexFileList", + "SResourceArchiveMemoryCacheList", + "sResourceStartUpArchiveList", + "sResourceEnglishVoiceList"}; + for (auto setting : archiveSettings) { + auto archives = getArchivesFromKey(customIniFile, setting, 4096); + if (archives.isEmpty()) + archives = getArchivesFromKey(defaultIniFile, setting, 4096); + result.append(archives); + } + + return result; +} + +void StarfieldDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{} + +void StarfieldDataArchives::addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) +{} + +void StarfieldDataArchives::removeArchive(MOBase::IProfile* profile, + const QString& archiveName) +{} diff --git a/libs/game_bethesda/src/games/starfield/starfielddataarchives.h b/libs/game_bethesda/src/games/starfield/starfielddataarchives.h new file mode 100644 index 0000000..c83943f --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfielddataarchives.h @@ -0,0 +1,28 @@ +#ifndef STARFIELDDATAARCHIVES_H +#define STARFIELDDATAARCHIVES_H + +#include <QDir> +#include <QStringList> + +#include "gamebryodataarchives.h" + +class GameGamebryo; + +class StarfieldDataArchives : public GamebryoDataArchives +{ +public: + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + virtual void addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) override; + virtual void removeArchive(MOBase::IProfile* profile, + const QString& archiveName) override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // STARFIELDDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp new file mode 100644 index 0000000..6b4e6ab --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp @@ -0,0 +1,35 @@ +#include "starfieldgameplugins.h" + +using namespace MOBase; + +StarfieldGamePlugins::StarfieldGamePlugins(MOBase::IOrganizer* organizer) + : CreationGamePlugins(organizer) +{} + +bool StarfieldGamePlugins::mediumPluginsAreSupported() +{ + return true; +} + +bool StarfieldGamePlugins::blueprintPluginsAreSupported() +{ + return true; +} + +void StarfieldGamePlugins::writePluginList(const IPluginList* pluginList, + const QString& filePath) +{ + if (m_Organizer->managedGame()->loadOrderMechanism() == + MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt) { + CreationGamePlugins::writePluginList(pluginList, filePath); + } +} + +QStringList StarfieldGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + if (m_Organizer->managedGame()->loadOrderMechanism() == + MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt) { + return CreationGamePlugins::readPluginList(pluginList); + } + return {}; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldgameplugins.h b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.h new file mode 100644 index 0000000..a957b13 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.h @@ -0,0 +1,23 @@ +#ifndef _STARFIELDGAMEPLUGINS_H +#define _STARFIELDGAMEPLUGINS_H + +#include <creationgameplugins.h> + +#include <QObject> +#include <QtGlobal> + +class StarfieldGamePlugins : public CreationGamePlugins +{ + +public: + StarfieldGamePlugins(MOBase::IOrganizer* organizer); + +protected: + virtual bool mediumPluginsAreSupported() override; + virtual bool blueprintPluginsAreSupported() override; + virtual void writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) override; + virtual QStringList readPluginList(MOBase::IPluginList* pluginList) override; +}; + +#endif // _STARFIELDGAMEPLUGINS_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldmoddatachecker.h b/libs/game_bethesda/src/games/starfield/starfieldmoddatachecker.h new file mode 100644 index 0000000..13c1fcc --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldmoddatachecker.h @@ -0,0 +1,29 @@ +#ifndef STARFIELD_MODATACHECKER_H +#define STARFIELD_MODATACHECKER_H + +#include <gamebryomoddatachecker.h> + +class StarfieldModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{ + "interface", "meshes", "geometries", "music", "scripts", "sound", + "strings", "textures", "trees", "video", "materials", "sfse", + "distantlod", "asi", "Tools", "MCM", "distantland", "mits", + "dllplugins", "CalienteTools", "shadersfx", "aaf"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "esl", "ba2", + "modgroups", "ini", "csg", "cdx"}; + return result; + } +}; + +#endif // STARFIELD_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldmoddatacontent.h b/libs/game_bethesda/src/games/starfield/starfieldmoddatacontent.h new file mode 100644 index 0000000..d4b8014 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldmoddatacontent.h @@ -0,0 +1,55 @@ +#ifndef STARFIELD_MODDATACONTENT_H +#define STARFIELD_MODDATACONTENT_H + +#include <gamebryomoddatacontent.h> +#include <ifiletree.h> + +class StarfieldModDataContent : public GamebryoModDataContent +{ +protected: + enum StarfieldContent + { + CONTENT_MATERIAL = CONTENT_NEXT_VALUE, + CONTENT_GEOMETRIES = CONTENT_NEXT_VALUE + 1, + CONTENT_VIDEO = CONTENT_NEXT_VALUE + 2 + }; + +public: + StarfieldModDataContent(MOBase::IGameFeatures const* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + m_Enabled[CONTENT_SKYPROC] = false; + } + + std::vector<Content> getAllContents() const override + { + static std::vector<Content> STARFIELD_CONTENTS{ + {CONTENT_MATERIAL, QT_TR_NOOP("Materials"), ":/MO/gui/content/material"}, + {CONTENT_GEOMETRIES, QT_TR_NOOP("Geometries"), ":/MO/gui/content/geometries"}, + {CONTENT_VIDEO, QT_TR_NOOP("Video"), ":/MO/gui/content/media"}}; + auto contents = GamebryoModDataContent::getAllContents(); + std::copy(std::begin(STARFIELD_CONTENTS), std::end(STARFIELD_CONTENTS), + std::back_inserter(contents)); + return contents; + } + + std::vector<int> + getContentsFor(std::shared_ptr<const MOBase::IFileTree> fileTree) const override + { + auto contents = GamebryoModDataContent::getContentsFor(fileTree); + for (auto e : *fileTree) { + if (e->isDir()) { + if (e->compare("materials") == 0) { + contents.push_back(CONTENT_MATERIAL); + } else if (e->compare("geometries") == 0) { + contents.push_back(CONTENT_GEOMETRIES); + } else if (e->compare("video") == 0) { + contents.push_back(CONTENT_VIDEO); + } + } + } + return contents; + } +}; + +#endif // STARFIELD_MODDATACONTENT_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp b/libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp new file mode 100644 index 0000000..49653cb --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp @@ -0,0 +1,157 @@ +#include "starfieldsavegame.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 "gamestarfield.h" + +StarfieldSaveGame::StarfieldSaveGame(QString const& fileName, GameStarfield const* game) + : GamebryoSaveGame(fileName, game, true, true) +{ + FileWrapper file(getFilepath(), "BCPS"); + + getData(file); + FILETIME creationTime; + unsigned char saveVersion; + fetchInformationFields(file, m_SaveNumber, saveVersion, m_PCName, m_PCLevel, + m_PCLocation, creationTime); + file.closeCompressedData(); + file.close(); + + // 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(&creationTime, &ctime); + + setCreationTime(ctime); +} + +void StarfieldSaveGame::getData(FileWrapper& file) const +{ + file.skip<uint32_t>(); // header version + file.skip<uint64_t>(); // chunk compressed size array start location + file.skip<uint64_t>(); // unknown (0?) + file.setCompressionType(1); + /* + * Parse following variables then begin decompressing data + * - 64-bit int = compressed data start location + * - 64-bit int = complete uncompressed data size + */ + file.openCompressedData(); + /* + * Remaining headers before start of compressed data: + * - 32-bit float (version? appears to be 2.0) + * - 64-bit int - size of uncompressed chunks (250 KiB) + * - 64-bit int - size of byte rows? (16 bytes) used to determine start of each + * compressed chunk + * - 32-bit int - number of chunks? + * - 'ZIP ' - denotes start of chunk compressed size array + * - compressed size array - array of 32-bit ints containing the compressed size of + * each compressed chunk (see number of chunks above) + */ +} + +void StarfieldSaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, unsigned char& saveVersion, + QString& playerName, unsigned short& playerLevel, QString& playerLocation, + FILETIME& creationTime) const +{ + char fileID[12]; // SFS_SAVEGAME + unsigned int headerSize; + unsigned int version; + // file.read(fileID, 12); + headerSize = file.readInt(12); + version = file.readInt(); + saveVersion = file.readChar(); + saveNumber = file.readInt(); + file.read(playerName); + + unsigned int temp; + temp = file.readInt(); + playerLevel = static_cast<unsigned short>(temp); + file.read(playerLocation); + + QString ignore; + file.read(ignore); // playtime as ascii hh.mm.ss + file.read(ignore); // race name (i.e. BretonRace) + + unsigned short gender; + gender = file.readShort(); // Player gender (0 = male) + float experience, experienceRequired; + experience = file.readFloat(); + experienceRequired = file.readFloat(); + + unsigned long long time = file.readLong(); + creationTime.dwLowDateTime = (DWORD)time; + creationTime.dwHighDateTime = time >> 32; +} + +std::unique_ptr<GamebryoSaveGame::DataFields> StarfieldSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "BCPS"); // 10bytes + + getData(file); + FILETIME creationTime; + unsigned char saveVersion; + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, saveVersion, dummyName, dummyLevel, + dummyLocation, dummyTime); + } + + int extraInfo = 0; + if (saveVersion >= 122) + extraInfo = 1; + if (saveVersion >= 140) + extraInfo = 2; + QStringList gamePlugins = m_Game->primaryPlugins() + m_Game->enabledPlugins(); + + QString ignore; + std::unique_ptr<DataFields> fields = std::make_unique<DataFields>(); + + file.readChar(12); + file.read(ignore); // game version + file.read(ignore); // game version again? + file.readInt(); // plugin info size + + fields->Plugins = file.readPlugins(0, extraInfo, gamePlugins); + fields->LightPlugins = file.readLightPlugins(0, extraInfo, gamePlugins); + if (saveVersion >= 122) + fields->MediumPlugins = file.readMediumPlugins(0, extraInfo, gamePlugins); + file.closeCompressedData(); + file.close(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldsavegame.h b/libs/game_bethesda/src/games/starfield/starfieldsavegame.h new file mode 100644 index 0000000..7bd07e0 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldsavegame.h @@ -0,0 +1,27 @@ +#ifndef STARFIELDSAVEGAME_H +#define STARFIELDSAVEGAME_H + +#include "gamebryosavegame.h" +#include "windows_compat.h" +#include <QDataStream> + +class GameStarfield; + +class StarfieldSaveGame : public GamebryoSaveGame +{ +public: + StarfieldSaveGame(QString const& fileName, GameStarfield const* game); + +protected: + // Fetch easy-to-access information. + void getData(FileWrapper& file) const; + + void fetchInformationFields(FileWrapper& file, uint32_t& saveNumber, + unsigned char& saveVersion, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, + FILETIME& creationTime) const; + + std::unique_ptr<DataFields> fetchDataFields() const override; +}; + +#endif // STARFIELDSAVEGAME_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp b/libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp new file mode 100644 index 0000000..15cf6cd --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp @@ -0,0 +1,18 @@ +#include "starfieldscriptextender.h" + +#include <QString> +#include <QStringList> + +StarfieldScriptExtender::StarfieldScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString StarfieldScriptExtender::BinaryName() const +{ + return "sfse_loader.exe"; +} + +QString StarfieldScriptExtender::PluginPath() const +{ + return "sfse/plugins"; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldscriptextender.h b/libs/game_bethesda/src/games/starfield/starfieldscriptextender.h new file mode 100644 index 0000000..b7c27f1 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldscriptextender.h @@ -0,0 +1,17 @@ +#ifndef STARFIELDSCRIPTEXTENDER_H +#define STARFIELDSCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class StarfieldScriptExtender : public GamebryoScriptExtender +{ +public: + StarfieldScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // STARFIELDSCRIPTEXTENDER_H diff --git a/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp new file mode 100644 index 0000000..30abb38 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp @@ -0,0 +1,132 @@ +#include "starfieldunmanagedmods.h" + +#include "log.h" + +#include <QFile> +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonValue> + +StarfieldUnmanagedMods::StarfieldUnmanagedMods(const GameStarfield* game, + const QString& appDataFolder) + : GamebryoUnmangedMods(game), m_AppDataFolder(appDataFolder) +{} + +StarfieldUnmanagedMods::~StarfieldUnmanagedMods() {} + +QStringList StarfieldUnmanagedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins() + game()->enabledPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QMap<QString, QDir> directories = {{"data", game()->dataDirectory()}}; + directories.insert(game()->secondaryDataDirectories()); + for (QDir directory : directories) { + for (const QString& fileName : directory.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; +} + +QFileInfo StarfieldUnmanagedMods::referenceFile(const QString& modName) const +{ + QFileInfoList files; + QMap<QString, QDir> directories = {{"data", game()->dataDirectory()}}; + directories.insert(game()->secondaryDataDirectories()); + for (QDir directory : directories) { + files += directory.entryInfoList(QStringList() << modName + ".es*"); + } + if (files.size() > 0) { + return files.at(0); + } else { + return QFileInfo(); + } +} + +std::map<QString, StarfieldUnmanagedMods::ContentCatalog> +StarfieldUnmanagedMods::parseContentCatalog() const +{ + QFile content(m_AppDataFolder + "/" + game()->gameShortName() + + "/ContentCatalog.txt"); + std::map<QString, StarfieldUnmanagedMods::ContentCatalog> contentCatalog; + if (content.open(QIODevice::OpenModeFlag::ReadOnly)) { + auto contentData = content.readAll(); + QString convertedData = QString::fromLatin1(contentData); + contentData = convertedData.toUtf8(); + QJsonParseError jsonError; + QJsonDocument contentDoc = QJsonDocument::fromJson(contentData, &jsonError); + if (jsonError.error) { + MOBase::log::warn(QObject::tr("ContentCatalog.txt appears to be corrupt: %1") + .arg(jsonError.errorString())); + } else { + QJsonObject contentObj = contentDoc.object(); + for (const auto& mod : contentObj.keys()) { + if (mod == "ContentCatalog") + continue; + auto modInfo = contentObj.value(mod).toObject(); + QStringList pluginList; + QStringList files; + for (const auto& file : modInfo.value("Files").toArray()) { + QString fileName = file.toString(); + files.append(fileName); + if (fileName.endsWith(".esm", Qt::CaseInsensitive) || + fileName.endsWith(".esl", Qt::CaseInsensitive) || + fileName.endsWith(".esp", Qt::CaseInsensitive)) { + pluginList.append(fileName); + } + } + for (const auto& plugin : pluginList) { + contentCatalog[plugin] = ContentCatalog(); + contentCatalog[plugin].files = files; + contentCatalog[plugin].name = modInfo.value("Title").toString(); + } + } + } + } + return contentCatalog; +} + +QStringList StarfieldUnmanagedMods::secondaryFiles(const QString& modName) const +{ + QStringList files; + auto contentCatalog = parseContentCatalog(); + for (const auto& mod : contentCatalog) { + if (mod.first.startsWith(modName, Qt::CaseInsensitive)) { + files += mod.second.files; + break; + } + } + // file extension in FO4 is .ba2 instead of bsa + QMap<QString, QDir> directories = {{"data", game()->dataDirectory()}}; + directories.insert(game()->secondaryDataDirectories()); + for (QDir directory : directories) { + for (const QString& archiveName : directory.entryList({modName + "*.ba2"})) { + files.append(directory.absoluteFilePath(archiveName)); + } + } + return files; +} + +QString StarfieldUnmanagedMods::displayName(const QString& modName) const +{ + auto contentCatalog = parseContentCatalog(); + for (const auto& mod : contentCatalog) { + if (mod.first.startsWith(modName, Qt::CaseInsensitive)) { + return mod.second.name; + } + } + // unlike in earlier games, in fallout 4 the file name doesn't correspond to + // the public name + return modName; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h new file mode 100644 index 0000000..abe14ef --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h @@ -0,0 +1,36 @@ +#ifndef STARFIELDUNMANAGEDMODS_H +#define STARFIELDUNMANAGEDMODS_H + +#include "gamebryounmanagedmods.h" +#include "gamestarfield.h" +#include <gamegamebryo.h> + +#include <QJsonObject> + +class StarfieldUnmanagedMods : public GamebryoUnmangedMods +{ + friend class GameStarfield; + + struct ContentCatalog + { + QString name; + QStringList files; + }; + +public: + StarfieldUnmanagedMods(const GameStarfield* game, const QString& appDataFolder); + ~StarfieldUnmanagedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; + virtual QFileInfo referenceFile(const QString& modName) const override; + virtual QStringList secondaryFiles(const QString& modName) const override; + virtual QString displayName(const QString& modName) const override; + +private: + std::map<QString, ContentCatalog> parseContentCatalog() const; + +private: + QString m_AppDataFolder; +}; + +#endif // STARFIELDUNMANAGEDMODS_H |
