aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-13 18:24:40 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-13 18:24:40 -0500
commit0770a18d6968c464f175eae1fa129b0a3fbf8438 (patch)
tree7f5508ca294ed80d211ab27785ee2c7d27ac5560 /src
parent145fc7f610cc4607c42ad55c37317bdd93223fbf (diff)
Sync plugins.txt/loadorder.txt back from prefix after tool run
LOOT (and any other tool that edits the plugin list) writes to the deployed copies in the Wine prefix AppData. afterRun was syncing saves and INIs back but not plugins, so LOOT's reordering was silently reverted by savePluginList() writing the old in-memory order on top of the untouched profile file. Add WinePrefix::syncPluginsBack that picks the newest case variant of plugins.txt and loadorder.txt from the prefix and copies them back to the profile. Run it in afterRun alongside the existing save/INI sync. For FileTime-based games (Skyrim LE, FO3, FNV) the loadorder.txt removal now happens after the sync, so the file-time fallback still works. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/src/organizercore.cpp34
-rw-r--r--src/src/wineprefix.cpp64
-rw-r--r--src/src/wineprefix.h7
3 files changed, 97 insertions, 8 deletions
diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp
index 810e4c2..6973376 100644
--- a/src/src/organizercore.cpp
+++ b/src/src/organizercore.cpp
@@ -2617,14 +2617,9 @@ bool OrganizerCore::beforeRun(
void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode)
{
- // need to remove our stored load order because it may be outdated if a
- // foreign tool changed the file time. After removing that file,
- // refreshESPList will use the file time as the order
- if (managedGame()->loadOrderMechanism() ==
- IPluginGame::LoadOrderMechanism::FileTime) {
- log::debug("removing loadorder.txt");
- QFile::remove(m_CurrentProfile->getLoadOrderFileName());
- }
+ const bool fileTimeLoadOrder =
+ managedGame()->loadOrderMechanism() ==
+ IPluginGame::LoadOrderMechanism::FileTime;
#ifndef _WIN32
// Unmount the FUSE VFS now that the application has exited. unmount()
@@ -2695,11 +2690,34 @@ void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode)
prefixPathStr);
}
}
+
+ // Sync plugins.txt / loadorder.txt back from the prefix. LOOT and
+ // similar tools edit the deployed copies in AppData/Local/<Game>/ —
+ // without this sync, their changes are lost when refreshESPList
+ // rereads the untouched profile copy and savePluginList clobbers
+ // them with the old in-memory order.
+ const QString profilePluginsPath = m_CurrentProfile->getPluginsFileName();
+ const QString profileLoadOrderPath =
+ m_CurrentProfile->getLoadOrderFileName();
+ if (!prefix.syncPluginsBack(profilePluginsPath, profileLoadOrderPath,
+ dataDirName)) {
+ log::warn("Failed to sync plugins.txt / loadorder.txt back from prefix '{}'",
+ prefixPathStr);
+ }
}
}
}
#endif
+ // FileTime-based games (Skyrim LE, FO3, FNV) derive the load order from
+ // plugin file mtimes rather than loadorder.txt. Drop the profile copy so
+ // refreshESPList falls back to file times — LOOT updates those directly
+ // on the .esp files via FUSE setattr.
+ if (fileTimeLoadOrder) {
+ log::debug("removing loadorder.txt (FileTime load order mechanism)");
+ QFile::remove(m_CurrentProfile->getLoadOrderFileName());
+ }
+
// Refresh directory structure after VFS is unmounted so the refresher
// reads the real (vanilla) data directory plus individual mod directories,
// matching Windows USVFS behaviour.
diff --git a/src/src/wineprefix.cpp b/src/src/wineprefix.cpp
index de61172..741855d 100644
--- a/src/src/wineprefix.cpp
+++ b/src/src/wineprefix.cpp
@@ -532,6 +532,70 @@ bool WinePrefix::syncProfileInisBack(
return allCopied;
}
+bool WinePrefix::syncPluginsBack(const QString& profilePluginsPath,
+ const QString& profileLoadOrderPath,
+ const QString& dataDir) const
+{
+ if (!isValid()) {
+ MOBase::log::error("syncPluginsBack: prefix '{}' is not valid", m_prefixPath);
+ return false;
+ }
+
+ const QString pluginsDir = QDir(appdataLocal()).filePath(dataDir);
+ if (!QDir(pluginsDir).exists()) {
+ MOBase::log::debug("syncPluginsBack: prefix plugins dir '{}' does not exist",
+ pluginsDir);
+ return true;
+ }
+
+ auto pickNewest = [&](const QString& canonicalName) -> QString {
+ const QStringList variants =
+ findCaseVariants(QDir(pluginsDir).filePath(canonicalName));
+ QString newest;
+ QDateTime newestTime;
+ for (const QString& v : variants) {
+ const QFileInfo fi(v);
+ if (!newestTime.isValid() || fi.lastModified() > newestTime) {
+ newestTime = fi.lastModified();
+ newest = v;
+ }
+ }
+ return newest;
+ };
+
+ bool ok = true;
+
+ const QString newestPlugins = pickNewest("plugins.txt");
+ if (!newestPlugins.isEmpty()) {
+ MOBase::log::info("syncPluginsBack: '{}' <- '{}'", profilePluginsPath,
+ newestPlugins);
+ if (!copyFileWithParents(newestPlugins, profilePluginsPath)) {
+ MOBase::log::error("syncPluginsBack: failed to copy plugins.txt back to '{}'",
+ profilePluginsPath);
+ ok = false;
+ }
+ } else {
+ MOBase::log::debug("syncPluginsBack: no plugins.txt variant found in '{}'",
+ pluginsDir);
+ }
+
+ const QString newestLoadOrder = pickNewest("loadorder.txt");
+ if (!newestLoadOrder.isEmpty()) {
+ MOBase::log::info("syncPluginsBack: '{}' <- '{}'", profileLoadOrderPath,
+ newestLoadOrder);
+ if (!copyFileWithParents(newestLoadOrder, profileLoadOrderPath)) {
+ MOBase::log::error("syncPluginsBack: failed to copy loadorder.txt back to '{}'",
+ profileLoadOrderPath);
+ ok = false;
+ }
+ } else {
+ MOBase::log::debug("syncPluginsBack: no loadorder.txt variant found in '{}'",
+ pluginsDir);
+ }
+
+ return ok;
+}
+
// ── Wine registry (.reg file) access ─────────────────────────────────────────
// Wine .reg files use doubled backslashes in key paths:
diff --git a/src/src/wineprefix.h b/src/src/wineprefix.h
index ac8f0a2..b0e54e7 100644
--- a/src/src/wineprefix.h
+++ b/src/src/wineprefix.h
@@ -32,6 +32,13 @@ public:
bool syncProfileInisBack(
const QList<QPair<QString, QString>>& iniMappings) const;
+ // Sync plugins.txt / loadorder.txt from the prefix AppData back to the
+ // profile after a tool like LOOT may have edited them. Picks the newest
+ // case-insensitive variant of each file.
+ bool syncPluginsBack(const QString& profilePluginsPath,
+ const QString& profileLoadOrderPath,
+ 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;