From 4279200bd015d79179961f02a1b4d1544c61ff52 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Sat, 18 Apr 2026 08:47:27 -0500 Subject: Gate panel-plugin swap on enabled, pre-sync plugins.txt from prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bethesda Plugin Manager disabled in settings still swapped the espTab at UI init — the callback ran unconditionally. Gate it on isPluginEnabled and warn + flag restart when toggling panel plugins, since the swap is one-shot. Pre-deploy sync-back: if prefix Plugins.txt mtime is newer than profile (external LOOT run), syncPluginsBack first so the deploy doesn't clobber the external edits. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/MOPlugin/IPluginPanel.cpp | 4 +++ src/src/organizercore.cpp | 40 ++++++++++++++++++++++ src/src/settingsdialogplugins.cpp | 14 ++++++++ src/src/wineprefix.cpp | 20 +++++++++++ src/src/wineprefix.h | 6 ++++ 5 files changed, 84 insertions(+) diff --git a/libs/installer_bsplugins/src/MOPlugin/IPluginPanel.cpp b/libs/installer_bsplugins/src/MOPlugin/IPluginPanel.cpp index 904f049..9ec2b88 100644 --- a/libs/installer_bsplugins/src/MOPlugin/IPluginPanel.cpp +++ b/libs/installer_bsplugins/src/MOPlugin/IPluginPanel.cpp @@ -37,6 +37,10 @@ bool IPluginPanel::init(MOBase::IOrganizer* organizer) } organizer->onUserInterfaceInitialized([this, organizer](QMainWindow* mainWindow) { + if (!organizer->isPluginEnabled(this)) { + return; + } + const auto tabWidget = mainWindow->findChild(u"tabWidget"_s); if (tabWidget == nullptr) { diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index e70f68f..d236abe 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -2503,6 +2503,46 @@ bool OrganizerCore::beforeRun( localSavesFeature.get(), m_CurrentProfile); log::info("Wine prefix save target: '{}'", absoluteSaveDir); + // If the prefix's plugin-list file is newer than the profile's + // (e.g. LOOT ran outside MO2 and edited it), sync back first so + // external edits aren't clobbered by the deploy below. + { + WinePrefix::PluginListMechanism preMech = + WinePrefix::PluginListMechanism::None; + switch (managedGame()->loadOrderMechanism()) { + case IPluginGame::LoadOrderMechanism::PluginsTxt: + preMech = WinePrefix::PluginListMechanism::PluginsTxt; + break; + case IPluginGame::LoadOrderMechanism::FileTime: + preMech = WinePrefix::PluginListMechanism::FileTime; + break; + case IPluginGame::LoadOrderMechanism::None: + break; + } + if (preMech != WinePrefix::PluginListMechanism::None) { + const QString profilePluginsPath = + m_CurrentProfile->getPluginsFileName(); + const QDateTime prefixMTime = + prefix.prefixPluginsMTime(dataDirName); + const QDateTime profileMTime = + QFileInfo(profilePluginsPath).lastModified(); + if (prefixMTime.isValid() && + (!profileMTime.isValid() || prefixMTime > profileMTime)) { + log::info("Prefix plugins.txt newer than profile " + "(prefix={}, profile={}), syncing back before deploy", + prefixMTime.toString(Qt::ISODate), + profileMTime.toString(Qt::ISODate)); + if (prefix.syncPluginsBack(profilePluginsPath, dataDirName, + preMech)) { + refreshESPList(true); + } else { + log::warn("Pre-deploy sync-back failed; proceeding with " + "deploy — external edits may be lost"); + } + } + } + } + // Read plugin lines from profile's plugins.txt QFile pluginsFile(m_CurrentProfile->getPluginsFileName()); if (pluginsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { diff --git a/src/src/settingsdialogplugins.cpp b/src/src/settingsdialogplugins.cpp index 409c5f3..879ea0d 100644 --- a/src/src/settingsdialogplugins.cpp +++ b/src/src/settingsdialogplugins.cpp @@ -277,6 +277,20 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) dialog().setExitNeeded(Exit::Restart); } + // Panel plugins swap UI tabs at init and cannot be hot-unloaded. + static const QStringList panelPluginNames = { + QStringLiteral("Bethesda Plugin Manager"), + }; + if (panelPluginNames.contains(plugin->name())) { + QMessageBox::information( + parentWidget(), QObject::tr("Restart required"), + QObject::tr("The '%1' plugin manages a UI panel. Restart Fluorine Manager " + "for the change to take effect.") + .arg(plugin->localizedName()), + QMessageBox::Ok); + dialog().setExitNeeded(Exit::Restart); + } + updateListItems(); } diff --git a/src/src/wineprefix.cpp b/src/src/wineprefix.cpp index c320807..8ac9fd6 100644 --- a/src/src/wineprefix.cpp +++ b/src/src/wineprefix.cpp @@ -603,6 +603,26 @@ bool WinePrefix::syncProfileInisBack( return allCopied; } +QDateTime WinePrefix::prefixPluginsMTime(const QString& dataDir) const +{ + if (!isValid()) { + return {}; + } + const QString pluginsDir = QDir(appdataLocal()).filePath(dataDir); + if (!QDir(pluginsDir).exists()) { + return {}; + } + QDateTime newest; + for (const QString& v : + findCaseVariants(QDir(pluginsDir).filePath("plugins.txt"))) { + const QDateTime t = QFileInfo(v).lastModified(); + if (!newest.isValid() || t > newest) { + newest = t; + } + } + return newest; +} + bool WinePrefix::syncPluginsBack(const QString& profilePluginsPath, const QString& dataDir, PluginListMechanism mechanism) const diff --git a/src/src/wineprefix.h b/src/src/wineprefix.h index fd88043..338e9e4 100644 --- a/src/src/wineprefix.h +++ b/src/src/wineprefix.h @@ -1,6 +1,7 @@ #ifndef WINEPREFIX_H #define WINEPREFIX_H +#include #include #include #include @@ -61,6 +62,11 @@ public: const QString& dataDir, PluginListMechanism mechanism) const; + // Last-modified time of the newest plugin-list variant in the prefix. + // Invalid QDateTime if no variant exists. Used to decide whether to + // sync-back (prefix newer, e.g. external LOOT run) before deploying. + QDateTime prefixPluginsMTime(const QString& dataDir) const; + // Restore any stale .mo2linux_backup INI/save files left by a crash. // Should be called at startup before any game runs. void restoreStaleBackups() const; -- cgit v1.3.1