diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-13 18:24:40 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-13 18:24:40 -0500 |
| commit | 0770a18d6968c464f175eae1fa129b0a3fbf8438 (patch) | |
| tree | 7f5508ca294ed80d211ab27785ee2c7d27ac5560 /src | |
| parent | 145fc7f610cc4607c42ad55c37317bdd93223fbf (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.cpp | 34 | ||||
| -rw-r--r-- | src/src/wineprefix.cpp | 64 | ||||
| -rw-r--r-- | src/src/wineprefix.h | 7 |
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; |
