From d22d8c267b012729b6b84142def2a30ca4691d7a Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Fri, 20 Feb 2026 23:53:26 -0600 Subject: Fix AppImage plugin loading, save timestamps, and INI corruption - Add AppConfig::pluginsPath()/dllsPath() that respect MO2_PLUGINS_DIR and MO2_DLLS_DIR env vars, fixing plugin discovery when plugins live inside read-only AppImage squashfs (#22, game plugin not found errors) - Preserve file modification times in save deploy/sync so games display correct save timestamps instead of all showing the same time (#17) - Replace QSettings INI reader in saves tab with direct line-by-line parser to avoid backslash-as-line-continuation corruption of sLocalSavePath=__MO_Saves\ (#22, saves tab showing wrong directory) - Add diagnostic logging to plugin symlink creation, save backup/restore operations to help diagnose silent failures through prefix symlinks - Fix Python SONAME mismatch in AppImage build (patch portable Python SONAME to match librunner.so DT_NEEDED, preventing dual libpython load) - Bundle PyQt6 Qt dependencies, improve RPATH for lib/ and plugins/ Co-Authored-By: Claude Opus 4.6 --- src/src/commandline.cpp | 3 +- src/src/mainwindow.cpp | 162 ++++++----- src/src/moapplication.cpp | 129 ++++----- src/src/organizercore.cpp | 129 +++++---- src/src/plugincontainer.cpp | 326 +++++++++++---------- src/src/processrunner.cpp | 412 +++++++++++++-------------- src/src/protonlauncher.cpp | 201 ++++++++++--- src/src/protonlauncher.h | 2 + src/src/savestab.cpp | 655 +++++++++++++++++++++++-------------------- src/src/shared/appconfig.cpp | 22 ++ src/src/shared/appconfig.h | 10 + src/src/spawn.cpp | 550 ++++++++++++++++++------------------ src/src/wineprefix.cpp | 36 ++- 13 files changed, 1427 insertions(+), 1210 deletions(-) (limited to 'src') diff --git a/src/src/commandline.cpp b/src/src/commandline.cpp index 4613350..121fb58 100644 --- a/src/src/commandline.cpp +++ b/src/src/commandline.cpp @@ -853,8 +853,7 @@ std::optional ReloadPluginCommand::runPostOrganizer(OrganizerCore& core) const QString name = QString::fromStdString(vm()["PLUGIN"].as()); QString filepath = - QDir(AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath())) - .absoluteFilePath(name); + QDir(AppConfig::pluginsPath()).absoluteFilePath(name); log::debug("reloading plugin from {}", filepath); core.pluginContainer().reloadPlugin(filepath); diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp index 7aa6964..1eb5e36 100644 --- a/src/src/mainwindow.cpp +++ b/src/src/mainwindow.cpp @@ -544,12 +544,12 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, QApplication::instance()->installEventFilter(this); - scheduleCheckForProblems(); - refreshExecutablesList(); - updatePinnedExecutables(); - resetActionIcons(); - resetButtonIcons(); - processUpdates(); + scheduleCheckForProblems(); + refreshExecutablesList(); + updatePinnedExecutables(); + resetActionIcons(); + resetButtonIcons(); + processUpdates(); ui->modList->updateModCount(); ui->espList->updatePluginCount(); @@ -573,8 +573,8 @@ void MainWindow::setupModList() }); } -void MainWindow::resetActionIcons() -{ +void MainWindow::resetActionIcons() +{ // this is a bit of a hack // // the .qss files have historically set qproperty-icon by id and these ids @@ -636,38 +636,38 @@ void MainWindow::resetActionIcons() } } - // update the button for the potentially new icon - updateProblemsButton(); -} - -void MainWindow::resetButtonIcons() -{ - // Some icon-only QPushButtons can lose their icons after stylesheet repolish on - // Linux/AppImage. Re-apply explicit resource icons to keep these controls visible. - ui->listOptionsBtn->setIcon(QIcon::fromTheme( - "preferences-system", QIcon(":/MO/gui/settings"))); - ui->openFolderMenu->setIcon( - QIcon::fromTheme("folder-open", QIcon(":/MO/gui/open_folder"))); - ui->restoreModsButton->setIcon( - QIcon::fromTheme("edit-undo", QIcon(":/MO/gui/restore"))); - ui->saveModsButton->setIcon( - QIcon::fromTheme("document-save", QIcon(":/MO/gui/backup"))); - ui->sortButton->setIcon(QIcon::fromTheme("view-sort-ascending", QIcon(":/MO/gui/sort"))); - ui->restoreButton->setIcon( - QIcon::fromTheme("edit-undo", QIcon(":/MO/gui/restore"))); - ui->saveButton->setIcon(QIcon::fromTheme("document-save", QIcon(":/MO/gui/backup"))); - - ui->listOptionsBtn->setIconSize(QSize(16, 16)); - ui->openFolderMenu->setIconSize(QSize(16, 16)); - ui->restoreModsButton->setIconSize(QSize(16, 16)); - ui->saveModsButton->setIconSize(QSize(16, 16)); - ui->sortButton->setIconSize(QSize(16, 16)); - ui->restoreButton->setIconSize(QSize(16, 16)); - ui->saveButton->setIconSize(QSize(16, 16)); -} - -MainWindow::~MainWindow() -{ + // update the button for the potentially new icon + updateProblemsButton(); +} + +void MainWindow::resetButtonIcons() +{ + // Some icon-only QPushButtons can lose their icons after stylesheet repolish on + // Linux/AppImage. Re-apply explicit resource icons to keep these controls visible. + ui->listOptionsBtn->setIcon(QIcon::fromTheme( + "preferences-system", QIcon(":/MO/gui/settings"))); + ui->openFolderMenu->setIcon( + QIcon::fromTheme("folder-open", QIcon(":/MO/gui/open_folder"))); + ui->restoreModsButton->setIcon( + QIcon::fromTheme("edit-undo", QIcon(":/MO/gui/restore"))); + ui->saveModsButton->setIcon( + QIcon::fromTheme("document-save", QIcon(":/MO/gui/backup"))); + ui->sortButton->setIcon(QIcon::fromTheme("view-sort-ascending", QIcon(":/MO/gui/sort"))); + ui->restoreButton->setIcon( + QIcon::fromTheme("edit-undo", QIcon(":/MO/gui/restore"))); + ui->saveButton->setIcon(QIcon::fromTheme("document-save", QIcon(":/MO/gui/backup"))); + + ui->listOptionsBtn->setIconSize(QSize(16, 16)); + ui->openFolderMenu->setIconSize(QSize(16, 16)); + ui->restoreModsButton->setIconSize(QSize(16, 16)); + ui->saveModsButton->setIconSize(QSize(16, 16)); + ui->sortButton->setIconSize(QSize(16, 16)); + ui->restoreButton->setIconSize(QSize(16, 16)); + ui->saveButton->setIconSize(QSize(16, 16)); +} + +MainWindow::~MainWindow() +{ try { cleanup(); @@ -739,11 +739,11 @@ void MainWindow::allowListResize() ui->espList->header()->setStretchLastSection(true); } -void MainWindow::updateStyle(const QString&) -{ - resetActionIcons(); - resetButtonIcons(); -} +void MainWindow::updateStyle(const QString&) +{ + resetActionIcons(); + resetButtonIcons(); +} void MainWindow::resizeEvent(QResizeEvent* event) { @@ -1913,10 +1913,10 @@ bool MainWindow::refreshProfiles(bool selectProfile, QString newProfile) return profileBox->count() > 1; } -void MainWindow::refreshExecutablesList() -{ - QAbstractItemModel* model = ui->executablesListBox->model(); - +void MainWindow::refreshExecutablesList() +{ + QAbstractItemModel* model = ui->executablesListBox->model(); + auto add = [&](const QString& title, const QFileInfo& binary) { QIcon icon; if (!binary.fileName().isEmpty()) { @@ -1932,36 +1932,36 @@ void MainWindow::refreshExecutablesList() Qt::SizeHintRole); }; - ui->executablesListBox->setEnabled(false); - ui->executablesListBox->clear(); - - add(tr(""), {}); - - auto shouldSkipInLaunchDropdown = [](const Executable& exe) { - const QString title = exe.title().trimmed(); - const QString lower = title.toLower(); - if (lower == QStringLiteral("proton") || lower == QStringLiteral("prefix") || - lower.startsWith(QStringLiteral("proton ")) || - lower.startsWith(QStringLiteral("prefix "))) { - return true; - } - - return false; - }; - - for (const auto& exe : *m_OrganizerCore.executablesList()) { - if (exe.hide()) { - continue; - } - - if (shouldSkipInLaunchDropdown(exe)) { - log::debug("Skipping internal executable entry '{}' from launch dropdown", - exe.title()); - continue; - } - - add(exe.title(), exe.binaryInfo()); - } + ui->executablesListBox->setEnabled(false); + ui->executablesListBox->clear(); + + add(tr(""), {}); + + auto shouldSkipInLaunchDropdown = [](const Executable& exe) { + const QString title = exe.title().trimmed(); + const QString lower = title.toLower(); + if (lower == QStringLiteral("proton") || lower == QStringLiteral("prefix") || + lower.startsWith(QStringLiteral("proton ")) || + lower.startsWith(QStringLiteral("prefix "))) { + return true; + } + + return false; + }; + + for (const auto& exe : *m_OrganizerCore.executablesList()) { + if (exe.hide()) { + continue; + } + + if (shouldSkipInLaunchDropdown(exe)) { + log::debug("Skipping internal executable entry '{}' from launch dropdown", + exe.title()); + continue; + } + + add(exe.title(), exe.binaryInfo()); + } if (ui->executablesListBox->count() == 1) { // all executables are hidden, add an empty one to at least be able to @@ -2692,9 +2692,7 @@ void MainWindow::openInstallFolder() void MainWindow::openPluginsFolder() { - QString pluginsPath = - AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath()); - shell::Explore(pluginsPath); + shell::Explore(AppConfig::pluginsPath()); } void MainWindow::openStylesheetsFolder() diff --git a/src/src/moapplication.cpp b/src/src/moapplication.cpp index fd60745..3009a45 100644 --- a/src/src/moapplication.cpp +++ b/src/src/moapplication.cpp @@ -41,9 +41,9 @@ along with Mod Organizer. If not, see . #include "shared/util.h" #include "thread_utils.h" #include "tutorialmanager.h" -#include -#include -#include +#include +#include +#include #include #include #include @@ -51,10 +51,10 @@ along with Mod Organizer. If not, see . #include #include #include -#include -#include -#include -#include +#include +#include +#include +#include #ifdef _WIN32 // see addDllsToPath() below @@ -270,43 +270,43 @@ void MOApplication::firstTimeSetup(MOMultiProcess& multiProcess) Qt::QueuedConnection); } -int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) -{ - TimeThis tt("MOApplication setup()"); - std::fprintf(stderr, "[setup-diag] setup() entered\n"); - std::fflush(stderr); +int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) +{ + TimeThis tt("MOApplication setup()"); + std::fprintf(stderr, "[setup-diag] setup() entered\n"); + std::fflush(stderr); // makes plugin data path available to plugins, see // IOrganizer::getPluginDataPath() MOBase::details::setPluginDataPath(OrganizerCore::pluginDataPath()); // figuring out the current instance - m_instance = getCurrentInstance(forceSelect); - if (!m_instance) { - std::fprintf(stderr, "[setup-diag] getCurrentInstance() returned null\n"); - std::fflush(stderr); - return 1; - } - std::fprintf(stderr, "[setup-diag] instance dir='%s' ini='%s'\n", - qUtf8Printable(m_instance->directory()), - qUtf8Printable(m_instance->iniPath())); - std::fflush(stderr); + m_instance = getCurrentInstance(forceSelect); + if (!m_instance) { + std::fprintf(stderr, "[setup-diag] getCurrentInstance() returned null\n"); + std::fflush(stderr); + return 1; + } + std::fprintf(stderr, "[setup-diag] instance dir='%s' ini='%s'\n", + qUtf8Printable(m_instance->directory()), + qUtf8Printable(m_instance->iniPath())); + std::fflush(stderr); // first time the data path is available, set the global property and log // directory, then log a bunch of debug stuff const QString dataPath = m_instance->directory(); setProperty("dataPath", dataPath); - if (!setLogDirectory(dataPath)) { - std::fprintf(stderr, "[setup-diag] setLogDirectory() failed for '%s'\n", - qUtf8Printable(dataPath)); - std::fflush(stderr); - reportError(tr("Failed to create log folder.")); - InstanceManager::singleton().clearCurrentInstance(); - return 1; - } - std::fprintf(stderr, "[setup-diag] setLogDirectory() ok\n"); - std::fflush(stderr); + if (!setLogDirectory(dataPath)) { + std::fprintf(stderr, "[setup-diag] setLogDirectory() failed for '%s'\n", + qUtf8Printable(dataPath)); + std::fflush(stderr); + reportError(tr("Failed to create log folder.")); + InstanceManager::singleton().clearCurrentInstance(); + return 1; + } + std::fprintf(stderr, "[setup-diag] setLogDirectory() ok\n"); + std::fflush(stderr); #ifdef _WIN32 log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW())); @@ -336,11 +336,11 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) } // loading settings - m_settings.reset(new Settings(m_instance->iniPath(), true)); - std::fprintf(stderr, "[setup-diag] settings loaded from '%s'\n", - qUtf8Printable(m_instance->iniPath())); - std::fflush(stderr); - log::getDefault().setLevel(m_settings->diagnostics().logLevel()); + m_settings.reset(new Settings(m_instance->iniPath(), true)); + std::fprintf(stderr, "[setup-diag] settings loaded from '%s'\n", + qUtf8Printable(m_instance->iniPath())); + std::fflush(stderr); + log::getDefault().setLevel(m_settings->diagnostics().logLevel()); log::debug("using ini at '{}'", m_settings->filename()); OrganizerCore::setGlobalCoreDumpType(m_settings->diagnostics().coreDumpType()); @@ -375,30 +375,30 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) tt.start("MOApplication::doOneRun() OrganizerCore"); log::debug("initializing core"); - m_core.reset(new OrganizerCore(*m_settings)); - std::fprintf(stderr, "[setup-diag] organizer core constructed\n"); - std::fflush(stderr); - if (!m_core->bootstrap()) { - std::fprintf(stderr, "[setup-diag] organizer core bootstrap failed\n"); - std::fflush(stderr); - reportError(tr("Failed to set up data paths.")); - InstanceManager::singleton().clearCurrentInstance(); - return 1; - } - std::fprintf(stderr, "[setup-diag] organizer core bootstrap ok\n"); - std::fflush(stderr); + m_core.reset(new OrganizerCore(*m_settings)); + std::fprintf(stderr, "[setup-diag] organizer core constructed\n"); + std::fflush(stderr); + if (!m_core->bootstrap()) { + std::fprintf(stderr, "[setup-diag] organizer core bootstrap failed\n"); + std::fflush(stderr); + reportError(tr("Failed to set up data paths.")); + InstanceManager::singleton().clearCurrentInstance(); + return 1; + } + std::fprintf(stderr, "[setup-diag] organizer core bootstrap ok\n"); + std::fflush(stderr); // plugins tt.start("MOApplication::doOneRun() plugins"); log::debug("initializing plugins"); - m_plugins = std::make_unique(m_core.get()); - std::fprintf(stderr, "[setup-diag] plugin container constructed, loading plugins...\n"); - std::fflush(stderr); - m_plugins->loadPlugins(); - std::fprintf(stderr, "[setup-diag] plugin loading finished\n"); - std::fflush(stderr); - log::debug("all plugins loaded"); + m_plugins = std::make_unique(m_core.get()); + std::fprintf(stderr, "[setup-diag] plugin container constructed, loading plugins...\n"); + std::fflush(stderr); + m_plugins->loadPlugins(); + std::fprintf(stderr, "[setup-diag] plugin loading finished\n"); + std::fflush(stderr); + log::debug("all plugins loaded"); // instance log::debug("entering setupInstanceLoop..."); @@ -456,14 +456,15 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) m_core->createDefaultProfile(); m_core->createOverwriteDirectories(); - log::info("using game plugin '{}' ('{}', variant {}, steam id '{}') at {}", - m_instance->gamePlugin()->gameName(), - m_instance->gamePlugin()->gameShortName(), - (m_settings->game().edition().value_or("").isEmpty() - ? "(none)" - : *m_settings->game().edition()), - m_instance->gamePlugin()->steamAPPId(), - m_instance->gamePlugin()->gameDirectory().absolutePath()); + { + const auto edition = m_settings->game().edition().value_or(""); + const auto variant = edition.isEmpty() ? QString("Steam") : edition; + log::info("using game plugin '{}' ('{}', variant {}) at {}", + m_instance->gamePlugin()->gameName(), + m_instance->gamePlugin()->gameShortName(), + variant, + m_instance->gamePlugin()->gameDirectory().absolutePath()); + } CategoryFactory::instance().loadCategories(); m_core->updateExecutablesList(); diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index 630f23b..79ea90b 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -904,8 +904,7 @@ QString OrganizerCore::pluginDataPath() // separate writable directory so mkdir() never hits a read-only FS. return AppConfig::basePath() + "/plugin_data"; #else - return AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath()) + - "/data"; + return AppConfig::pluginsPath() + "/data"; #endif } @@ -2193,23 +2192,23 @@ bool OrganizerCore::beforeRun( WinePrefix prefix(prefixPathStr); if (prefix.isValid()) { const QString dataDirName = resolveWineDataDirName(managedGame()); - const QString appDataLocal = prefix.appdataLocal(); - const QString pluginsTargetDir = - QDir(appDataLocal).filePath(dataDirName); - const QString documentsDir = - resolvePrefixGameDocumentsDir(prefix, dataDirName); + const QString appDataLocal = prefix.appdataLocal(); + const QString pluginsTargetDir = + QDir(appDataLocal).filePath(dataDirName); + const QString documentsDir = + resolvePrefixGameDocumentsDir(prefix, dataDirName); log::info("Wine prefix paths: AppData/Local plugins dir='{}', " "Documents/My Games INI dir='{}'", pluginsTargetDir, documentsDir); - const auto localSavesFeature = gameFeatures().gameFeature(); - const QString absoluteSaveDir = - resolveAbsoluteSaveDir(prefix, managedGame(), - localSavesFeature.get(), m_CurrentProfile); - log::info("Wine prefix save target: '{}'", absoluteSaveDir); - - // Read plugin lines from profile's plugins.txt - QFile pluginsFile(m_CurrentProfile->getPluginsFileName()); - if (pluginsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + const auto localSavesFeature = gameFeatures().gameFeature(); + const QString absoluteSaveDir = + resolveAbsoluteSaveDir(prefix, managedGame(), + localSavesFeature.get(), m_CurrentProfile); + log::info("Wine prefix save target: '{}'", absoluteSaveDir); + + // Read plugin lines from profile's plugins.txt + QFile pluginsFile(m_CurrentProfile->getPluginsFileName()); + if (pluginsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QStringList plugins; QTextStream stream(&pluginsFile); while (!stream.atEnd()) { @@ -2219,17 +2218,17 @@ bool OrganizerCore::beforeRun( } } pluginsFile.close(); - - if (!plugins.isEmpty()) { - log::info( - "Deploying plugin list to '{}' and '{}' (load order to '{}')", - QDir(pluginsTargetDir).filePath("Plugins.txt"), - QDir(pluginsTargetDir).filePath("plugins.txt"), - QDir(pluginsTargetDir).filePath("loadorder.txt")); - if (prefix.deployPlugins(plugins, dataDirName)) { - log::debug("Deployed {} plugins to prefix '{}' (dataDirName='{}')", - plugins.size(), prefixPathStr, dataDirName); - } else { + + if (!plugins.isEmpty()) { + log::info( + "Deploying plugin list to '{}' and '{}' (load order to '{}')", + QDir(pluginsTargetDir).filePath("Plugins.txt"), + QDir(pluginsTargetDir).filePath("plugins.txt"), + QDir(pluginsTargetDir).filePath("loadorder.txt")); + if (prefix.deployPlugins(plugins, dataDirName)) { + log::debug("Deployed {} plugins to prefix '{}' (dataDirName='{}')", + plugins.size(), prefixPathStr, dataDirName); + } else { log::error("Failed to deploy {} plugins to prefix '{}' " "(dataDirName='{}')", plugins.size(), prefixPathStr, dataDirName); @@ -2244,18 +2243,18 @@ bool OrganizerCore::beforeRun( const QString targetIniBase = resolvePrefixGameDocumentsDir(prefix, dataDirName); int deployedIniCount = 0; - for (const QString& iniFile : managedGame()->iniFiles()) { - const QString sourceIni = - m_CurrentProfile->absoluteIniFilePath(iniFile); - const QString targetIni = - QDir(targetIniBase).filePath(QFileInfo(iniFile).fileName()); - log::info("INI deploy target: '{}' -> '{}' (exists={})", sourceIni, - targetIni, QFileInfo::exists(sourceIni)); - if (QFileInfo::exists(sourceIni) && - prefix.deployProfileIni(sourceIni, targetIni)) { - ++deployedIniCount; - log::debug("Deployed profile INI '{}' -> '{}'", sourceIni, targetIni); - } + for (const QString& iniFile : managedGame()->iniFiles()) { + const QString sourceIni = + m_CurrentProfile->absoluteIniFilePath(iniFile); + const QString targetIni = + QDir(targetIniBase).filePath(QFileInfo(iniFile).fileName()); + log::info("INI deploy target: '{}' -> '{}' (exists={})", sourceIni, + targetIni, QFileInfo::exists(sourceIni)); + if (QFileInfo::exists(sourceIni) && + prefix.deployProfileIni(sourceIni, targetIni)) { + ++deployedIniCount; + log::debug("Deployed profile INI '{}' -> '{}'", sourceIni, targetIni); + } } if (deployedIniCount > 0) { log::debug("Deployed {} profile INI files to prefix '{}'", deployedIniCount, @@ -2267,15 +2266,15 @@ bool OrganizerCore::beforeRun( managedGame()->documentsDirectory().absolutePath()); } - if (m_CurrentProfile->localSavesEnabled()) { - const QString profileSavesDir = - QDir(m_CurrentProfile->absolutePath()).filePath("saves"); - log::info("Save deploy target: '{}' -> '{}'", profileSavesDir, - absoluteSaveDir); - if (!prefix.deployProfileSaves(profileSavesDir, absoluteSaveDir, true)) { - log::warn("Failed to deploy profile saves from '{}' to prefix '{}'", - profileSavesDir, prefixPathStr); - } else { + if (m_CurrentProfile->localSavesEnabled()) { + const QString profileSavesDir = + QDir(m_CurrentProfile->absolutePath()).filePath("saves"); + log::info("Save deploy target: '{}' -> '{}'", profileSavesDir, + absoluteSaveDir); + if (!prefix.deployProfileSaves(profileSavesDir, absoluteSaveDir, true)) { + log::warn("Failed to deploy profile saves from '{}' to prefix '{}'", + profileSavesDir, prefixPathStr); + } else { log::debug("Deployed profile saves '{}' -> '{}' in prefix '{}'", profileSavesDir, absoluteSaveDir, prefixPathStr); } @@ -2321,29 +2320,29 @@ void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode) resolveAbsoluteSaveDir(prefix, managedGame(), localSavesFeature.get(), m_CurrentProfile); - if (m_CurrentProfile->localSavesEnabled()) { - const QString profileSavesDir = - QDir(m_CurrentProfile->absolutePath()).filePath("saves"); - log::info("Save sync target: '{}' <- '{}'", profileSavesDir, - absoluteSaveDir); - if (!prefix.syncSavesBack(profileSavesDir, absoluteSaveDir)) { - log::warn("Failed to sync saves back from prefix '{}' to '{}'", - prefixPathStr, profileSavesDir); - } - } + if (m_CurrentProfile->localSavesEnabled()) { + const QString profileSavesDir = + QDir(m_CurrentProfile->absolutePath()).filePath("saves"); + log::info("Save sync target: '{}' <- '{}'", profileSavesDir, + absoluteSaveDir); + if (!prefix.syncSavesBack(profileSavesDir, absoluteSaveDir)) { + log::warn("Failed to sync saves back from prefix '{}' to '{}'", + prefixPathStr, profileSavesDir); + } + } if (m_CurrentProfile->localSettingsEnabled()) { const QString targetIniBase = resolvePrefixGameDocumentsDir(prefix, dataDirName); QList> iniMappings; for (const QString& iniFile : managedGame()->iniFiles()) { - const QString profileIni = - m_CurrentProfile->absoluteIniFilePath(iniFile); - const QString targetIni = - QDir(targetIniBase).filePath(QFileInfo(iniFile).fileName()); - iniMappings.append({profileIni, targetIni}); - log::info("INI sync target: '{}' <- '{}'", profileIni, targetIni); - } + const QString profileIni = + m_CurrentProfile->absoluteIniFilePath(iniFile); + const QString targetIni = + QDir(targetIniBase).filePath(QFileInfo(iniFile).fileName()); + iniMappings.append({profileIni, targetIni}); + log::info("INI sync target: '{}' <- '{}'", profileIni, targetIni); + } if (!iniMappings.isEmpty() && !prefix.syncProfileInisBack(iniMappings)) { diff --git a/src/src/plugincontainer.cpp b/src/src/plugincontainer.cpp index dad0117..e215c0a 100644 --- a/src/src/plugincontainer.cpp +++ b/src/src/plugincontainer.cpp @@ -4,12 +4,12 @@ #include "organizerproxy.h" #include "report.h" #include "shared/appconfig.h" -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -22,13 +22,13 @@ using namespace MOShared; namespace bf = boost::fusion; -#ifndef _WIN32 +#ifndef _WIN32 // Ensure the instance plugin directory contains symlinks to all bundled plugins. // Real user files (not matching any bundled plugin name) are left untouched. // Stale copies of bundled plugins are replaced with symlinks so that RPATH // resolution works correctly (critical for Flatpak where libs live in /app/). -static void ensureBundledPluginsLinked(const QString& bundledDir, - const QString& instanceDir) +static void ensureBundledPluginsLinked(const QString& bundledDir, + const QString& instanceDir) { QDir().mkpath(instanceDir); QDirIterator it(bundledDir, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); @@ -54,16 +54,19 @@ static void ensureBundledPluginsLinked(const QString& bundledDir, } } - QFile::link(it.filePath(), target); + if (!QFile::link(it.filePath(), target)) { + log::warn("ensureBundledPluginsLinked: failed to symlink '{}' -> '{}'", + it.filePath(), target); + } } -} -#endif - -static void printPluginDiagToStderr(const QString& message) -{ - std::fprintf(stderr, "[plugin-diag] %s\n", qUtf8Printable(message)); - std::fflush(stderr); -} +} +#endif + +static void printPluginDiagToStderr(const QString& message) +{ + std::fprintf(stderr, "[plugin-diag] %s\n", qUtf8Printable(message)); + std::fflush(stderr); +} // Welcome to the wonderful world of MO2 plugin management! // @@ -446,9 +449,7 @@ QStringList PluginContainer::pluginFileNames() const std::vector proxyList = bf::at_key(m_Plugins); for (IPluginProxy* proxy : proxyList) { QStringList proxiedPlugins = proxy->pluginList( - m_PluginPath.isEmpty() - ? (AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath())) - : m_PluginPath); + m_PluginPath.isEmpty() ? AppConfig::pluginsPath() : m_PluginPath); result.append(proxiedPlugins); } return result; @@ -643,33 +644,31 @@ IPlugin* PluginContainer::registerPlugin(QObject* plugin, const QString& filepat return preview; } } - { // proxy plugins - IPluginProxy* proxy = qobject_cast(plugin); - if (initPlugin(proxy, pluginProxy, skipInit)) { - bf::at_key(m_Plugins).push_back(proxy); - emit pluginRegistered(proxy); - - const QString pluginRoot = - m_PluginPath.isEmpty() - ? (AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath())) - : m_PluginPath; - QStringList filepaths = proxy->pluginList(pluginRoot); - log::warn("proxy '{}' discovered {} proxied plugin candidate(s) in '{}'", - proxy->name(), filepaths.size(), - QDir::toNativeSeparators(pluginRoot)); - printPluginDiagToStderr( - QString("proxy '%1' discovered %2 proxied plugin candidate(s) in '%3'") - .arg(proxy->name()) - .arg(filepaths.size()) - .arg(QDir::toNativeSeparators(pluginRoot))); - for (const QString& filepath : filepaths) { - log::debug("proxy '{}' candidate: '{}'", proxy->name(), - QDir::toNativeSeparators(filepath)); - loadProxied(filepath, proxy); - } - return proxy; - } - } + { // proxy plugins + IPluginProxy* proxy = qobject_cast(plugin); + if (initPlugin(proxy, pluginProxy, skipInit)) { + bf::at_key(m_Plugins).push_back(proxy); + emit pluginRegistered(proxy); + + const QString pluginRoot = + m_PluginPath.isEmpty() ? AppConfig::pluginsPath() : m_PluginPath; + QStringList filepaths = proxy->pluginList(pluginRoot); + log::warn("proxy '{}' discovered {} proxied plugin candidate(s) in '{}'", + proxy->name(), filepaths.size(), + QDir::toNativeSeparators(pluginRoot)); + printPluginDiagToStderr( + QString("proxy '%1' discovered %2 proxied plugin candidate(s) in '%3'") + .arg(proxy->name()) + .arg(filepaths.size()) + .arg(QDir::toNativeSeparators(pluginRoot))); + for (const QString& filepath : filepaths) { + log::debug("proxy '{}' candidate: '{}'", proxy->name(), + QDir::toNativeSeparators(filepath)); + loadProxied(filepath, proxy); + } + return proxy; + } + } { // dummy plugins // only initialize these, no processing otherwise @@ -853,70 +852,70 @@ void PluginContainer::startPluginsImpl(const std::vector& plugins) con } } -std::vector PluginContainer::loadProxied(const QString& filepath, - IPluginProxy* proxy) -{ - std::vector proxiedPlugins; - - try { - log::warn("loading proxied plugin candidate '{}' via proxy '{}'", - QDir::toNativeSeparators(filepath), - (proxy ? proxy->name() : QStringLiteral(""))); - printPluginDiagToStderr( - QString("loading proxied plugin candidate '%1' via proxy '%2'") - .arg(QDir::toNativeSeparators(filepath)) - .arg(proxy ? proxy->name() : QStringLiteral(""))); - - // We get a list of matching plugins as proxies can return multiple plugins - // per file and do not have a good way of supporting multiple inheritance. - QList matchingPlugins = proxy->load(filepath); - if (matchingPlugins.isEmpty()) { - log::warn("no plugins were returned for proxied candidate '{}' via proxy '{}'", - QDir::toNativeSeparators(filepath), proxy->name()); - printPluginDiagToStderr( - QString("no plugins were returned for proxied candidate '%1' via proxy '%2'") - .arg(QDir::toNativeSeparators(filepath)) - .arg(proxy->name())); - } - - // We are going to group plugin by names and "fix" them later: - std::map> proxiedByNames; - - for (QObject* proxiedPlugin : matchingPlugins) { - if (proxiedPlugin == nullptr) { - log::warn("proxy '{}' returned a null QObject for '{}'", proxy->name(), - QDir::toNativeSeparators(filepath)); - printPluginDiagToStderr( - QString("proxy '%1' returned a null QObject for '%2'") - .arg(proxy->name()) - .arg(QDir::toNativeSeparators(filepath))); - continue; - } - - if (IPlugin* proxied = registerPlugin(proxiedPlugin, filepath, proxy); proxied) { - log::debug("loaded plugin '{}@{}' from '{}' - [{}]", proxied->name(), - proxied->version().canonicalString(), - QFileInfo(filepath).fileName(), - implementedInterfaces(proxied).join(", ")); - - // Store the plugin for later: - proxiedPlugins.push_back(proxiedPlugin); - proxiedByNames[proxied->name()].push_back(proxied); - } else { - log::warn( - "proxied candidate '{}' from proxy '{}' failed to register as an MO2 plugin", - QDir::toNativeSeparators(filepath), proxy->name()); - printPluginDiagToStderr( - QString("proxied candidate '%1' from proxy '%2' failed to register as an " - "MO2 plugin") - .arg(QDir::toNativeSeparators(filepath)) - .arg(proxy->name())); - } - } +std::vector PluginContainer::loadProxied(const QString& filepath, + IPluginProxy* proxy) +{ + std::vector proxiedPlugins; + + try { + log::warn("loading proxied plugin candidate '{}' via proxy '{}'", + QDir::toNativeSeparators(filepath), + (proxy ? proxy->name() : QStringLiteral(""))); + printPluginDiagToStderr( + QString("loading proxied plugin candidate '%1' via proxy '%2'") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy ? proxy->name() : QStringLiteral(""))); + + // We get a list of matching plugins as proxies can return multiple plugins + // per file and do not have a good way of supporting multiple inheritance. + QList matchingPlugins = proxy->load(filepath); + if (matchingPlugins.isEmpty()) { + log::warn("no plugins were returned for proxied candidate '{}' via proxy '{}'", + QDir::toNativeSeparators(filepath), proxy->name()); + printPluginDiagToStderr( + QString("no plugins were returned for proxied candidate '%1' via proxy '%2'") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy->name())); + } + + // We are going to group plugin by names and "fix" them later: + std::map> proxiedByNames; + + for (QObject* proxiedPlugin : matchingPlugins) { + if (proxiedPlugin == nullptr) { + log::warn("proxy '{}' returned a null QObject for '{}'", proxy->name(), + QDir::toNativeSeparators(filepath)); + printPluginDiagToStderr( + QString("proxy '%1' returned a null QObject for '%2'") + .arg(proxy->name()) + .arg(QDir::toNativeSeparators(filepath))); + continue; + } + + if (IPlugin* proxied = registerPlugin(proxiedPlugin, filepath, proxy); proxied) { + log::debug("loaded plugin '{}@{}' from '{}' - [{}]", proxied->name(), + proxied->version().canonicalString(), + QFileInfo(filepath).fileName(), + implementedInterfaces(proxied).join(", ")); + + // Store the plugin for later: + proxiedPlugins.push_back(proxiedPlugin); + proxiedByNames[proxied->name()].push_back(proxied); + } else { + log::warn( + "proxied candidate '{}' from proxy '{}' failed to register as an MO2 plugin", + QDir::toNativeSeparators(filepath), proxy->name()); + printPluginDiagToStderr( + QString("proxied candidate '%1' from proxy '%2' failed to register as an " + "MO2 plugin") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy->name())); + } + } // Fake masters: - for (auto& [name, proxiedPlugins] : proxiedByNames) { - if (proxiedPlugins.size() > 1) { + for (auto& [name, proxiedPlugins] : proxiedByNames) { + if (proxiedPlugins.size() > 1) { auto it = std::min_element(std::begin(proxiedPlugins), std::end(proxiedPlugins), [&](auto const& lhs, auto const& rhs) { return isBetterInterface(as_qobject(lhs), @@ -928,43 +927,43 @@ std::vector PluginContainer::loadProxied(const QString& filepath, m_Requirements.at(proxiedPlugin).setMaster(*it); } } - } - } - log::warn("finished proxied candidate '{}' via proxy '{}': {} plugin(s) loaded", - QDir::toNativeSeparators(filepath), proxy->name(), - proxiedPlugins.size()); - printPluginDiagToStderr( - QString("finished proxied candidate '%1' via proxy '%2': %3 plugin(s) loaded") - .arg(QDir::toNativeSeparators(filepath)) - .arg(proxy->name()) - .arg(proxiedPlugins.size())); - } catch (const std::exception& e) { - log::error("failed to initialize proxied candidate '{}' via proxy '{}': {}", - QDir::toNativeSeparators(filepath), - (proxy ? proxy->name() : QStringLiteral("")), e.what()); - printPluginDiagToStderr( - QString("failed to initialize proxied candidate '%1' via proxy '%2': %3") - .arg(QDir::toNativeSeparators(filepath)) - .arg(proxy ? proxy->name() : QStringLiteral("")) - .arg(e.what())); - reportError( - QObject::tr("failed to initialize plugin %1: %2").arg(filepath).arg(e.what())); - } catch (...) { - log::error("failed to initialize proxied candidate '{}' via proxy '{}': " - "unknown exception", - QDir::toNativeSeparators(filepath), - (proxy ? proxy->name() : QStringLiteral(""))); - printPluginDiagToStderr( - QString("failed to initialize proxied candidate '%1' via proxy '%2': unknown " - "exception") - .arg(QDir::toNativeSeparators(filepath)) - .arg(proxy ? proxy->name() : QStringLiteral(""))); - reportError(QObject::tr("failed to initialize plugin %1: unknown exception") - .arg(filepath)); - } - - return proxiedPlugins; -} + } + } + log::warn("finished proxied candidate '{}' via proxy '{}': {} plugin(s) loaded", + QDir::toNativeSeparators(filepath), proxy->name(), + proxiedPlugins.size()); + printPluginDiagToStderr( + QString("finished proxied candidate '%1' via proxy '%2': %3 plugin(s) loaded") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy->name()) + .arg(proxiedPlugins.size())); + } catch (const std::exception& e) { + log::error("failed to initialize proxied candidate '{}' via proxy '{}': {}", + QDir::toNativeSeparators(filepath), + (proxy ? proxy->name() : QStringLiteral("")), e.what()); + printPluginDiagToStderr( + QString("failed to initialize proxied candidate '%1' via proxy '%2': %3") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy ? proxy->name() : QStringLiteral("")) + .arg(e.what())); + reportError( + QObject::tr("failed to initialize plugin %1: %2").arg(filepath).arg(e.what())); + } catch (...) { + log::error("failed to initialize proxied candidate '{}' via proxy '{}': " + "unknown exception", + QDir::toNativeSeparators(filepath), + (proxy ? proxy->name() : QStringLiteral(""))); + printPluginDiagToStderr( + QString("failed to initialize proxied candidate '%1' via proxy '%2': unknown " + "exception") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy ? proxy->name() : QStringLiteral(""))); + reportError(QObject::tr("failed to initialize plugin %1: unknown exception") + .arg(filepath)); + } + + return proxiedPlugins; +} QObject* PluginContainer::loadQtPlugin(const QString& filepath) { @@ -1033,9 +1032,7 @@ void PluginContainer::loadPlugin(QString const& filepath) // We need to check if this can be handled by a proxy. for (auto* proxy : this->plugins()) { auto filepaths = proxy->pluginList( - m_PluginPath.isEmpty() - ? (AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath())) - : m_PluginPath); + m_PluginPath.isEmpty() ? AppConfig::pluginsPath() : m_PluginPath); if (filepaths.contains(filepath)) { plugins = loadProxied(filepath, proxy); break; @@ -1247,14 +1244,13 @@ void PluginContainer::loadPlugins() loadCheck.close(); } - if (!loadCheck.open(QIODevice::WriteOnly)) { - log::warn("failed to open loadcheck file for writing '{}'", - QDir::toNativeSeparators(loadCheck.fileName())); - } + if (!loadCheck.open(QIODevice::WriteOnly)) { + log::warn("failed to open loadcheck file for writing '{}'", + QDir::toNativeSeparators(loadCheck.fileName())); + } } - QString pluginPath = - AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath()); + QString pluginPath = AppConfig::pluginsPath(); #ifndef _WIN32 // Per-instance plugin directory: symlink bundled plugins, then load from there. @@ -1334,15 +1330,15 @@ void PluginContainer::loadPlugins() } log::warn("user skipped plugin '{}', remembering in loadcheck", skipPlugin); - if (loadCheck.open(QIODevice::WriteOnly)) { - loadCheck.write(skipPlugin.toUtf8()); - loadCheck.write("\n"); - loadCheck.flush(); - } else { - log::warn("failed to persist skipped plugin to '{}'", - QDir::toNativeSeparators(loadCheck.fileName())); - } - } + if (loadCheck.open(QIODevice::WriteOnly)) { + loadCheck.write(skipPlugin.toUtf8()); + loadCheck.write("\n"); + loadCheck.flush(); + } else { + log::warn("failed to persist skipped plugin to '{}'", + QDir::toNativeSeparators(loadCheck.fileName())); + } + } bf::at_key(m_Plugins).push_back(this); diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp index e7354f3..892fd79 100644 --- a/src/src/processrunner.cpp +++ b/src/src/processrunner.cpp @@ -22,43 +22,45 @@ #include #include #include +#include #include #include -#include + #endif using namespace MOBase; -void adjustForVirtualized(const IPluginGame* game, spawn::SpawnParameters& sp, - const Settings& settings) -{ +void adjustForVirtualized(const IPluginGame *game, spawn::SpawnParameters &sp, + const Settings &settings) { const QString modsPath = settings.paths().mods(); // Check if this a request with either an executable or a working directory // under our mods folder then will start the process in a virtualized // "environment" with the appropriate paths fixed: // (i.e. mods\FNIS\path\exe => game\data\path\exe) - QString cwdPath = sp.currentDirectory.absolutePath(); + QString cwdPath = sp.currentDirectory.absolutePath(); QString trailedModsPath = modsPath; if (!trailedModsPath.endsWith('/')) { trailedModsPath = trailedModsPath + '/'; } - bool virtualizedCwd = cwdPath.startsWith(trailedModsPath, Qt::CaseInsensitive); - QString binPath = sp.binary.absoluteFilePath(); - bool virtualizedBin = binPath.startsWith(trailedModsPath, Qt::CaseInsensitive); + bool virtualizedCwd = + cwdPath.startsWith(trailedModsPath, Qt::CaseInsensitive); + QString binPath = sp.binary.absoluteFilePath(); + bool virtualizedBin = + binPath.startsWith(trailedModsPath, Qt::CaseInsensitive); if (virtualizedCwd || virtualizedBin) { if (virtualizedCwd) { - int cwdOffset = cwdPath.indexOf('/', trailedModsPath.length()); + int cwdOffset = cwdPath.indexOf('/', trailedModsPath.length()); QString adjustedCwd = cwdPath.mid(cwdOffset, -1); - cwdPath = game->dataDirectory().absolutePath(); + cwdPath = game->dataDirectory().absolutePath(); if (cwdOffset >= 0) cwdPath += adjustedCwd; } if (virtualizedBin) { - int binOffset = binPath.indexOf('/', trailedModsPath.length()); + int binOffset = binPath.indexOf('/', trailedModsPath.length()); QString adjustedBin = binPath.mid(binOffset, -1); - binPath = game->dataDirectory().absolutePath(); + binPath = game->dataDirectory().absolutePath(); if (binOffset >= 0) binPath += adjustedBin; } @@ -69,7 +71,7 @@ void adjustForVirtualized(const IPluginGame* game, spawn::SpawnParameters& sp, .arg(QDir::toNativeSeparators(cwdPath), QDir::toNativeSeparators(binPath), sp.arguments); - sp.binary = QFileInfo(QCoreApplication::applicationFilePath()); + sp.binary = QFileInfo(QCoreApplication::applicationFilePath()); sp.arguments = cmdline; sp.currentDirectory.setPath(QCoreApplication::applicationDirPath()); #else @@ -97,8 +99,7 @@ void adjustForVirtualized(const IPluginGame* game, spawn::SpawnParameters& sp, } #ifdef _WIN32 -std::optional singleWait(HANDLE handle, DWORD pid) -{ +std::optional singleWait(HANDLE handle, DWORD pid) { if (handle == INVALID_HANDLE_VALUE) { return ProcessRunner::Error; } @@ -116,7 +117,7 @@ std::optional singleWait(HANDLE handle, DWORD pid) return {}; } - case WAIT_FAILED: // fall-through + case WAIT_FAILED: // fall-through default: { // error const auto e = ::GetLastError(); @@ -126,23 +127,16 @@ std::optional singleWait(HANDLE handle, DWORD pid) } } #else -std::optional singleWait(HANDLE handle, DWORD pid) -{ +std::optional singleWait(HANDLE handle, DWORD pid) { Q_UNUSED(handle); Q_UNUSED(pid); return ProcessRunner::Completed; } #endif -enum class Interest -{ - None = 0, - Weak, - Strong -}; +enum class Interest { None = 0, Weak, Strong }; -QString toString(Interest i) -{ +QString toString(Interest i) { switch (i) { case Interest::Weak: return "weak"; @@ -150,22 +144,20 @@ QString toString(Interest i) case Interest::Strong: return "strong"; - case Interest::None: // fall-through + case Interest::None: // fall-through default: return "no"; } } -struct InterestingProcess -{ +struct InterestingProcess { env::Process p; Interest interest = Interest::None; env::HandlePtr handle; }; -InterestingProcess findRandomProcess(const env::Process& root) -{ - for (auto&& c : root.children()) { +InterestingProcess findRandomProcess(const env::Process &root) { + for (auto &&c : root.children()) { env::HandlePtr h = c.openHandleForWait(); if (h) { return {c, Interest::Weak, std::move(h)}; @@ -183,18 +175,18 @@ InterestingProcess findRandomProcess(const env::Process& root) // returns a process that's in the hidden list, or the top-level process if // they're all hidden; returns an invalid process if the list is empty // -InterestingProcess findInterestingProcessInTrees(const env::Process& root) -{ +InterestingProcess findInterestingProcessInTrees(const env::Process &root) { // Certain process names we wish to "hide" for aesthetic reason: static const std::vector hiddenList = { - QFileInfo(QCoreApplication::applicationFilePath()).fileName(), "conhost.exe"}; + QFileInfo(QCoreApplication::applicationFilePath()).fileName(), + "conhost.exe"}; if (root.children().empty()) { return {}; } - auto isHidden = [&](auto&& p) { - for (auto& h : hiddenList) { + auto isHidden = [&](auto &&p) { + for (auto &h : hiddenList) { if (p.name().contains(h, Qt::CaseInsensitive)) { return true; } @@ -203,7 +195,7 @@ InterestingProcess findInterestingProcessInTrees(const env::Process& root) return false; }; - for (auto&& p : root.children()) { + for (auto &&p : root.children()) { if (!isHidden(p)) { env::HandlePtr h = p.openHandleForWait(); if (h) { @@ -221,21 +213,19 @@ InterestingProcess findInterestingProcessInTrees(const env::Process& root) return findRandomProcess(root); } -void dump(const env::Process& p, int indent) -{ - log::debug("{}{}, pid={}, ppid={}", std::string(indent * 4, ' '), p.name(), p.pid(), - p.ppid()); +void dump(const env::Process &p, int indent) { + log::debug("{}{}, pid={}, ppid={}", std::string(indent * 4, ' '), p.name(), + p.pid(), p.ppid()); - for (auto&& c : p.children()) { + for (auto &&c : p.children()) { dump(c, indent + 1); } } -void dump(const env::Process& root) -{ +void dump(const env::Process &root) { log::debug("process tree:"); - for (auto&& p : root.children()) { + for (auto &&p : root.children()) { dump(p, 1); } } @@ -243,8 +233,7 @@ void dump(const env::Process& root) #ifdef _WIN32 // gets the most interesting process in the list // -InterestingProcess getInterestingProcess(HANDLE job) -{ +InterestingProcess getInterestingProcess(HANDLE job) { env::Process root = env::getProcessTree(job); if (root.children().empty()) { log::debug("nothing to wait for"); @@ -270,10 +259,9 @@ const std::chrono::milliseconds Infinite(-1); // waits for completion, times out after `wait` if not Infinite // std::optional timedWait(HANDLE handle, DWORD pid, - UILocker::Session* ls, + UILocker::Session *ls, std::chrono::milliseconds wait, - std::atomic& interrupt) -{ + std::atomic &interrupt) { using namespace std::chrono; high_resolution_clock::time_point start; @@ -310,7 +298,7 @@ std::optional timedWait(HANDLE handle, DWORD pid, return ProcessRunner::Cancelled; } - case UILocker::NoResult: // fall-through + case UILocker::NoResult: // fall-through default: { // shouldn't happen log::debug("unexpected result {} while waiting for {}", @@ -335,9 +323,9 @@ std::optional timedWait(HANDLE handle, DWORD pid, return ProcessRunner::ForceUnlocked; } -ProcessRunner::Results waitForProcessesThreadImpl(HANDLE job, UILocker::Session* ls, - std::atomic& interrupt) -{ +ProcessRunner::Results +waitForProcessesThreadImpl(HANDLE job, UILocker::Session *ls, + std::atomic &interrupt) { using namespace std::chrono; DWORD currentPID = 0; @@ -395,9 +383,9 @@ ProcessRunner::Results waitForProcessesThreadImpl(HANDLE job, UILocker::Session* return ProcessRunner::ForceUnlocked; } -void waitForProcessesThread(ProcessRunner::Results& result, HANDLE job, - UILocker::Session* ls, std::atomic& interrupt) -{ +void waitForProcessesThread(ProcessRunner::Results &result, HANDLE job, + UILocker::Session *ls, + std::atomic &interrupt) { result = waitForProcessesThreadImpl(job, ls, interrupt); // the session can be null when running shortcuts with locking disabled @@ -406,9 +394,9 @@ void waitForProcessesThread(ProcessRunner::Results& result, HANDLE job, } } -ProcessRunner::Results waitForProcesses(const std::vector& initialProcesses, - UILocker::Session* ls) -{ +ProcessRunner::Results +waitForProcesses(const std::vector &initialProcesses, + UILocker::Session *ls) { if (initialProcesses.empty()) { // nothing to wait for return ProcessRunner::Completed; @@ -428,7 +416,7 @@ ProcessRunner::Results waitForProcesses(const std::vector& initialProces bool oneWorked = false; - for (auto&& h : initialProcesses) { + for (auto &&h : initialProcesses) { if (::AssignProcessToJobObject(job.get(), h)) { oneWorked = true; } else { @@ -457,13 +445,11 @@ ProcessRunner::Results waitForProcesses(const std::vector& initialProces auto results = ProcessRunner::Running; std::atomic interrupt(false); - auto* t = QThread::create(waitForProcessesThread, std::ref(results), monitor, ls, - std::ref(interrupt)); + auto *t = QThread::create(waitForProcessesThread, std::ref(results), monitor, + ls, std::ref(interrupt)); QEventLoop events; - QObject::connect(t, &QThread::finished, [&] { - events.quit(); - }); + QObject::connect(t, &QThread::finished, [&] { events.quit(); }); t->start(); events.exec(); @@ -479,8 +465,7 @@ ProcessRunner::Results waitForProcesses(const std::vector& initialProces } ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode, - UILocker::Session* ls) -{ + UILocker::Session *ls) { std::vector processes = {initialProcess}; const auto r = waitForProcesses(processes, ls); @@ -489,7 +474,8 @@ ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode, if (exitCode && r != ProcessRunner::Running) { if (!::GetExitCodeProcess(initialProcess, exitCode)) { const auto e = ::GetLastError(); - log::warn("failed to get exit code of process, {}", formatSystemMessage(e)); + log::warn("failed to get exit code of process, {}", + formatSystemMessage(e)); } } @@ -498,13 +484,11 @@ ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode, #else // !_WIN32 -pid_t handleToPid(HANDLE h) -{ +pid_t handleToPid(HANDLE h) { return static_cast(reinterpret_cast(h)); } -QString readProcComm(pid_t pid) -{ +QString readProcComm(pid_t pid) { QFile f(QString("/proc/%1/comm").arg(pid)); if (!f.open(QIODevice::ReadOnly)) { return {}; @@ -513,21 +497,20 @@ QString readProcComm(pid_t pid) return QString::fromUtf8(f.readAll()).trimmed(); } -std::unordered_map> buildProcChildrenMap() -{ +std::unordered_map> buildProcChildrenMap() { std::unordered_map> children; - DIR* proc = opendir("/proc"); + DIR *proc = opendir("/proc"); if (!proc) { return children; } - struct dirent* entry = nullptr; + struct dirent *entry = nullptr; while ((entry = readdir(proc)) != nullptr) { if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) { continue; } - const char* name = entry->d_name; + const char *name = entry->d_name; if (*name == '\0' || !std::isdigit(static_cast(*name))) { continue; } @@ -556,9 +539,8 @@ std::unordered_map> buildProcChildrenMap() return children; } -std::unordered_set -collectDescendants(pid_t root, const std::unordered_map>& children) -{ +std::unordered_set collectDescendants( + pid_t root, const std::unordered_map> &children) { std::unordered_set out; std::deque q; q.push_back(root); @@ -582,8 +564,8 @@ collectDescendants(pid_t root, const std::unordered_map 0 && trackedNameOut) { *trackedNameOut = bestName; + log::debug("findTrackedProcess: found best={}, name={}", best, + bestName.toStdString()); + } else { + log::debug("findTrackedProcess: no tracked process found for rootPid={}", + rootPid); } return best; } -DWORD exitCodeFromWaitStatus(int status) -{ +DWORD exitCodeFromWaitStatus(int status) { if (WIFEXITED(status)) { return static_cast(WEXITSTATUS(status)); } @@ -653,9 +640,9 @@ DWORD exitCodeFromWaitStatus(int status) return 0; } -ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session* ls, - const QStringList& expected) -{ +ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, + UILocker::Session *ls, + const QStringList &expected) { if (pid <= 0) { return ProcessRunner::Error; } @@ -665,10 +652,9 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session // which works for any process owned by the same user. bool useKillPoll = false; { - int status = 0; + int status = 0; const pid_t probe = ::waitpid(pid, &status, WNOHANG); if (probe == pid) { - // Already exited before we even started waiting if (exitCode != nullptr) { *exitCode = exitCodeFromWaitStatus(status); } @@ -686,13 +672,13 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session while (true) { QString trackedName; - pid_t displayPid = pid; - QString displayName = readProcComm(pid); - const pid_t tracked = findTrackedProcess(pid, expected, &trackedName); + pid_t displayPid = pid; + QString displayName = readProcComm(pid); + const pid_t tracked = findTrackedProcess(pid, expected, &trackedName); if (tracked > 0) { seenTrackedProcess = true; - displayPid = tracked; - displayName = trackedName; + displayPid = tracked; + displayName = trackedName; } else if (seenTrackedProcess) { if (exitCode != nullptr) { *exitCode = 0; @@ -702,7 +688,8 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session } if (ls != nullptr) { - ls->setInfo(static_cast(std::max(0, displayPid)), displayName); + ls->setInfo(static_cast(std::max(0, displayPid)), + displayName); } if (useKillPoll) { @@ -716,13 +703,13 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session return ProcessRunner::Completed; } // EPERM means the process exists but we can't signal it; keep waiting - if (errno != EPERM) { + else if (errno != EPERM) { log::error("failed checking process {}, errno={}", pid, errno); return ProcessRunner::Error; } } } else { - int status = 0; + int status = 0; const pid_t waitResult = ::waitpid(pid, &status, WNOHANG); if (waitResult == pid) { @@ -776,23 +763,21 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session } ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode, - UILocker::Session* ls, - const QStringList& expected) -{ + UILocker::Session *ls, + const QStringList &expected) { return waitForPid(handleToPid(initialProcess), exitCode, ls, expected); } -ProcessRunner::Results waitForProcesses(const std::vector& initialProcesses, - UILocker::Session* ls, - const QStringList& expected) -{ +ProcessRunner::Results +waitForProcesses(const std::vector &initialProcesses, + UILocker::Session *ls, const QStringList &expected) { if (initialProcesses.empty()) { return ProcessRunner::Completed; } for (HANDLE h : initialProcesses) { DWORD ignored = 0; - const auto r = waitForPid(handleToPid(h), &ignored, ls, expected); + const auto r = waitForPid(handleToPid(h), &ignored, ls, expected); if (r != ProcessRunner::Completed) { return r; } @@ -803,16 +788,14 @@ ProcessRunner::Results waitForProcesses(const std::vector& initialProces #endif // _WIN32 -ProcessRunner::ProcessRunner(OrganizerCore& core, IUserInterface* ui) - : m_core(core), m_ui(ui), m_lockReason(UILocker::NoReason), m_waitFlags(NoFlags), - m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1) -{ +ProcessRunner::ProcessRunner(OrganizerCore &core, IUserInterface *ui) + : m_core(core), m_ui(ui), m_lockReason(UILocker::NoReason), + m_waitFlags(NoFlags), m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1) { // all processes started in ProcessRunner are hooked by default setHooked(true); } -ProcessRunner& ProcessRunner::setBinary(const QFileInfo& binary) -{ +ProcessRunner &ProcessRunner::setBinary(const QFileInfo &binary) { #ifndef _WIN32 m_sp.binary = QFileInfo(MOBase::normalizePathForHost(binary.filePath())); #else @@ -821,14 +804,12 @@ ProcessRunner& ProcessRunner::setBinary(const QFileInfo& binary) return *this; } -ProcessRunner& ProcessRunner::setArguments(const QString& arguments) -{ +ProcessRunner &ProcessRunner::setArguments(const QString &arguments) { m_sp.arguments = arguments; return *this; } -ProcessRunner& ProcessRunner::setCurrentDirectory(const QDir& directory) -{ +ProcessRunner &ProcessRunner::setCurrentDirectory(const QDir &directory) { #ifndef _WIN32 m_sp.currentDirectory.setPath(MOBase::normalizePathForHost(directory.path())); #else @@ -837,37 +818,35 @@ ProcessRunner& ProcessRunner::setCurrentDirectory(const QDir& directory) return *this; } -ProcessRunner& ProcessRunner::setSteamID(const QString& steamID) -{ +ProcessRunner &ProcessRunner::setSteamID(const QString &steamID) { m_sp.steamAppID = steamID; return *this; } -ProcessRunner& ProcessRunner::setCustomOverwrite(const QString& customOverwrite) -{ +ProcessRunner & +ProcessRunner::setCustomOverwrite(const QString &customOverwrite) { m_customOverwrite = customOverwrite; return *this; } -ProcessRunner& ProcessRunner::setForcedLibraries(const ForcedLibraries& forcedLibraries) -{ +ProcessRunner & +ProcessRunner::setForcedLibraries(const ForcedLibraries &forcedLibraries) { m_forcedLibraries = forcedLibraries; return *this; } -ProcessRunner& ProcessRunner::setProfileName(const QString& profileName) -{ +ProcessRunner &ProcessRunner::setProfileName(const QString &profileName) { m_profileName = profileName; return *this; } -ProcessRunner& ProcessRunner::setWaitForCompletion(WaitFlags flags, - UILocker::Reasons reason) -{ - m_waitFlags = flags; +ProcessRunner &ProcessRunner::setWaitForCompletion(WaitFlags flags, + UILocker::Reasons reason) { + m_waitFlags = flags; m_lockReason = reason; - if (m_waitFlags.testFlag(WaitForRefresh) && !m_waitFlags.testFlag(TriggerRefresh)) { + if (m_waitFlags.testFlag(WaitForRefresh) && + !m_waitFlags.testFlag(TriggerRefresh)) { log::warn("process runner: WaitForRefresh without TriggerRefresh " "makes no sense, will be ignored"); } @@ -875,14 +854,13 @@ ProcessRunner& ProcessRunner::setWaitForCompletion(WaitFlags flags, return *this; } -ProcessRunner& ProcessRunner::setHooked(bool b) -{ +ProcessRunner &ProcessRunner::setHooked(bool b) { m_sp.hooked = b; return *this; } -ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targetInfo) -{ +ProcessRunner &ProcessRunner::setFromFile(QWidget *parent, + const QFileInfo &targetInfo) { if (!parent && m_ui) { parent = m_ui->mainWindow(); } @@ -900,7 +878,7 @@ ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targ break; } - case spawn::FileExecutionTypes::Other: // fall-through + case spawn::FileExecutionTypes::Other: // fall-through default: { m_shellOpen = targetInfo; setHooked(false); @@ -911,8 +889,7 @@ ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targ return *this; } -ProcessRunner& ProcessRunner::setFromExecutable(const Executable& exe) -{ +ProcessRunner &ProcessRunner::setFromExecutable(const Executable &exe) { const auto profile = m_core.currentProfile(); if (!profile) { throw MyException(QObject::tr("No profile set")); @@ -941,33 +918,33 @@ ProcessRunner& ProcessRunner::setFromExecutable(const Executable& exe) return *this; } -ProcessRunner& ProcessRunner::setFromShortcut(const MOShortcut& shortcut) -{ +ProcessRunner &ProcessRunner::setFromShortcut(const MOShortcut &shortcut) { const auto currentInstance = InstanceManager::singleton().currentInstance(); if (currentInstance) { if (shortcut.hasInstance() && !shortcut.isForInstance(*currentInstance)) { - MOBase::reportError( - QObject::tr( - "This shortcut is for instance '%1' but Mod Organizer is currently " - "running for '%2'. Exit Mod Organizer before running the shortcut or " - "change the active instance.") - .arg(shortcut.instanceDisplayName()) - .arg(currentInstance->displayName())); + MOBase::reportError(QObject::tr("This shortcut is for instance '%1' but " + "Mod Organizer is currently " + "running for '%2'. Exit Mod Organizer " + "before running the shortcut or " + "change the active instance.") + .arg(shortcut.instanceDisplayName()) + .arg(currentInstance->displayName())); throw std::exception(); } } - const auto* exes = m_core.executablesList(); - const auto exe = exes->find(shortcut.executableName()); + const auto *exes = m_core.executablesList(); + const auto exe = exes->find(shortcut.executableName()); if (exe != exes->end()) { setFromExecutable(*exe); } else { - MOBase::reportError(QObject::tr("Executable '%1' does not exist in instance '%2'.") - .arg(shortcut.executableName()) - .arg(currentInstance->displayName())); + MOBase::reportError( + QObject::tr("Executable '%1' does not exist in instance '%2'.") + .arg(shortcut.executableName()) + .arg(currentInstance->displayName())); throw std::exception(); } @@ -975,11 +952,10 @@ ProcessRunner& ProcessRunner::setFromShortcut(const MOShortcut& shortcut) return *this; } -ProcessRunner& ProcessRunner::setFromFileOrExecutable( - const QString& executable, const QStringList& args, const QString& cwd, - const QString& profileOverride, const QString& forcedCustomOverwrite, - bool ignoreCustomOverwrite) -{ +ProcessRunner &ProcessRunner::setFromFileOrExecutable( + const QString &executable, const QStringList &args, const QString &cwd, + const QString &profileOverride, const QString &forcedCustomOverwrite, + bool ignoreCustomOverwrite) { const auto profile = m_core.currentProfile(); if (!profile) { throw MyException(QObject::tr("No profile set")); @@ -1004,24 +980,27 @@ ProcessRunner& ProcessRunner::setFromFileOrExecutable( } try { - const Executable& exe = m_core.executablesList()->getByBinary(m_sp.binary); + const Executable &exe = + m_core.executablesList()->getByBinary(m_sp.binary); setSteamID(exe.steamAppID()); - setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString()); + setCustomOverwrite( + profile->setting("custom_overwrites", exe.title()).toString()); if (profile->forcedLibrariesEnabled(exe.title())) { setForcedLibraries(profile->determineForcedLibraries(exe.title())); } - } catch (const std::runtime_error&) { + } catch (const std::runtime_error &) { // nop } } else { // only a file name, search executables list try { - const Executable& exe = m_core.executablesList()->get(executable); + const Executable &exe = m_core.executablesList()->get(executable); setSteamID(exe.steamAppID()); - setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString()); + setCustomOverwrite( + profile->setting("custom_overwrites", exe.title()).toString()); if (profile->forcedLibrariesEnabled(exe.title())) { setForcedLibraries(profile->determineForcedLibraries(exe.title())); @@ -1036,7 +1015,7 @@ ProcessRunner& ProcessRunner::setFromFileOrExecutable( if (cwd == "") { setCurrentDirectory(exe.workingDirectory()); } - } catch (const std::runtime_error&) { + } catch (const std::runtime_error &) { log::warn("\"{}\" not set up as executable", executable); } } @@ -1050,13 +1029,11 @@ ProcessRunner& ProcessRunner::setFromFileOrExecutable( return *this; } -bool ProcessRunner::shouldRunShell() const -{ +bool ProcessRunner::shouldRunShell() const { return !m_shellOpen.filePath().isEmpty(); } -ProcessRunner::Results ProcessRunner::run() -{ +ProcessRunner::Results ProcessRunner::run() { // check if setHooked() was called after setFromFile(); this needs to // modify the settings to run the associated executable instead of using // shell::Open() @@ -1097,9 +1074,9 @@ ProcessRunner::Results ProcessRunner::run() return postRun(); } -std::optional ProcessRunner::runShell() -{ - const auto file = MOBase::normalizePathForHost(m_shellOpen.absoluteFilePath()); +std::optional ProcessRunner::runShell() { + const auto file = + MOBase::normalizePathForHost(m_shellOpen.absoluteFilePath()); log::debug("executing from shell: '{}'", file); @@ -1122,8 +1099,7 @@ std::optional ProcessRunner::runShell() return {}; } -std::optional ProcessRunner::runBinary() -{ +std::optional ProcessRunner::runBinary() { if (m_profileName.isEmpty()) { // get the current profile name if it wasn't overridden const auto profile = m_core.currentProfile(); @@ -1143,10 +1119,10 @@ std::optional ProcessRunner::runBinary() } // parent widget used for any dialog popped up while checking for things - QWidget* parent = (m_ui ? m_ui->mainWindow() : nullptr); + QWidget *parent = (m_ui ? m_ui->mainWindow() : nullptr); - const auto* game = m_core.managedGame(); - auto& settings = m_core.settings(); + const auto *game = m_core.managedGame(); + auto &settings = m_core.settings(); if (m_sp.steamAppID.trimmed().isEmpty()) { const QString gameSteamId = game->steamAPPId().trimmed(); @@ -1158,7 +1134,8 @@ std::optional ProcessRunner::runBinary() } // start steam if needed - if (!checkSteam(parent, m_sp, game->gameDirectory(), m_sp.steamAppID, settings)) { + if (!checkSteam(parent, m_sp, game->gameDirectory(), m_sp.steamAppID, + settings)) { return Error; } @@ -1175,7 +1152,8 @@ std::optional ProcessRunner::runBinary() #ifdef _WIN32 m_handle.reset(startBinary(parent, m_sp)); #else - m_handle.reset(reinterpret_cast(static_cast(startBinary(parent, m_sp)))); + m_handle.reset(reinterpret_cast( + static_cast(startBinary(parent, m_sp)))); #endif if (m_handle.get() == INVALID_HANDLE_VALUE) { return Error; @@ -1184,8 +1162,7 @@ std::optional ProcessRunner::runBinary() return {}; } -bool ProcessRunner::shouldRefresh(Results r) const -{ +bool ProcessRunner::shouldRefresh(Results r) const { // afterRun() is only called with the Refresh flag; it refreshes the // directory structure and notifies plugins // @@ -1211,11 +1188,12 @@ bool ProcessRunner::shouldRefresh(Results r) const case ForceUnlocked: { // The process may still be running when the user force-unlocks. // Refreshing in that state can race with file updates. - log::debug("process runner: not refreshing because the ui was force unlocked"); + log::debug( + "process runner: not refreshing because the ui was force unlocked"); return false; } - case Error: // fall-through + case Error: // fall-through case Cancelled: case Running: default: { @@ -1224,8 +1202,7 @@ bool ProcessRunner::shouldRefresh(Results r) const } } -ProcessRunner::Results ProcessRunner::postRun() -{ +ProcessRunner::Results ProcessRunner::postRun() { const bool mustWait = (m_waitFlags & ForceWait); if (!m_sp.hooked && !mustWait) { @@ -1248,8 +1225,9 @@ ProcessRunner::Results ProcessRunner::postRun() if (mustWait) { if (!lockEnabled) { // at least tell the user what's going on - log::debug("locking is disabled, but the output of the application is required; " - "overriding this setting and locking the ui"); + log::debug( + "locking is disabled, but the output of the application is required; " + "overriding this setting and locking the ui"); } } else { // no force wait @@ -1257,10 +1235,12 @@ ProcessRunner::Results ProcessRunner::postRun() if (m_lockReason == UILocker::NoReason) { // no locking requested #ifndef _WIN32 - // Main window launches typically use TriggerRefresh without waiting/locking. - // In that mode we still need post-run refresh/sync once the process exits. + // Main window launches typically use TriggerRefresh without + // waiting/locking. In that mode we still need post-run refresh/sync once + // the process exits. if (m_waitFlags.testFlag(TriggerRefresh)) { - const pid_t pid = static_cast(reinterpret_cast(m_handle.get())); + const pid_t pid = + static_cast(reinterpret_cast(m_handle.get())); const QFileInfo binary = m_sp.binary; QPointer core = &m_core; @@ -1283,11 +1263,11 @@ ProcessRunner::Results ProcessRunner::postRun() } else if (errno == ECHILD) { // Detached process — poll with kill(0) until gone. while (::kill(pid, 0) == 0 || errno == EPERM) { - usleep(200000); // 200ms + usleep(200000); // 200ms } } else { - MOBase::log::warn("process runner: waitpid failed for pid {}: {}", pid, - errno); + MOBase::log::warn("process runner: waitpid failed for pid {}: {}", + pid, errno); } if (!core) { @@ -1295,7 +1275,8 @@ ProcessRunner::Results ProcessRunner::postRun() } QMetaObject::invokeMethod( - core, [core, binary, exitCode]() { + core, + [core, binary, exitCode]() { if (core) { core->afterRun(binary, exitCode); } @@ -1303,7 +1284,8 @@ ProcessRunner::Results ProcessRunner::postRun() Qt::QueuedConnection); }).detach(); - log::debug("process runner: scheduled async post-run refresh for pid {}", pid); + log::debug( + "process runner: scheduled async post-run refresh for pid {}", pid); } #endif return Running; @@ -1331,14 +1313,14 @@ ProcessRunner::Results ProcessRunner::postRun() // // MO will be running in the background with no visual feedback, but that's // how it is - r = waitForProcess(m_handle.get(), &m_exitCode, nullptr, expectedExecutables); + r = waitForProcess(m_handle.get(), &m_exitCode, nullptr, + expectedExecutables); } else { - withLock([&](auto& ls) { + withLock([&](auto &ls) { r = waitForProcess(m_handle.get(), &m_exitCode, &ls, expectedExecutables); }); } - if (shouldRefresh(r)) { QEventLoop loop; const bool wait = m_waitFlags.testFlag(WaitForRefresh); @@ -1361,38 +1343,28 @@ ProcessRunner::Results ProcessRunner::postRun() } #ifdef _WIN32 -ProcessRunner::Results ProcessRunner::attachToProcess(HANDLE h) -{ +ProcessRunner::Results ProcessRunner::attachToProcess(HANDLE h) { m_handle.reset(h); return postRun(); } #else -ProcessRunner::Results ProcessRunner::attachToProcess(pid_t pid) -{ +ProcessRunner::Results ProcessRunner::attachToProcess(pid_t pid) { m_handle.reset(reinterpret_cast(static_cast(pid))); return postRun(); } #endif -DWORD ProcessRunner::exitCode() const -{ - return m_exitCode; -} +DWORD ProcessRunner::exitCode() const { return m_exitCode; } #ifdef _WIN32 -HANDLE ProcessRunner::getProcessHandle() const -{ - return m_handle.get(); -} +HANDLE ProcessRunner::getProcessHandle() const { return m_handle.get(); } #else -pid_t ProcessRunner::getProcessHandle() const -{ +pid_t ProcessRunner::getProcessHandle() const { return static_cast(reinterpret_cast(m_handle.get())); } #endif -env::HandlePtr ProcessRunner::stealProcessHandle() -{ +env::HandlePtr ProcessRunner::stealProcessHandle() { auto h = m_handle.release(); m_handle.reset(INVALID_HANDLE_VALUE); return env::HandlePtr(h); @@ -1400,8 +1372,7 @@ env::HandlePtr ProcessRunner::stealProcessHandle() #ifdef _WIN32 ProcessRunner::Results -ProcessRunner::waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason) -{ +ProcessRunner::waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason) { m_lockReason = reason; if (!m_core.settings().interface().lockGUI()) { @@ -1412,7 +1383,7 @@ ProcessRunner::waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason) auto r = Error; for (;;) { - withLock([&](auto& ls) { + withLock([&](auto &ls) { const auto processes = getRunningUSVFSProcesses(); if (processes.empty()) { r = Completed; @@ -1439,8 +1410,7 @@ ProcessRunner::waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason) } #endif -void ProcessRunner::withLock(std::function f) -{ +void ProcessRunner::withLock(std::function f) { auto ls = UILocker::instance().lock(m_lockReason); f(*ls); } diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp index f2bd7a0..599ef62 100644 --- a/src/src/protonlauncher.cpp +++ b/src/src/protonlauncher.cpp @@ -3,14 +3,98 @@ #include #include #include +#include #include #include #include #include #include + namespace { +// Restore the pre-AppImage environment for child processes (umu-run, Proton, +// pressure-vessel). The AppRun script saves FLUORINE_ORIG_* vars before +// modifying PATH, LD_LIBRARY_PATH, etc. We restore from those saved values +// so game processes get a clean host environment without AppImage library paths. +void cleanAppImageEnv(QProcessEnvironment& env) +{ + // Remove Fluorine/AppImage-specific vars that should never leak to game processes. + env.remove("QT_QPA_PLATFORM_PLUGIN_PATH"); + env.remove("MO2_PLUGINS_DIR"); + env.remove("MO2_DLLS_DIR"); + env.remove("MO2_PYTHON_DIR"); + env.remove("MO2_BASE_DIR"); + + // AppImage runtime injects these — they can confuse pressure-vessel/umu-run. + env.remove("APPIMAGE"); + env.remove("APPDIR"); + env.remove("OWD"); + env.remove("ARGV0"); + env.remove("APPIMAGE_ORIGINAL_EXEC"); + + env.remove("DESKTOPINTEGRATION"); + + // Restore saved pre-AppImage values. AppRun sets FLUORINE_ORIG_* before + // modifying PATH, LD_LIBRARY_PATH, etc. If those vars exist, use them to + // restore the original host environment. If not (standalone/non-AppImage), + // fall back to stripping known AppImage patterns. + auto restoreOrStrip = [](const QString& var, const QString& origVar, + QProcessEnvironment& e) { + if (e.contains(origVar)) { + const QString orig = e.value(origVar); + if (orig.isEmpty()) { + e.remove(var); + } else { + e.insert(var, orig); + } + e.remove(origVar); + } else { + // Fallback: strip AppImage mount paths by pattern. + const QString value = e.value(var); + if (value.isEmpty()) return; + QStringList kept; + for (const QString& p : value.split(':')) { + if (p.contains(".mount_Fluori") || p.contains("/fluorine/python")) { + continue; + } + kept.append(p); + } + if (kept.isEmpty()) { + e.remove(var); + } else { + e.insert(var, kept.join(':')); + } + } + }; + + const bool hasOrigVars = env.contains("FLUORINE_ORIG_PATH"); + restoreOrStrip("LD_LIBRARY_PATH", "FLUORINE_ORIG_LD_LIBRARY_PATH", env); + restoreOrStrip("PATH", "FLUORINE_ORIG_PATH", env); + restoreOrStrip("XDG_DATA_DIRS", "FLUORINE_ORIG_XDG_DATA_DIRS", env); + restoreOrStrip("QT_PLUGIN_PATH", "FLUORINE_ORIG_QT_PLUGIN_PATH", env); + + MOBase::log::debug("cleanAppImageEnv: {} (LD_LIBRARY_PATH='{}')", + hasOrigVars ? "restored from FLUORINE_ORIG_*" : "pattern-strip fallback", + env.value("LD_LIBRARY_PATH", "")); +} + +// GE-Proton crashes in update_builtin_libs() if tracked_files doesn't exist +// (e.g. prefix was created by plain Wine, not Proton). Create an empty one. +void ensureTrackedFilesExist(const QString& compatDataPath) +{ + if (compatDataPath.isEmpty()) return; + const QString tracked = QDir(compatDataPath).filePath("tracked_files"); + if (!QFileInfo::exists(tracked)) { + QDir().mkpath(compatDataPath); + QFile f(tracked); + if (f.open(QIODevice::WriteOnly)) { + f.close(); + MOBase::log::debug("created empty tracked_files at '{}'", tracked); + } + } +} + QString compatDataPathFromPrefix(const QString& prefixPath) { if (prefixPath.isEmpty()) { @@ -226,6 +310,12 @@ ProtonLauncher& ProtonLauncher::setSteamDrm(bool useSteamDrm) return *this; } +ProtonLauncher& ProtonLauncher::setStoreVariant(const QString& variant) +{ + m_storeVariant = variant.trimmed(); + return *this; +} + ProtonLauncher& ProtonLauncher::addEnvVar(const QString& key, const QString& value) { if (!key.isEmpty()) { @@ -277,6 +367,7 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove("PYTHONHOME"); + cleanAppImageEnv(env); if (!m_prefixPath.isEmpty()) { env.insert("WINEPREFIX", m_prefixPath); @@ -284,6 +375,7 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const const QString compatDataPath = compatDataPathFromPrefix(m_prefixPath); if (!compatDataPath.isEmpty()) { + ensureTrackedFilesExist(compatDataPath); env.insert("STEAM_COMPAT_DATA_PATH", compatDataPath); } @@ -320,6 +412,10 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const MOBase::log::info("Proton launch: '{}' run '{}'", protonScript, m_binary); + if (!m_workingDir.isEmpty()) { + env.insert("PWD", m_workingDir); + } + return startDetachedWithEnv(program, arguments, m_workingDir, env, pid); } @@ -335,9 +431,11 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const ensureSteamRunning(); } - // Resolve umu-run according to user preference (bundled vs system). + // Resolve umu-run: prefer the copy deployed to the Fluorine data dir + // (~/.local/share/fluorine/umu-run), then system PATH, then AppImage bundled. const QString appDir = QCoreApplication::applicationDirPath(); const QString bundled = appDir + QStringLiteral("/umu-run"); + const QString dataDir = QDir::home().filePath(".local/share/fluorine/umu-run"); // Search PATH for a system umu-run, excluding our own app directory // (the launcher prepends it to PATH, so findExecutable would find the @@ -356,25 +454,20 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const } } + // Priority: data dir > system (if preferred) > bundled > system (fallback) QString umuRun; - if (m_preferSystemUmu) { - if (!system.isEmpty()) { - umuRun = system; - } else if (QFileInfo::exists(bundled)) { - umuRun = bundled; - MOBase::log::warn( - "System umu-run preferred but not found in PATH, falling back to bundled"); - } - } else { - if (QFileInfo::exists(bundled)) { - umuRun = bundled; - } else if (!system.isEmpty()) { - umuRun = system; - } + if (QFileInfo::exists(dataDir)) { + umuRun = dataDir; + } else if (m_preferSystemUmu && !system.isEmpty()) { + umuRun = system; + } else if (QFileInfo::exists(bundled)) { + umuRun = bundled; + } else if (!system.isEmpty()) { + umuRun = system; } - MOBase::log::info("umu-run: preferSystem={}, bundled='{}' (exists={}), system='{}', selected='{}'", - m_preferSystemUmu, bundled, QFileInfo::exists(bundled), system, umuRun); + MOBase::log::info("umu-run: dataDir='{}' (exists={}), bundled='{}' (exists={}), system='{}', selected='{}'", + dataDir, QFileInfo::exists(dataDir), bundled, QFileInfo::exists(bundled), system, umuRun); if (umuRun.isEmpty()) { MOBase::log::warn("umu-run not found (bundled or in PATH)"); @@ -390,6 +483,7 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove("PYTHONHOME"); + cleanAppImageEnv(env); if (!m_prefixPath.isEmpty()) { env.insert("WINEPREFIX", m_prefixPath); @@ -400,12 +494,8 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const } // umu-run sets STEAM_COMPAT_DATA_PATH internally from WINEPREFIX, so we - // do NOT set it here. However, the game's Steamworks DRM still needs - // STEAM_COMPAT_CLIENT_INSTALL_PATH to locate the Steam client libraries. - const QString steamPath = detectSteamPath(); - if (!steamPath.isEmpty()) { - env.insert("STEAM_COMPAT_CLIENT_INSTALL_PATH", steamPath); - } + // do NOT set it here. However, ensure tracked_files exists for Proton. + ensureTrackedFilesExist(compatDataPathFromPrefix(m_prefixPath)); uint32_t effectiveSteamAppId = m_steamAppId; if (effectiveSteamAppId == 0) { @@ -418,17 +508,57 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const } } - if (m_useSteamDrm && effectiveSteamAppId != 0) { - // umu-run expects GAMEID in "umu-" format to extract SteamAppId. + // Always pass the game's Steam App ID as GAMEID for protonfixes lookup. + // GAMEID identifies the game (for compatibility fixes), separate from DRM. + if (effectiveSteamAppId != 0) { env.insert("GAMEID", QStringLiteral("umu-") + QString::number(effectiveSteamAppId)); - env.insert("SteamAppId", QString::number(effectiveSteamAppId)); - env.insert("SteamGameId", QString::number(effectiveSteamAppId)); - env.insert("STORE", QStringLiteral("steam")); } else { - // No Steam DRM — use a generic game ID so umu-run doesn't try Steam integration. env.insert("GAMEID", QStringLiteral("umu-0")); } + if (m_useSteamDrm) { + // Steam DRM games need the Steam client path and Steam identity env vars. + env.insert("STORE", QStringLiteral("steam")); + const QString steamPath = detectSteamPath(); + if (!steamPath.isEmpty()) { + env.insert("STEAM_COMPAT_CLIENT_INSTALL_PATH", steamPath); + } + if (effectiveSteamAppId != 0) { + env.insert("SteamAppId", QString::number(effectiveSteamAppId)); + env.insert("SteamGameId", QString::number(effectiveSteamAppId)); + } + } else { + // Non-Steam games: do NOT set SteamAppId/SteamGameId — those can trigger + // Steamworks initialization in Wine which crashes GOG/Epic games. + env.remove("SteamAPPId"); + env.remove("SteamAppId"); + env.remove("SteamGameId"); + + // Set STORE so umu-run knows this is a non-Steam game. Without STORE, + // umu-run may attempt Steam-specific behavior that breaks GOG/Epic titles. + // umu-run expects lowercase values (gog, egs, etc.). + if (!m_storeVariant.isEmpty()) { + env.insert("STORE", m_storeVariant.toLower()); + } else { + env.insert("STORE", QStringLiteral("gog")); + } + } + + // Remove FUSE VFS file descriptors — these are Fluorine-internal and must + // not leak into game processes (especially pressure-vessel containers). + env.remove("_FUSE_COMMFD"); + env.remove("_FUSE_COMMFD2"); + + // Remove Qt/KDE theme vars that can confuse pressure-vessel/Wine. + env.remove("QML_DISABLE_DISK_CACHE"); + env.remove("QT_ICON_THEME_NAME"); + env.remove("QT_STYLE_OVERRIDE"); + env.remove("OLDPWD"); + + if (!m_workingDir.isEmpty()) { + env.insert("PWD", m_workingDir); + } + for (auto it = m_wrapperEnvVars.cbegin(); it != m_wrapperEnvVars.cend(); ++it) { env.insert(it.key(), it.value()); } @@ -446,13 +576,11 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const } } - MOBase::log::info("UMU launch: '{}' '{}' (game id: {}, steam: '{}')", umuRun, - m_binary, - (effectiveSteamAppId == 0 - ? QStringLiteral("") - : QStringLiteral("umu-") + - QString::number(effectiveSteamAppId)), - steamPath); + MOBase::log::info("UMU launch: '{}' '{}' (GAMEID={}, STORE={}, steam_drm={})", + umuRun, m_binary, + env.value("GAMEID"), + env.value("STORE", ""), + m_useSteamDrm); return startDetachedWithEnv(program, arguments, m_workingDir, env, pid); } @@ -470,6 +598,7 @@ bool ProtonLauncher::launchDirect(qint64& pid) const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove("PYTHONHOME"); + cleanAppImageEnv(env); for (auto it = m_wrapperEnvVars.cbegin(); it != m_wrapperEnvVars.cend(); ++it) { env.insert(it.key(), it.value()); } diff --git a/src/src/protonlauncher.h b/src/src/protonlauncher.h index 5480c69..7ef6e76 100644 --- a/src/src/protonlauncher.h +++ b/src/src/protonlauncher.h @@ -24,6 +24,7 @@ public: ProtonLauncher& setPreferSystemUmu(bool preferSystemUmu); ProtonLauncher& setUseSteamRun(bool useSteamRun); ProtonLauncher& setSteamDrm(bool useSteamDrm); + ProtonLauncher& setStoreVariant(const QString& variant); ProtonLauncher& addEnvVar(const QString& key, const QString& value); // Launch dispatch: UMU -> Proton -> Direct @@ -46,6 +47,7 @@ private: bool m_preferSystemUmu; bool m_useSteamRun; bool m_useSteamDrm; + QString m_storeVariant; // "GOG", "Epic", or empty for Steam QMap m_envVars; QMap m_wrapperEnvVars; }; diff --git a/src/src/savestab.cpp b/src/src/savestab.cpp index 2f921e5..29411fa 100644 --- a/src/src/savestab.cpp +++ b/src/src/savestab.cpp @@ -1,6 +1,6 @@ -#include "savestab.h" -#include "activatemodsdialog.h" -#include "organizercore.h" +#include "savestab.h" +#include "activatemodsdialog.h" +#include "organizercore.h" #include "ui_mainwindow.h" #include #include @@ -8,12 +8,55 @@ #include #include +#include +#include + using namespace MOBase; namespace { bool g_disableSaveTooltipsAfterOOM = false; +// Read a value from a Bethesda-style INI file without QSettings. +// QSettings::IniFormat interprets backslashes as line continuations, +// which corrupts values like "sLocalSavePath=__MO_Saves\". +QString readIniValueDirect(const QString& iniFile, const QString& section, + const QString& key) +{ + QFile file(iniFile); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return {}; + } + + const QString sectionHeader = "[" + section + "]"; + QTextStream in(&file); + bool inSection = false; + + while (!in.atEnd()) { + QString line = in.readLine().trimmed(); + if (line.startsWith('[') && line.endsWith(']')) { + if (inSection) + break; + if (line.compare(sectionHeader, Qt::CaseInsensitive) == 0) + inSection = true; + continue; + } + + if (inSection && !line.isEmpty() && !line.startsWith(';') && + !line.startsWith('#')) { + int eqPos = line.indexOf('='); + if (eqPos > 0) { + QString existingKey = line.left(eqPos).trimmed(); + if (existingKey.compare(key, Qt::CaseInsensitive) == 0) { + return line.mid(eqPos + 1).trimmed(); + } + } + } + } + + return {}; +} + QString sanitizeText(QString in, int maxLen = 200) { for (int i = 0; i < in.size(); ++i) { @@ -51,48 +94,48 @@ bool isLikelyCorruptSaveText(QString const& in) } // namespace SavesTab::SavesTab(QWidget* window, OrganizerCore& core, Ui::MainWindow* mwui) - : m_window(window), m_core(core), m_CurrentSaveView(nullptr), - ui{mwui->tabWidget, mwui->savesTab, mwui->savegameList} -{ - m_SavesWatcherTimer.setSingleShot(true); - m_SavesWatcherTimer.setInterval(500); - - ui.list->installEventFilter(this); - ui.list->setMouseTracking(true); - - connect(&m_SavesWatcher, &QFileSystemWatcher::directoryChanged, [&] { - m_SavesWatcherTimer.start(); - }); - - connect(&m_SavesWatcherTimer, &QTimer::timeout, [&] { - refreshSavesIfOpen(); - }); - - connect(ui.list, &QWidget::customContextMenuRequested, [&](auto pos) { - onContextMenu(pos); - }); - - connect(ui.list, &QTreeWidget::itemEntered, [&](auto* item) { - saveSelectionChanged(item); - }); -} - -bool SavesTab::eventFilter(QObject* object, QEvent* e) -{ - if (object == ui.list) { - if (e->type() == QEvent::Leave || e->type() == QEvent::WindowDeactivate) { - hideSaveGameInfo(); - } else if (e->type() == QEvent::KeyPress) { - QKeyEvent* keyEvent = static_cast(e); - if (keyEvent->key() == Qt::Key_Delete) { - deleteSavegame(); - } - } - } - - return false; -} - + : m_window(window), m_core(core), m_CurrentSaveView(nullptr), + ui{mwui->tabWidget, mwui->savesTab, mwui->savegameList} +{ + m_SavesWatcherTimer.setSingleShot(true); + m_SavesWatcherTimer.setInterval(500); + + ui.list->installEventFilter(this); + ui.list->setMouseTracking(true); + + connect(&m_SavesWatcher, &QFileSystemWatcher::directoryChanged, [&] { + m_SavesWatcherTimer.start(); + }); + + connect(&m_SavesWatcherTimer, &QTimer::timeout, [&] { + refreshSavesIfOpen(); + }); + + connect(ui.list, &QWidget::customContextMenuRequested, [&](auto pos) { + onContextMenu(pos); + }); + + connect(ui.list, &QTreeWidget::itemEntered, [&](auto* item) { + saveSelectionChanged(item); + }); +} + +bool SavesTab::eventFilter(QObject* object, QEvent* e) +{ + if (object == ui.list) { + if (e->type() == QEvent::Leave || e->type() == QEvent::WindowDeactivate) { + hideSaveGameInfo(); + } else if (e->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(e); + if (keyEvent->key() == Qt::Key_Delete) { + deleteSavegame(); + } + } + } + + return false; +} + void SavesTab::displaySaveGameInfo(QTreeWidgetItem* newItem) { if (g_disableSaveTooltipsAfterOOM) { @@ -100,21 +143,21 @@ void SavesTab::displaySaveGameInfo(QTreeWidgetItem* newItem) } // don't display the widget if the main window doesn't have focus - // - // this goes against the standard behaviour for tooltips, which are displayed - // on hover regardless of focus, but this widget is so large and busy that - // it's probably better this way - if (!m_window->isActiveWindow()) { - return; - } - + // + // this goes against the standard behaviour for tooltips, which are displayed + // on hover regardless of focus, but this widget is so large and busy that + // it's probably better this way + if (!m_window->isActiveWindow()) { + return; + } + if (m_CurrentSaveView == nullptr) { auto info = m_core.gameFeatures().gameFeature(); - - if (info != nullptr) { - m_CurrentSaveView = info->getSaveGameWidget(m_window); - } - + + if (info != nullptr) { + m_CurrentSaveView = info->getSaveGameWidget(m_window); + } + if (m_CurrentSaveView == nullptr) { return; } @@ -135,129 +178,135 @@ void SavesTab::displaySaveGameInfo(QTreeWidgetItem* newItem) hideSaveGameInfo(); return; } - - QWindow* window = m_CurrentSaveView->window()->windowHandle(); - QRect screenRect; - if (window == nullptr) - screenRect = QGuiApplication::primaryScreen()->geometry(); - else - screenRect = window->screen()->geometry(); - - QPoint pos = QCursor::pos(); - if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) { - pos.rx() -= (m_CurrentSaveView->width() + 2); - } else { - pos.rx() += 5; - } - - if (pos.y() + m_CurrentSaveView->height() > screenRect.bottom()) { - pos.ry() -= (m_CurrentSaveView->height() + 10); - } else { - pos.ry() += 20; - } - m_CurrentSaveView->move(pos); - - m_CurrentSaveView->show(); - m_CurrentSaveView->setProperty("displayItem", - QVariant::fromValue(static_cast(newItem))); -} - -void SavesTab::saveSelectionChanged(QTreeWidgetItem* newItem) -{ - if (newItem == nullptr) { - hideSaveGameInfo(); - } else if (m_CurrentSaveView == nullptr || - newItem != m_CurrentSaveView->property("displayItem").value()) { - displaySaveGameInfo(newItem); - } -} - -void SavesTab::hideSaveGameInfo() -{ - if (m_CurrentSaveView != nullptr) { - m_CurrentSaveView->deleteLater(); - m_CurrentSaveView = nullptr; - } -} - -void SavesTab::refreshSavesIfOpen() -{ - if (ui.mainTabs->currentWidget() == ui.tab) { - refreshSaveList(); - } -} - -QDir SavesTab::currentSavesDir() const -{ - // TODO: This code should probably be handled by the game plugins - QDir savesDir; - if (m_core.currentProfile()->localSavesEnabled()) { - savesDir.setPath(m_core.currentProfile()->savePath()); - } else { - auto iniFiles = m_core.managedGame()->iniFiles(); - - if (iniFiles.isEmpty() || - m_core.gameFeatures().gameFeature() == nullptr) { - return m_core.managedGame()->savesDirectory(); - } - - QString iniPath = m_core.currentProfile()->absoluteIniFilePath(iniFiles[0]); - -#ifdef _WIN32 - wchar_t path[MAX_PATH]; - if (::GetPrivateProfileStringW(L"General", L"SLocalSavePath", L"", path, MAX_PATH, - iniPath.toStdWString().c_str())) { - savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath( - QString::fromWCharArray(path))); - } else { - savesDir = m_core.managedGame()->savesDirectory(); - } -#else - // On Linux, use QSettings to read the INI file - QSettings ini(iniPath, QSettings::IniFormat); - QString savePath = ini.value("General/SLocalSavePath").toString(); - if (!savePath.isEmpty()) { - savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath(savePath)); - } else { - savesDir = m_core.managedGame()->savesDirectory(); - } -#endif - } - - return savesDir; -} - -void SavesTab::startMonitorSaves() -{ - stopMonitorSaves(); - - QDir savesDir = currentSavesDir(); - - m_SavesWatcher.addPath(savesDir.absolutePath()); -} - -void SavesTab::stopMonitorSaves() -{ - if (m_SavesWatcher.directories().length() > 0) { - m_SavesWatcher.removePaths(m_SavesWatcher.directories()); - } -} - -void SavesTab::refreshSaveList() -{ - TimeThis tt("MainWindow::refreshSaveList()"); - - startMonitorSaves(); // re-starts monitoring - - try { - QDir savesDir = currentSavesDir(); - MOBase::log::debug("reading save games from {}", savesDir.absolutePath()); - m_SaveGames = m_core.managedGame()->listSaves(savesDir); - std::sort(m_SaveGames.begin(), m_SaveGames.end(), - [](auto const& lhs, auto const& rhs) { - return lhs->getCreationTime() > rhs->getCreationTime(); - }); - + + QWindow* window = m_CurrentSaveView->window()->windowHandle(); + QRect screenRect; + if (window == nullptr) + screenRect = QGuiApplication::primaryScreen()->geometry(); + else + screenRect = window->screen()->geometry(); + + QPoint pos = QCursor::pos(); + if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) { + pos.rx() -= (m_CurrentSaveView->width() + 2); + } else { + pos.rx() += 5; + } + + if (pos.y() + m_CurrentSaveView->height() > screenRect.bottom()) { + pos.ry() -= (m_CurrentSaveView->height() + 10); + } else { + pos.ry() += 20; + } + m_CurrentSaveView->move(pos); + + m_CurrentSaveView->show(); + m_CurrentSaveView->setProperty("displayItem", + QVariant::fromValue(static_cast(newItem))); +} + +void SavesTab::saveSelectionChanged(QTreeWidgetItem* newItem) +{ + if (newItem == nullptr) { + hideSaveGameInfo(); + } else if (m_CurrentSaveView == nullptr || + newItem != m_CurrentSaveView->property("displayItem").value()) { + displaySaveGameInfo(newItem); + } +} + +void SavesTab::hideSaveGameInfo() +{ + if (m_CurrentSaveView != nullptr) { + m_CurrentSaveView->deleteLater(); + m_CurrentSaveView = nullptr; + } +} + +void SavesTab::refreshSavesIfOpen() +{ + if (ui.mainTabs->currentWidget() == ui.tab) { + refreshSaveList(); + } +} + +QDir SavesTab::currentSavesDir() const +{ + // TODO: This code should probably be handled by the game plugins + QDir savesDir; + if (m_core.currentProfile()->localSavesEnabled()) { + savesDir.setPath(m_core.currentProfile()->savePath()); + } else { + auto iniFiles = m_core.managedGame()->iniFiles(); + + if (iniFiles.isEmpty() || + m_core.gameFeatures().gameFeature() == nullptr) { + return m_core.managedGame()->savesDirectory(); + } + + QString iniPath = m_core.currentProfile()->absoluteIniFilePath(iniFiles[0]); + +#ifdef _WIN32 + wchar_t path[MAX_PATH]; + if (::GetPrivateProfileStringW(L"General", L"SLocalSavePath", L"", path, MAX_PATH, + iniPath.toStdWString().c_str())) { + savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath( + QString::fromWCharArray(path))); + } else { + savesDir = m_core.managedGame()->savesDirectory(); + } +#else + // Read directly without QSettings — QSettings::IniFormat interprets + // trailing backslashes as line continuations, corrupting values like + // "sLocalSavePath=__MO_Saves\". + QString savePath = readIniValueDirect(iniPath, "General", "sLocalSavePath"); + // Strip trailing path separators (Bethesda INIs use "Saves\" with a + // trailing backslash that is part of the value, not a continuation). + while (savePath.endsWith('\\') || savePath.endsWith('/')) { + savePath.chop(1); + } + if (!savePath.isEmpty()) { + savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath(savePath)); + } else { + savesDir = m_core.managedGame()->savesDirectory(); + } +#endif + } + + return savesDir; +} + +void SavesTab::startMonitorSaves() +{ + stopMonitorSaves(); + + QDir savesDir = currentSavesDir(); + + m_SavesWatcher.addPath(savesDir.absolutePath()); +} + +void SavesTab::stopMonitorSaves() +{ + if (m_SavesWatcher.directories().length() > 0) { + m_SavesWatcher.removePaths(m_SavesWatcher.directories()); + } +} + +void SavesTab::refreshSaveList() +{ + TimeThis tt("MainWindow::refreshSaveList()"); + + startMonitorSaves(); // re-starts monitoring + + try { + QDir savesDir = currentSavesDir(); + MOBase::log::debug("reading save games from {}", savesDir.absolutePath()); + m_SaveGames = m_core.managedGame()->listSaves(savesDir); + std::sort(m_SaveGames.begin(), m_SaveGames.end(), + [](auto const& lhs, auto const& rhs) { + return lhs->getCreationTime() > rhs->getCreationTime(); + }); + ui.list->clear(); for (auto& save : m_SaveGames) { auto relpath = savesDir.relativeFilePath(save->getFilepath()); @@ -269,125 +318,125 @@ void SavesTab::refreshSaveList() ui.list->addTopLevelItem(new QTreeWidgetItem(ui.list, {display, relpath})); } } catch (std::exception& e) { - // listSaves() can throw - log::error("{}", e.what()); - } -} - -void SavesTab::deleteSavegame() -{ - auto info = m_core.gameFeatures().gameFeature(); - - QString savesMsgLabel; - QStringList deleteFiles; - - int count = 0; - - for (const QModelIndex& idx : ui.list->selectionModel()->selectedRows()) { - - auto& saveGame = m_SaveGames[idx.row()]; - - if (count < 10) { - savesMsgLabel += - "
  • " + QFileInfo(saveGame->getFilepath()).completeBaseName() + "
  • "; - } - ++count; - - deleteFiles += saveGame->allFiles(); - } - - if (count > 10) { - savesMsgLabel += "
  • ... " + tr("%1 more").arg(count - 10) + "
  • "; - } - - if (QMessageBox::question( - m_window, tr("Confirm"), - tr("Are you sure you want to remove the following %n save(s)?
    " - "
      %1

    " - "Removed saves will be sent to the Recycle Bin.", - "", count) - .arg(savesMsgLabel), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - shellDelete(deleteFiles, true); // recycle bin delete. - refreshSaveList(); - } -} - -void SavesTab::onContextMenu(const QPoint& pos) -{ - QItemSelectionModel* selection = ui.list->selectionModel(); - - if (!selection->hasSelection()) { - return; - } - - QMenu menu; - - auto info = m_core.gameFeatures().gameFeature(); - if (info != nullptr) { - QAction* action = menu.addAction(tr("Fix enabled mods...")); - action->setEnabled(false); - if (selection->selectedRows().count() == 1) { - auto& save = m_SaveGames[selection->selectedRows()[0].row()]; - SaveGameInfo::MissingAssets missing = info->getMissingAssets(*save); - if (missing.size() != 0) { - connect(action, &QAction::triggered, this, [this, missing] { - fixMods(missing); - }); - action->setEnabled(true); - } - } - } - - QString deleteMenuLabel = - tr("Delete %n save(s)", "", selection->selectedRows().count()); - menu.addAction(deleteMenuLabel, [&] { - deleteSavegame(); - }); - - menu.addAction(tr("Open in Explorer..."), [&] { - openInExplorer(); - }); - - menu.exec(ui.list->viewport()->mapToGlobal(pos)); -} - -void SavesTab::fixMods(SaveGameInfo::MissingAssets const& missingAssets) -{ - ActivateModsDialog dialog(missingAssets, m_window); - if (dialog.exec() == QDialog::Accepted) { - // activate the required mods, then enable all esps - std::set modsToActivate = dialog.getModsToActivate(); - for (std::set::iterator iter = modsToActivate.begin(); - iter != modsToActivate.end(); ++iter) { - if ((*iter != "") && (*iter != "")) { - unsigned int modIndex = ModInfo::getIndex(*iter); - m_core.currentProfile()->setModEnabled(modIndex, true); - } - } - - m_core.currentProfile()->writeModlist(); - m_core.refreshLists(); - - std::set espsToActivate = dialog.getESPsToActivate(); - for (std::set::iterator iter = espsToActivate.begin(); - iter != espsToActivate.end(); ++iter) { - m_core.pluginList()->enableESP(*iter); - } - - m_core.saveCurrentLists(); - } -} - -void SavesTab::openInExplorer() -{ - auto info = m_core.gameFeatures().gameFeature(); - - const auto sel = ui.list->selectionModel()->selectedRows(); - if (sel.empty()) { - return; - } - - auto& saveGame = m_SaveGames[sel[0].row()]; - shell::Explore(saveGame->getFilepath()); -} + // listSaves() can throw + log::error("{}", e.what()); + } +} + +void SavesTab::deleteSavegame() +{ + auto info = m_core.gameFeatures().gameFeature(); + + QString savesMsgLabel; + QStringList deleteFiles; + + int count = 0; + + for (const QModelIndex& idx : ui.list->selectionModel()->selectedRows()) { + + auto& saveGame = m_SaveGames[idx.row()]; + + if (count < 10) { + savesMsgLabel += + "
  • " + QFileInfo(saveGame->getFilepath()).completeBaseName() + "
  • "; + } + ++count; + + deleteFiles += saveGame->allFiles(); + } + + if (count > 10) { + savesMsgLabel += "
  • ... " + tr("%1 more").arg(count - 10) + "
  • "; + } + + if (QMessageBox::question( + m_window, tr("Confirm"), + tr("Are you sure you want to remove the following %n save(s)?
    " + "
      %1

    " + "Removed saves will be sent to the Recycle Bin.", + "", count) + .arg(savesMsgLabel), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + shellDelete(deleteFiles, true); // recycle bin delete. + refreshSaveList(); + } +} + +void SavesTab::onContextMenu(const QPoint& pos) +{ + QItemSelectionModel* selection = ui.list->selectionModel(); + + if (!selection->hasSelection()) { + return; + } + + QMenu menu; + + auto info = m_core.gameFeatures().gameFeature(); + if (info != nullptr) { + QAction* action = menu.addAction(tr("Fix enabled mods...")); + action->setEnabled(false); + if (selection->selectedRows().count() == 1) { + auto& save = m_SaveGames[selection->selectedRows()[0].row()]; + SaveGameInfo::MissingAssets missing = info->getMissingAssets(*save); + if (missing.size() != 0) { + connect(action, &QAction::triggered, this, [this, missing] { + fixMods(missing); + }); + action->setEnabled(true); + } + } + } + + QString deleteMenuLabel = + tr("Delete %n save(s)", "", selection->selectedRows().count()); + menu.addAction(deleteMenuLabel, [&] { + deleteSavegame(); + }); + + menu.addAction(tr("Open in Explorer..."), [&] { + openInExplorer(); + }); + + menu.exec(ui.list->viewport()->mapToGlobal(pos)); +} + +void SavesTab::fixMods(SaveGameInfo::MissingAssets const& missingAssets) +{ + ActivateModsDialog dialog(missingAssets, m_window); + if (dialog.exec() == QDialog::Accepted) { + // activate the required mods, then enable all esps + std::set modsToActivate = dialog.getModsToActivate(); + for (std::set::iterator iter = modsToActivate.begin(); + iter != modsToActivate.end(); ++iter) { + if ((*iter != "") && (*iter != "")) { + unsigned int modIndex = ModInfo::getIndex(*iter); + m_core.currentProfile()->setModEnabled(modIndex, true); + } + } + + m_core.currentProfile()->writeModlist(); + m_core.refreshLists(); + + std::set espsToActivate = dialog.getESPsToActivate(); + for (std::set::iterator iter = espsToActivate.begin(); + iter != espsToActivate.end(); ++iter) { + m_core.pluginList()->enableESP(*iter); + } + + m_core.saveCurrentLists(); + } +} + +void SavesTab::openInExplorer() +{ + auto info = m_core.gameFeatures().gameFeature(); + + const auto sel = ui.list->selectionModel()->selectedRows(); + if (sel.empty()) { + return; + } + + auto& saveGame = m_SaveGames[sel[0].row()]; + shell::Explore(saveGame->getFilepath()); +} diff --git a/src/src/shared/appconfig.cpp b/src/src/shared/appconfig.cpp index 90bbffb..579318f 100644 --- a/src/src/shared/appconfig.cpp +++ b/src/src/shared/appconfig.cpp @@ -46,6 +46,28 @@ QString basePath() return QCoreApplication::applicationDirPath(); } +QString pluginsPath() +{ +#ifndef _WIN32 + const char* envDir = std::getenv("MO2_PLUGINS_DIR"); + if (envDir && envDir[0] != '\0') { + return QString::fromUtf8(envDir); + } +#endif + return basePath() + "/" + QString::fromStdWString(pluginPath()); +} + +QString dllsPath() +{ +#ifndef _WIN32 + const char* envDir = std::getenv("MO2_DLLS_DIR"); + if (envDir && envDir[0] != '\0') { + return QString::fromUtf8(envDir); + } +#endif + return basePath() + "/dlls"; +} + namespace MOShared { #undef PARWSTRING diff --git a/src/src/shared/appconfig.h b/src/src/shared/appconfig.h index 03f3300..0c0f7b4 100644 --- a/src/src/shared/appconfig.h +++ b/src/src/shared/appconfig.h @@ -36,6 +36,16 @@ namespace AppConfig // returned; otherwise falls back to QCoreApplication::applicationDirPath(). QString basePath(); +// Returns the directory containing MO2 plugins. On Linux, if the +// MO2_PLUGINS_DIR environment variable is set (e.g. when plugins live +// inside a read-only AppImage squashfs) that value is used; otherwise +// falls back to basePath() + "/plugins". +QString pluginsPath(); + +// Returns the directory containing bundled DLLs (7z.so, etc.). +// Respects MO2_DLLS_DIR on Linux, otherwise basePath() + "/dlls". +QString dllsPath(); + namespace MOShared { #undef PARWSTRING diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp index 05e07bd..850d5a2 100644 --- a/src/src/spawn.cpp +++ b/src/src/spawn.cpp @@ -53,12 +53,10 @@ along with Mod Organizer. If not, see . using namespace MOBase; using namespace MOShared; -namespace spawn::dialogs -{ +namespace spawn::dialogs { #ifdef _WIN32 -std::wstring makeRightsDetails(const env::FileSecurity& fs) -{ +std::wstring makeRightsDetails(const env::FileSecurity &fs) { if (fs.rights.normalRights) { return L"(normal rights)"; } @@ -75,22 +73,22 @@ std::wstring makeRightsDetails(const env::FileSecurity& fs) return s; } -QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more = {}) -{ +QString makeDetails(const SpawnParameters &sp, DWORD code, + const QString &more = {}) { std::wstring owner, rights; if (sp.binary.isFile()) { const auto fs = env::getFileSecurity(sp.binary.absoluteFilePath()); if (fs.error.isEmpty()) { - owner = fs.owner.toStdWString(); + owner = fs.owner.toStdWString(); rights = makeRightsDetails(fs); } else { - owner = fs.error.toStdWString(); + owner = fs.error.toStdWString(); rights = fs.error.toStdWString(); } } else { - owner = L"(file not found)"; + owner = L"(file not found)"; rights = L"(file not found)"; } @@ -98,7 +96,7 @@ QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more = (sp.currentDirectory.isEmpty() ? true : sp.currentDirectory.exists()); const auto appDir = QCoreApplication::applicationDirPath(); - const auto sep = QDir::separator(); + const auto sep = QDir::separator(); const std::wstring usvfs_x86_dll = QFileInfo(appDir + sep + "usvfs_x86.dll").isFile() ? L"ok" : L"not found"; @@ -107,10 +105,12 @@ QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more = QFileInfo(appDir + sep + "usvfs_x64.dll").isFile() ? L"ok" : L"not found"; const std::wstring usvfs_x86_proxy = - QFileInfo(appDir + sep + "usvfs_proxy_x86.exe").isFile() ? L"ok" : L"not found"; + QFileInfo(appDir + sep + "usvfs_proxy_x86.exe").isFile() ? L"ok" + : L"not found"; const std::wstring usvfs_x64_proxy = - QFileInfo(appDir + sep + "usvfs_proxy_x64.exe").isFile() ? L"ok" : L"not found"; + QFileInfo(appDir + sep + "usvfs_proxy_x64.exe").isFile() ? L"ok" + : L"not found"; std::wstring elevated; if (auto b = env::Environment().windowsInfo().isElevated()) { @@ -119,42 +119,44 @@ QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more = elevated = L"(not available)"; } - auto s = std::format(L"Error {} {}{}: {}\n" - L" . binary: '{}'\n" - L" . owner: {}\n" - L" . rights: {}\n" - L" . arguments: '{}'\n" - L" . cwd: '{}'{}\n" - L" . stdout: {}, stderr: {}, hooked: {}\n" - L" . MO elevated: {}", - code, errorCodeName(code), (more.isEmpty() ? more : ", " + more), - formatSystemMessage(code), - QDir::toNativeSeparators(sp.binary.absoluteFilePath()), owner, - rights, sp.arguments, - QDir::toNativeSeparators(sp.currentDirectory.absolutePath()), - (cwdExists ? L"" : L" (not found)"), - (sp.stdOut == INVALID_HANDLE_VALUE ? L"no" : L"yes"), - (sp.stdErr == INVALID_HANDLE_VALUE ? L"no" : L"yes"), - (sp.hooked ? L"yes" : L"no"), elevated); + auto s = std::format( + L"Error {} {}{}: {}\n" + L" . binary: '{}'\n" + L" . owner: {}\n" + L" . rights: {}\n" + L" . arguments: '{}'\n" + L" . cwd: '{}'{}\n" + L" . stdout: {}, stderr: {}, hooked: {}\n" + L" . MO elevated: {}", + code, errorCodeName(code), (more.isEmpty() ? more : ", " + more), + formatSystemMessage(code), + QDir::toNativeSeparators(sp.binary.absoluteFilePath()), owner, rights, + sp.arguments, + QDir::toNativeSeparators(sp.currentDirectory.absolutePath()), + (cwdExists ? L"" : L" (not found)"), + (sp.stdOut == INVALID_HANDLE_VALUE ? L"no" : L"yes"), + (sp.stdErr == INVALID_HANDLE_VALUE ? L"no" : L"yes"), + (sp.hooked ? L"yes" : L"no"), elevated); if (sp.hooked) { s += std::format(L"\n . usvfs x86:{} x64:{} proxy_x86:{} proxy_x64:{}", - usvfs_x86_dll, usvfs_x64_dll, usvfs_x86_proxy, usvfs_x64_proxy); + usvfs_x86_dll, usvfs_x64_dll, usvfs_x86_proxy, + usvfs_x64_proxy); } return QString::fromStdWString(s); } #else -QString makeDetails(const SpawnParameters& sp, int code, const QString& more = {}) -{ +QString makeDetails(const SpawnParameters &sp, int code, + const QString &more = {}) { const bool cwdExists = (sp.currentDirectory.isEmpty() ? true : sp.currentDirectory.exists()); QString s = QString("Error %1%2: %3\n" - " . binary: '%4'\n" - " . arguments: '%5'\n" - " . cwd: '%6'%7\n" - " . stdout: %8, stderr: %9, hooked: %10") + " . binary: '%4'\n" + " . arguments: '%5'\n" + " . cwd: '%6'%7\n" + " . stdout: %8, stderr: %9, hooked: %10") .arg(code) .arg(more.isEmpty() ? more : ", " + more) .arg(QString::fromUtf8(strerror(code))) @@ -171,19 +173,21 @@ QString makeDetails(const SpawnParameters& sp, int code, const QString& more = { #endif #ifdef _WIN32 -QString makeContent(const SpawnParameters& sp, DWORD code) -{ +QString makeContent(const SpawnParameters &sp, DWORD code) { if (code == ERROR_INVALID_PARAMETER) { return QObject::tr( - "This error typically happens because an antivirus has deleted critical " + "This error typically happens because an antivirus has deleted " + "critical " "files from Mod Organizer's installation folder or has made them " "generally inaccessible. Add an exclusion for Mod Organizer's " - "installation folder in your antivirus, reinstall Mod Organizer and try " + "installation folder in your antivirus, reinstall Mod Organizer and " + "try " "again."); } else if (code == ERROR_ACCESS_DENIED) { return QObject::tr( "This error typically happens because an antivirus is preventing Mod " - "Organizer from starting programs. Add an exclusion for Mod Organizer's " + "Organizer from starting programs. Add an exclusion for Mod " + "Organizer's " "installation folder in your antivirus and try again."); } else if (code == ERROR_FILE_NOT_FOUND) { return QObject::tr("The file '%1' does not exist.") @@ -198,15 +202,13 @@ QString makeContent(const SpawnParameters& sp, DWORD code) return QString::fromStdWString(formatSystemMessage(code)); } #else -QString makeContent(const SpawnParameters& sp, int code) -{ +QString makeContent(const SpawnParameters &sp, int code) { if (code == ENOENT) { return QObject::tr("The file '%1' does not exist.") .arg(sp.binary.absoluteFilePath()); } else if (code == EACCES) { - return QObject::tr( - "Permission denied when trying to start '%1'. " - "Check that the file is executable.") + return QObject::tr("Permission denied when trying to start '%1'. " + "Check that the file is executable.") .arg(sp.binary.absoluteFilePath()); } @@ -220,38 +222,39 @@ QString makeContent(const SpawnParameters& sp, int code) #endif #ifdef _WIN32 -QMessageBox::StandardButton badSteamReg(QWidget* parent, const QString& keyName, - const QString& valueName) -{ +QMessageBox::StandardButton badSteamReg(QWidget *parent, const QString &keyName, + const QString &valueName) { const auto details = - QString("can't start steam, registry value at '%1' is empty or doesn't exist") + QString( + "can't start steam, registry value at '%1' is empty or doesn't exist") .arg(keyName + "\\" + valueName); log::error("{}", details); return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam")) .main(QObject::tr("Cannot start Steam")) - .content( - QObject::tr("The path to the Steam executable cannot be found. You might try " - "reinstalling Steam.")) + .content(QObject::tr( + "The path to the Steam executable cannot be found. You might try " + "reinstalling Steam.")) .details(details) .icon(QMessageBox::Critical) .button({QObject::tr("Continue without starting Steam"), - QObject::tr("The program may fail to launch."), QMessageBox::Yes}) + QObject::tr("The program may fail to launch."), + QMessageBox::Yes}) .button({QObject::tr("Cancel"), QMessageBox::Cancel}) .exec(); } #endif #ifdef _WIN32 -QMessageBox::StandardButton startSteamFailed(QWidget* parent, const QString& keyName, - const QString& valueName, - const QString& exe, - const SpawnParameters& sp, DWORD e) -{ - auto details = QString("a steam install was found in the registry at '%1': '%2'\n\n") - .arg(keyName + "\\" + valueName) - .arg(exe); +QMessageBox::StandardButton +startSteamFailed(QWidget *parent, const QString &keyName, + const QString &valueName, const QString &exe, + const SpawnParameters &sp, DWORD e) { + auto details = + QString("a steam install was found in the registry at '%1': '%2'\n\n") + .arg(keyName + "\\" + valueName) + .arg(exe); details += makeDetails(sp, e); @@ -263,26 +266,27 @@ QMessageBox::StandardButton startSteamFailed(QWidget* parent, const QString& key .details(details) .icon(QMessageBox::Critical) .button({QObject::tr("Continue without starting Steam"), - QObject::tr("The program may fail to launch."), QMessageBox::Yes}) + QObject::tr("The program may fail to launch."), + QMessageBox::Yes}) .button({QObject::tr("Cancel"), QMessageBox::Cancel}) .exec(); } #endif -void spawnFailed(QWidget* parent, const SpawnParameters& sp, +void spawnFailed(QWidget *parent, const SpawnParameters &sp, #ifdef _WIN32 DWORD code #else int code #endif -) -{ +) { const auto details = makeDetails(sp, code); log::error("{}", details); const auto title = QObject::tr("Cannot launch program"); - const auto mainText = QObject::tr("Cannot start %1").arg(sp.binary.fileName()); + const auto mainText = + QObject::tr("Cannot start %1").arg(sp.binary.fileName()); MOBase::TaskDialog(parent, title) .main(mainText) @@ -293,10 +297,9 @@ void spawnFailed(QWidget* parent, const SpawnParameters& sp, } #ifdef _WIN32 -void helperFailed(QWidget* parent, DWORD code, const QString& why, - const std::wstring& binary, const std::wstring& cwd, - const std::wstring& args) -{ +void helperFailed(QWidget *parent, DWORD code, const QString &why, + const std::wstring &binary, const std::wstring &cwd, + const std::wstring &args) { SpawnParameters sp; sp.binary = QFileInfo(QString::fromStdWString(binary)); sp.currentDirectory.setPath(QString::fromStdWString(cwd)); @@ -307,7 +310,8 @@ void helperFailed(QWidget* parent, DWORD code, const QString& why, const auto title = QObject::tr("Cannot launch helper"); - const auto mainText = QObject::tr("Cannot start %1").arg(sp.binary.fileName()); + const auto mainText = + QObject::tr("Cannot start %1").arg(sp.binary.fileName()); MOBase::TaskDialog(parent, title) .main(mainText) @@ -317,20 +321,22 @@ void helperFailed(QWidget* parent, DWORD code, const QString& why, .exec(); } -bool confirmRestartAsAdmin(QWidget* parent, const SpawnParameters& sp) -{ +bool confirmRestartAsAdmin(QWidget *parent, const SpawnParameters &sp) { const auto details = makeDetails(sp, ERROR_ELEVATION_REQUIRED); log::error("{}", details); const auto title = QObject::tr("Elevation required"); - const auto mainText = QObject::tr("Cannot start %1").arg(sp.binary.fileName()); + const auto mainText = + QObject::tr("Cannot start %1").arg(sp.binary.fileName()); const auto content = QObject::tr( "This program is requesting to run as administrator but Mod Organizer " - "itself is not running as administrator. Running programs as administrator " - "is typically unnecessary as long as the game and Mod Organizer have been " + "itself is not running as administrator. Running programs as " + "administrator " + "is typically unnecessary as long as the game and Mod Organizer have " + "been " "installed outside \"Program Files\".\r\n\r\n" "You can restart Mod Organizer as administrator and try launching the " "program again."); @@ -344,24 +350,24 @@ bool confirmRestartAsAdmin(QWidget* parent, const SpawnParameters& sp) .details(details) .icon(QMessageBox::Question) .button({QObject::tr("Restart Mod Organizer as administrator"), - QObject::tr( - "You must allow \"helper.exe\" to make changes to the system."), + QObject::tr("You must allow \"helper.exe\" to make changes " + "to the system."), QMessageBox::Yes}) .button({QObject::tr("Cancel"), QMessageBox::Cancel}) .exec(); return (r == QMessageBox::Yes); } -#endif // _WIN32 +#endif // _WIN32 -QMessageBox::StandardButton -confirmStartSteam(QWidget* parent, const SpawnParameters& sp, const QString& details) -{ - const auto title = QObject::tr("Launch Steam"); +QMessageBox::StandardButton confirmStartSteam(QWidget *parent, + const SpawnParameters &sp, + const QString &details) { + const auto title = QObject::tr("Launch Steam"); const auto mainText = QObject::tr("This program requires Steam"); - const auto content = QObject::tr( - "Mod Organizer has detected that this program likely requires Steam to be " - "running to function properly."); + const auto content = QObject::tr("Mod Organizer has detected that this " + "program likely requires Steam to be " + "running to function properly."); return MOBase::TaskDialog(parent, title) .main(mainText) @@ -377,17 +383,16 @@ confirmStartSteam(QWidget* parent, const SpawnParameters& sp, const QString& det } #ifdef _WIN32 -QMessageBox::StandardButton confirmRestartAsAdminForSteam(QWidget* parent, - const SpawnParameters& sp) -{ - const auto title = QObject::tr("Elevation required"); +QMessageBox::StandardButton +confirmRestartAsAdminForSteam(QWidget *parent, const SpawnParameters &sp) { + const auto title = QObject::tr("Elevation required"); const auto mainText = QObject::tr("Steam is running as administrator"); - const auto content = QObject::tr( + const auto content = QObject::tr( "Running Steam as administrator is typically unnecessary and can cause " - "problems when Mod Organizer itself is not running as administrator." - "\r\n\r\n" - "You can restart Mod Organizer as administrator and try launching the " - "program again."); + "problems when Mod Organizer itself is not running as administrator." + "\r\n\r\n" + "You can restart Mod Organizer as administrator and try launching the " + "program again."); return MOBase::TaskDialog(parent, title) .main(mainText) @@ -395,24 +400,25 @@ QMessageBox::StandardButton confirmRestartAsAdminForSteam(QWidget* parent, .icon(QMessageBox::Question) .button( {QObject::tr("Restart Mod Organizer as administrator"), - QObject::tr("You must allow \"helper.exe\" to make changes to the system."), + QObject::tr( + "You must allow \"helper.exe\" to make changes to the system."), QMessageBox::Yes}) - .button({QObject::tr("Continue"), QObject::tr("The program might fail to run."), - QMessageBox::No}) + .button({QObject::tr("Continue"), + QObject::tr("The program might fail to run."), QMessageBox::No}) .button({QObject::tr("Cancel"), QMessageBox::Cancel}) .remember("steamAdminQuery", sp.binary.fileName()) .exec(); } -bool eventLogNotRunning(QWidget* parent, const env::Service& s, - const SpawnParameters& sp) -{ - const auto title = QObject::tr("Event Log not running"); +bool eventLogNotRunning(QWidget *parent, const env::Service &s, + const SpawnParameters &sp) { + const auto title = QObject::tr("Event Log not running"); const auto mainText = QObject::tr("The Event Log service is not running"); - const auto content = QObject::tr( - "The Windows Event Log service is not running. This can prevent USVFS from " - "running properly and your mods may not be recognized by the program being " - "launched."); + const auto content = QObject::tr("The Windows Event Log service is not " + "running. This can prevent USVFS from " + "running properly and your mods may not be " + "recognized by the program being " + "launched."); const auto r = MOBase::TaskDialog(parent, title) @@ -421,18 +427,18 @@ bool eventLogNotRunning(QWidget* parent, const env::Service& s, .details(s.toString()) .icon(QMessageBox::Question) .remember("eventLogService", sp.binary.fileName()) - .button({QObject::tr("Continue"), QObject::tr("Your mods might not work."), - QMessageBox::Yes}) + .button({QObject::tr("Continue"), + QObject::tr("Your mods might not work."), QMessageBox::Yes}) .button({QObject::tr("Cancel"), QMessageBox::Cancel}) .exec(); return (r == QMessageBox::Yes); } -#endif // _WIN32 +#endif // _WIN32 -QMessageBox::StandardButton -confirmBlacklisted(QWidget* parent, const SpawnParameters& sp, Settings& settings) -{ +QMessageBox::StandardButton confirmBlacklisted(QWidget *parent, + const SpawnParameters &sp, + Settings &settings) { const auto title = QObject::tr("Blacklisted program"); const auto mainText = QObject::tr("The program %1 is blacklisted").arg(sp.binary.fileName()); @@ -446,17 +452,18 @@ confirmBlacklisted(QWidget* parent, const SpawnParameters& sp, Settings& setting "Current blacklist: " + settings.executablesBlacklist(); - auto r = MOBase::TaskDialog(parent, title) - .main(mainText) - .content(content) - .details(details) - .icon(QMessageBox::Question) - .remember("blacklistedExecutable", sp.binary.fileName()) - .button({QObject::tr("Continue"), - QObject::tr("Your mods might not work."), QMessageBox::Yes}) - .button({QObject::tr("Change the blacklist"), QMessageBox::Retry}) - .button({QObject::tr("Cancel"), QMessageBox::Cancel}) - .exec(); + auto r = + MOBase::TaskDialog(parent, title) + .main(mainText) + .content(content) + .details(details) + .icon(QMessageBox::Question) + .remember("blacklistedExecutable", sp.binary.fileName()) + .button({QObject::tr("Continue"), + QObject::tr("Your mods might not work."), QMessageBox::Yes}) + .button({QObject::tr("Change the blacklist"), QMessageBox::Retry}) + .button({QObject::tr("Cancel"), QMessageBox::Cancel}) + .exec(); if (r == QMessageBox::Retry) { if (!WorkaroundsSettingsTab::changeBlacklistNow(parent, settings)) { @@ -467,13 +474,11 @@ confirmBlacklisted(QWidget* parent, const SpawnParameters& sp, Settings& setting return r; } -} // namespace spawn::dialogs +} // namespace spawn::dialogs -namespace spawn -{ +namespace spawn { -void logSpawning(const SpawnParameters& sp, const QString& realCmd) -{ +void logSpawning(const SpawnParameters &sp, const QString &realCmd) { log::debug("spawning binary:\n" " . exe: '{}'\n" " . args: '{}'\n" @@ -489,23 +494,21 @@ void logSpawning(const SpawnParameters& sp, const QString& realCmd) (sp.stdOut == INVALID_HANDLE_VALUE ? "no" : "yes"), (sp.stdErr == INVALID_HANDLE_VALUE ? "no" : "yes"), #else - (sp.stdOut == -1 ? "no" : "yes"), - (sp.stdErr == -1 ? "no" : "yes"), + (sp.stdOut == -1 ? "no" : "yes"), (sp.stdErr == -1 ? "no" : "yes"), #endif realCmd); } #ifndef _WIN32 -uint32_t parseSteamAppId(const QString& steamAppId) -{ - bool ok = false; +uint32_t parseSteamAppId(const QString &steamAppId) { + bool ok = false; const auto n = steamAppId.toUInt(&ok); return (ok ? n : 0u); } -QString firstExistingSetting(const QSettings& settings, const QStringList& keys) -{ - for (const QString& key : keys) { +QString firstExistingSetting(const QSettings &settings, + const QStringList &keys) { + for (const QString &key : keys) { const QString value = settings.value(key).toString().trimmed(); if (!value.isEmpty()) { return value; @@ -515,13 +518,13 @@ QString firstExistingSetting(const QSettings& settings, const QStringList& keys) return {}; } -QString resolvePrefixPath() -{ - if (auto cfg = FluorineConfig::load(); cfg.has_value() && cfg->prefixExists()) { +QString resolvePrefixPath() { + if (auto cfg = FluorineConfig::load(); + cfg.has_value() && cfg->prefixExists()) { return cfg->prefix_path.trimmed(); } - const Settings* settings = Settings::maybeInstance(); + const Settings *settings = Settings::maybeInstance(); if (settings == nullptr) { return {}; } @@ -532,8 +535,7 @@ QString resolvePrefixPath() "Proton/prefix_path", "fluorine/prefix_path"}); } -QString resolveProtonPath() -{ +QString resolveProtonPath() { if (auto cfg = FluorineConfig::load(); cfg.has_value()) { const QString protonPath = cfg->proton_path.trimmed(); if (!protonPath.isEmpty()) { @@ -541,35 +543,34 @@ QString resolveProtonPath() } } - const Settings* settings = Settings::maybeInstance(); + const Settings *settings = Settings::maybeInstance(); if (settings == nullptr) { return {}; } const QSettings instanceSettings(settings->filename(), QSettings::IniFormat); - return firstExistingSetting(instanceSettings, - {"Settings/proton_path", "Proton/path", - "fluorine/proton_path"}); + return firstExistingSetting( + instanceSettings, + {"Settings/proton_path", "Proton/path", "fluorine/proton_path"}); } #endif #ifdef _WIN32 -DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle) -{ +DWORD spawn(const SpawnParameters &sp, HANDLE &processHandle) { BOOL inheritHandles = FALSE; STARTUPINFO si = {}; - si.cb = sizeof(si); + si.cb = sizeof(si); // inherit handles if we plan to use stdout or stderr reroute if (sp.stdOut != INVALID_HANDLE_VALUE) { - si.hStdOutput = sp.stdOut; + si.hStdOutput = sp.stdOut; inheritHandles = TRUE; si.dwFlags |= STARTF_USESTDHANDLES; } if (sp.stdErr != INVALID_HANDLE_VALUE) { - si.hStdError = sp.stdErr; + si.hStdError = sp.stdErr; inheritHandles = TRUE; si.dwFlags |= STARTF_USESTDHANDLES; } @@ -583,26 +584,26 @@ DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle) } const QString moPath = QCoreApplication::applicationDirPath(); - const auto oldPath = env::appendToPath(QDir::toNativeSeparators(moPath)); + const auto oldPath = env::appendToPath(QDir::toNativeSeparators(moPath)); PROCESS_INFORMATION pi = {}; - BOOL success = FALSE; + BOOL success = FALSE; logSpawning(sp, commandLine); const auto wcommandLine = commandLine.toStdWString(); - const auto wcwd = cwd.toStdWString(); + const auto wcwd = cwd.toStdWString(); const DWORD flags = CREATE_BREAKAWAY_FROM_JOB; if (sp.hooked) { success = ::usvfsCreateProcessHooked( - nullptr, const_cast(wcommandLine.c_str()), nullptr, nullptr, + nullptr, const_cast(wcommandLine.c_str()), nullptr, nullptr, inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi); } else { - success = ::CreateProcess(nullptr, const_cast(wcommandLine.c_str()), - nullptr, nullptr, inheritHandles, flags, nullptr, - wcwd.c_str(), &si, &pi); + success = ::CreateProcess( + nullptr, const_cast(wcommandLine.c_str()), nullptr, nullptr, + inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi); } const auto e = GetLastError(); @@ -618,10 +619,11 @@ DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle) return ERROR_SUCCESS; } #else -int spawn(const SpawnParameters& sp, pid_t& processId) -{ - const QString bin = MOBase::normalizePathForHost(sp.binary.absoluteFilePath()); - QString cwd = MOBase::normalizePathForHost(sp.currentDirectory.absolutePath()); +int spawn(const SpawnParameters &sp, pid_t &processId) { + const QString bin = + MOBase::normalizePathForHost(sp.binary.absoluteFilePath()); + QString cwd = + MOBase::normalizePathForHost(sp.currentDirectory.absolutePath()); QStringList argList; if (!sp.arguments.isEmpty()) { @@ -634,6 +636,21 @@ int spawn(const SpawnParameters& sp, pid_t& processId) logSpawning(sp, bin + " " + sp.arguments); + // Read per-instance settings from the instance INI (not the global + // QSettings). + const Settings *instanceForLaunch = Settings::maybeInstance(); + bool useSteamDrm = true; // default + QString storeVariant; + if (instanceForLaunch) { + const QSettings instanceIni(instanceForLaunch->filename(), + QSettings::IniFormat); + useSteamDrm = instanceIni.value("fluorine/steam_drm", true).toBool(); + // settingName() maps "General" section keys to top-level (no section + // prefix), so game_edition is stored as a plain key under [General] in the + // INI. + storeVariant = instanceIni.value("game_edition").toString().trimmed(); + } + ProtonLauncher launcher; launcher.setBinary(bin) .setArguments(argList) @@ -644,14 +661,18 @@ int spawn(const SpawnParameters& sp, pid_t& processId) QSettings().value("fluorine/prefer_system_umu", false).toBool()) .setUseSteamRun( QSettings().value("fluorine/use_steam_run", false).toBool()) - .setSteamDrm(QSettings().value("fluorine/steam_drm", true).toBool()); + .setSteamDrm(useSteamDrm) + .setStoreVariant(storeVariant); const QString prefixPath = resolvePrefixPath(); if (prefixPath.isEmpty()) { - MOBase::log::warn("No Wine prefix configured - games may not launch correctly. " - "Configure a prefix in Settings > Proton or via fluorine-manager."); + MOBase::log::warn( + "No Wine prefix configured - games may not launch correctly. " + "Configure a prefix in Settings > Proton or via fluorine-manager."); } else if (!QDir(QDir(prefixPath).filePath("drive_c")).exists()) { - MOBase::log::warn("Wine prefix '{}' does not contain drive_c/ - prefix may be invalid", prefixPath); + MOBase::log::warn( + "Wine prefix '{}' does not contain drive_c/ - prefix may be invalid", + prefixPath); } else { MOBase::log::info("Using Wine prefix: {}", prefixPath); launcher.setPrefix(prefixPath); @@ -662,7 +683,8 @@ int spawn(const SpawnParameters& sp, pid_t& processId) launcher.setProtonPath(protonPath); } - const QString wrapper = QSettings().value("fluorine/launch_wrapper").toString().trimmed(); + const QString wrapper = + QSettings().value("fluorine/launch_wrapper").toString().trimmed(); if (!wrapper.isEmpty()) { launcher.setWrapper(wrapper); } @@ -678,8 +700,7 @@ int spawn(const SpawnParameters& sp, pid_t& processId) #endif #ifdef _WIN32 -bool restartAsAdmin(QWidget* parent) -{ +bool restartAsAdmin(QWidget *parent) { WCHAR cwd[MAX_PATH] = {}; if (!GetCurrentDirectory(MAX_PATH, cwd)) { cwd[0] = L'\0'; @@ -698,8 +719,7 @@ bool restartAsAdmin(QWidget* parent) return true; } -void startBinaryAdmin(QWidget* parent, const SpawnParameters& sp) -{ +void startBinaryAdmin(QWidget *parent, const SpawnParameters &sp) { if (!dialogs::confirmRestartAsAdmin(parent, sp)) { log::debug("user declined"); return; @@ -708,25 +728,23 @@ void startBinaryAdmin(QWidget* parent, const SpawnParameters& sp) log::info("restarting MO as administrator"); restartAsAdmin(parent); } -#endif // _WIN32 +#endif // _WIN32 -struct SteamStatus -{ - bool running = false; +struct SteamStatus { + bool running = false; bool accessible = false; }; -SteamStatus getSteamStatus() -{ +SteamStatus getSteamStatus() { SteamStatus ss; #ifdef _WIN32 const auto ps = env::Environment().runningProcesses(); - for (const auto& p : ps) { + for (const auto &p : ps) { if ((p.name().compare("Steam.exe", Qt::CaseInsensitive) == 0) || (p.name().compare("SteamService.exe", Qt::CaseInsensitive) == 0)) { - ss.running = true; + ss.running = true; ss.accessible = p.canAccess(); log::debug("'{}' is running, accessible={}", p.name(), @@ -742,7 +760,7 @@ SteamStatus getSteamStatus() pgrep.waitForFinished(3000); if (pgrep.exitCode() == 0) { - ss.running = true; + ss.running = true; ss.accessible = true; log::debug("steam is running"); } @@ -751,8 +769,7 @@ SteamStatus getSteamStatus() return ss; } -QString makeSteamArguments(const QString& username, const QString& password) -{ +QString makeSteamArguments(const QString &username, const QString &password) { QString args; if (username != "") { @@ -766,17 +783,17 @@ QString makeSteamArguments(const QString& username, const QString& password) return args; } -bool startSteam(QWidget* parent) -{ +bool startSteam(QWidget *parent) { #ifdef _WIN32 - const QString keyName = "HKEY_CURRENT_USER\\Software\\Valve\\Steam"; + const QString keyName = "HKEY_CURRENT_USER\\Software\\Valve\\Steam"; const QString valueName = "SteamExe"; const QSettings steamSettings(keyName, QSettings::NativeFormat); const QString exe = steamSettings.value(valueName, "").toString(); if (exe.isEmpty()) { - return (dialogs::badSteamReg(parent, keyName, valueName) == QMessageBox::Yes); + return (dialogs::badSteamReg(parent, keyName, valueName) == + QMessageBox::Yes); } SpawnParameters sp; @@ -797,10 +814,11 @@ bool startSteam(QWidget* parent) log::debug("starting steam process:\n" " . program: '{}'\n" " . username={}, password={}", - sp.binary.filePath().toStdString(), (username.isEmpty() ? "no" : "yes"), + sp.binary.filePath().toStdString(), + (username.isEmpty() ? "no" : "yes"), (password.isEmpty() ? "no" : "yes")); - HANDLE ph = INVALID_HANDLE_VALUE; + HANDLE ph = INVALID_HANDLE_VALUE; const auto e = spawn(sp, ph); ::CloseHandle(ph); @@ -809,7 +827,8 @@ bool startSteam(QWidget* parent) sp.arguments = makeSteamArguments((username.isEmpty() ? "" : "USERNAME"), (password.isEmpty() ? "" : "PASSWORD")); - const auto r = dialogs::startSteamFailed(parent, keyName, valueName, exe, sp, e); + const auto r = + dialogs::startSteamFailed(parent, keyName, valueName, exe, sp, e); return (r == QMessageBox::Yes); } @@ -840,10 +859,11 @@ bool startSteam(QWidget* parent) MOBase::TaskDialog(parent, title) .main(title) .content(QObject::tr("The Steam executable could not be found. " - "Make sure Steam is installed.")) + "Make sure Steam is installed.")) .icon(QMessageBox::Critical) .button({QObject::tr("Continue without starting Steam"), - QObject::tr("The program may fail to launch."), QMessageBox::Yes}) + QObject::tr("The program may fail to launch."), + QMessageBox::Yes}) .button({QObject::tr("Cancel"), QMessageBox::Cancel}) .exec(); @@ -869,11 +889,12 @@ bool startSteam(QWidget* parent) #endif } -bool checkSteam(QWidget* parent, const SpawnParameters& sp, const QDir& gameDirectory, - const QString& steamAppID, const Settings& settings) -{ +bool checkSteam(QWidget *parent, const SpawnParameters &sp, + const QDir &gameDirectory, const QString &steamAppID, + const Settings &settings) { #ifdef _WIN32 - static const std::vector steamFiles = {"steam_api.dll", "steam_api64.dll"}; + static const std::vector steamFiles = {"steam_api.dll", + "steam_api64.dll"}; #else static const std::vector steamFiles = {"libsteam_api.so"}; #endif @@ -889,7 +910,7 @@ bool checkSteam(QWidget* parent, const SpawnParameters& sp, const QDir& gameDire bool steamRequired = false; QString details; - for (const auto& file : steamFiles) { + for (const auto &file : steamFiles) { const QFileInfo fi(gameDirectory.absoluteFilePath(file)); if (fi.exists()) { details = QString("managed game is located at '%1' and file '%2' exists") @@ -959,8 +980,8 @@ bool checkSteam(QWidget* parent, const SpawnParameters& sp, const QDir& gameDire return true; } -bool checkBlacklist(QWidget* parent, const SpawnParameters& sp, Settings& settings) -{ +bool checkBlacklist(QWidget *parent, const SpawnParameters &sp, + Settings &settings) { for (;;) { if (!settings.isExecutableBlacklisted(sp.binary.fileName())) { return true; @@ -975,10 +996,9 @@ bool checkBlacklist(QWidget* parent, const SpawnParameters& sp, Settings& settin } #ifdef _WIN32 -HANDLE startBinary(QWidget* parent, const SpawnParameters& sp) -{ +HANDLE startBinary(QWidget *parent, const SpawnParameters &sp) { HANDLE handle = INVALID_HANDLE_VALUE; - const auto e = spawn::spawn(sp, handle); + const auto e = spawn::spawn(sp, handle); switch (e) { case ERROR_SUCCESS: { @@ -997,8 +1017,7 @@ HANDLE startBinary(QWidget* parent, const SpawnParameters& sp) } } #else -pid_t startBinary(QWidget* parent, const SpawnParameters& sp) -{ +pid_t startBinary(QWidget *parent, const SpawnParameters &sp) { pid_t pid = -1; const auto e = spawn::spawn(sp, pid); @@ -1012,19 +1031,18 @@ pid_t startBinary(QWidget* parent, const SpawnParameters& sp) #endif #ifdef _WIN32 -QString getExecutableForJarFile(const QString& jarFile) -{ +QString getExecutableForJarFile(const QString &jarFile) { const std::wstring jarFileW = jarFile.toStdWString(); WCHAR buffer[MAX_PATH]; const auto hinst = ::FindExecutableW(jarFileW.c_str(), nullptr, buffer); - const auto r = static_cast(reinterpret_cast(hinst)); + const auto r = static_cast(reinterpret_cast(hinst)); // anything <= 32 signals failure if (r <= 32) { - log::warn("failed to find executable associated with file '{}', {}", jarFile, - shell::formatError(r)); + log::warn("failed to find executable associated with file '{}', {}", + jarFile, shell::formatError(r)); return {}; } @@ -1050,8 +1068,7 @@ QString getExecutableForJarFile(const QString& jarFile) return QString::fromWCharArray(buffer); } -QString getJavaHome() -{ +QString getJavaHome() { const QString key = "HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment"; const QString value = "CurrentVersion"; @@ -1064,11 +1081,12 @@ QString getJavaHome() } const QString currentVersion = reg.value("CurrentVersion").toString(); - const QString javaHome = QString("%1/JavaHome").arg(currentVersion); + const QString javaHome = QString("%1/JavaHome").arg(currentVersion); if (!reg.contains(javaHome)) { - log::warn("java version '{}' was found at '{}\\{}', but '{}\\{}' doesn't exist", - currentVersion, key, value, key, javaHome); + log::warn( + "java version '{}' was found at '{}\\{}', but '{}\\{}' doesn't exist", + currentVersion, key, value, key, javaHome); return {}; } @@ -1076,10 +1094,9 @@ QString getJavaHome() const auto path = reg.value(javaHome).toString(); return path + "\\bin\\javaw.exe"; } -#endif // _WIN32 +#endif // _WIN32 -QString findJavaInstallation(const QString& jarFile) -{ +QString findJavaInstallation(const QString &jarFile) { #ifdef _WIN32 // try to find java automatically based on the given jar file if (!jarFile.isEmpty()) { @@ -1108,8 +1125,7 @@ QString findJavaInstallation(const QString& jarFile) return {}; } -bool isBatchFile(const QFileInfo& target) -{ +bool isBatchFile(const QFileInfo &target) { #ifdef _WIN32 const auto batchExtensions = {"cmd", "bat"}; #else @@ -1117,7 +1133,7 @@ bool isBatchFile(const QFileInfo& target) #endif const QString extension = target.suffix(); - for (auto&& e : batchExtensions) { + for (auto &&e : batchExtensions) { if (extension.compare(e, Qt::CaseInsensitive) == 0) { return true; } @@ -1126,8 +1142,7 @@ bool isBatchFile(const QFileInfo& target) return false; } -bool isExeFile(const QFileInfo& target) -{ +bool isExeFile(const QFileInfo &target) { #ifdef _WIN32 return (target.suffix().compare("exe", Qt::CaseInsensitive) == 0); #else @@ -1136,13 +1151,11 @@ bool isExeFile(const QFileInfo& target) #endif } -bool isJavaFile(const QFileInfo& target) -{ +bool isJavaFile(const QFileInfo &target) { return (target.suffix().compare("jar", Qt::CaseInsensitive) == 0); } -QFileInfo getCmdPath() -{ +QFileInfo getCmdPath() { #ifdef _WIN32 const auto p = env::get("COMSPEC"); if (!p.isEmpty()) { @@ -1151,7 +1164,7 @@ QFileInfo getCmdPath() QString systemDirectory; - const std::size_t buffer_size = 1000; + const std::size_t buffer_size = 1000; wchar_t buffer[buffer_size + 1] = {}; const auto length = ::GetSystemDirectoryW(buffer, buffer_size); @@ -1176,8 +1189,7 @@ QFileInfo getCmdPath() #endif } -FileExecutionTypes getFileExecutionType(const QFileInfo& target) -{ +FileExecutionTypes getFileExecutionType(const QFileInfo &target) { if (isExeFile(target) || isBatchFile(target) || isJavaFile(target)) { return FileExecutionTypes::Executable; } @@ -1185,23 +1197,21 @@ FileExecutionTypes getFileExecutionType(const QFileInfo& target) return FileExecutionTypes::Other; } -FileExecutionContext getFileExecutionContext(QWidget* parent, const QFileInfo& target) -{ +FileExecutionContext getFileExecutionContext(QWidget *parent, + const QFileInfo &target) { if (isExeFile(target)) { return {target, "", FileExecutionTypes::Executable}; } if (isBatchFile(target)) { #ifdef _WIN32 - return { - getCmdPath(), - QString("/C \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())), - FileExecutionTypes::Executable}; + return {getCmdPath(), + QString("/C \"%1\"") + .arg(QDir::toNativeSeparators(target.absoluteFilePath())), + FileExecutionTypes::Executable}; #else - return { - getCmdPath(), - QString("\"%1\"").arg(target.absoluteFilePath()), - FileExecutionTypes::Executable}; + return {getCmdPath(), QString("\"%1\"").arg(target.absoluteFilePath()), + FileExecutionTypes::Executable}; #endif } @@ -1210,13 +1220,13 @@ FileExecutionContext getFileExecutionContext(QWidget* parent, const QFileInfo& t if (java.isEmpty()) { #ifdef _WIN32 - java = - QFileDialog::getOpenFileName(parent, QObject::tr("Select binary"), QString(), - QObject::tr("Binary") + " (*.exe)"); + java = QFileDialog::getOpenFileName(parent, QObject::tr("Select binary"), + QString(), + QObject::tr("Binary") + " (*.exe)"); #else - java = - QFileDialog::getOpenFileName(parent, QObject::tr("Select binary"), QString(), - QObject::tr("Binary") + " (*)"); + java = QFileDialog::getOpenFileName(parent, QObject::tr("Select binary"), + QString(), + QObject::tr("Binary") + " (*)"); #endif } @@ -1231,15 +1241,13 @@ FileExecutionContext getFileExecutionContext(QWidget* parent, const QFileInfo& t return {{}, {}, FileExecutionTypes::Other}; } -} // namespace spawn +} // namespace spawn #ifdef _WIN32 -namespace helper -{ +namespace helper { -bool helperExec(QWidget* parent, const std::wstring& moDirectory, - const std::wstring& commandLine, BOOL async) -{ +bool helperExec(QWidget *parent, const std::wstring &moDirectory, + const std::wstring &commandLine, BOOL async) { const std::wstring fileName = moDirectory + L"\\helper.exe"; env::HandlePtr process; @@ -1251,14 +1259,14 @@ bool helperExec(QWidget* parent, const std::wstring& moDirectory, if (!async) flags |= SEE_MASK_NOCLOSEPROCESS; - execInfo.cbSize = sizeof(SHELLEXECUTEINFOW); - execInfo.fMask = flags; - execInfo.hwnd = 0; - execInfo.lpVerb = L"runas"; - execInfo.lpFile = fileName.c_str(); + execInfo.cbSize = sizeof(SHELLEXECUTEINFOW); + execInfo.fMask = flags; + execInfo.hwnd = 0; + execInfo.lpVerb = L"runas"; + execInfo.lpFile = fileName.c_str(); execInfo.lpParameters = commandLine.c_str(); - execInfo.lpDirectory = moDirectory.c_str(); - execInfo.nShow = SW_SHOW; + execInfo.lpDirectory = moDirectory.c_str(); + execInfo.nShow = SW_SHOW; if (!::ShellExecuteExW(&execInfo) && execInfo.hProcess == 0) { const auto e = GetLastError(); @@ -1282,10 +1290,11 @@ bool helperExec(QWidget* parent, const std::wstring& moDirectory, // for WAIT_ABANDONED, the documentation doesn't mention that GetLastError() // returns something meaningful, but code ERROR_ABANDONED_WAIT_0 exists, so // use that instead - const auto code = (r == WAIT_ABANDONED ? ERROR_ABANDONED_WAIT_0 : GetLastError()); + const auto code = + (r == WAIT_ABANDONED ? ERROR_ABANDONED_WAIT_0 : GetLastError()); - spawn::dialogs::helperFailed(parent, code, "WaitForSingleObject()", fileName, - moDirectory, commandLine); + spawn::dialogs::helperFailed(parent, code, "WaitForSingleObject()", + fileName, moDirectory, commandLine); return false; } @@ -1303,22 +1312,21 @@ bool helperExec(QWidget* parent, const std::wstring& moDirectory, return (exitCode == 0); } -bool backdateBSAs(QWidget* parent, const std::wstring& moPath, - const std::wstring& dataPath) -{ +bool backdateBSAs(QWidget *parent, const std::wstring &moPath, + const std::wstring &dataPath) { const std::wstring commandLine = std::format(L"backdateBSA \"{}\"", dataPath); return helperExec(parent, moPath, commandLine, FALSE); } -bool adminLaunch(QWidget* parent, const std::wstring& moPath, - const std::wstring& moFile, const std::wstring& workingDir) -{ - const std::wstring commandLine = std::format( - L"adminLaunch {} \"{}\" \"{}\"", ::GetCurrentProcessId(), moFile, workingDir); +bool adminLaunch(QWidget *parent, const std::wstring &moPath, + const std::wstring &moFile, const std::wstring &workingDir) { + const std::wstring commandLine = + std::format(L"adminLaunch {} \"{}\" \"{}\"", ::GetCurrentProcessId(), + moFile, workingDir); return helperExec(parent, moPath, commandLine, true); } -} // namespace helper -#endif // _WIN32 +} // namespace helper +#endif // _WIN32 diff --git a/src/src/wineprefix.cpp b/src/src/wineprefix.cpp index bb4e166..e144460 100644 --- a/src/src/wineprefix.cpp +++ b/src/src/wineprefix.cpp @@ -10,10 +10,17 @@ #include #include +#ifndef _WIN32 +#include +#include +#endif + namespace { constexpr const char* BackupIniSuffix = ".mo2linux_backup"; +// Copy a file, preserving its modification time so that games see the +// original save timestamps rather than "now". bool copyFileWithParents(const QString& source, const QString& destination) { const QFileInfo destinationInfo(destination); @@ -25,7 +32,23 @@ bool copyFileWithParents(const QString& source, const QString& destination) return false; } - return QFile::copy(source, destination); + if (!QFile::copy(source, destination)) { + return false; + } + +#ifndef _WIN32 + // QFile::copy() does not preserve timestamps. Restore the source file's + // mtime so that games display the correct save date. + struct stat srcStat; + if (stat(source.toUtf8().constData(), &srcStat) == 0) { + struct utimbuf times; + times.actime = srcStat.st_atime; + times.modtime = srcStat.st_mtime; + utime(destination.toUtf8().constData(), ×); + } +#endif + + return true; } bool copyTreeContents(const QString& sourceRoot, const QString& destinationRoot) @@ -49,16 +72,22 @@ bool restoreBackedUpSaves(const QString& liveUpper, const QString& liveLower, const QString& backupUpper, const QString& backupLower) { if (QDir(liveUpper).exists() && !QDir(liveUpper).removeRecursively()) { + MOBase::log::warn("restoreBackedUpSaves: failed to remove '{}'", liveUpper); return false; } if (QDir(liveLower).exists() && !QDir(liveLower).removeRecursively()) { + MOBase::log::warn("restoreBackedUpSaves: failed to remove '{}'", liveLower); return false; } if (QDir(backupUpper).exists() && !QDir().rename(backupUpper, liveUpper)) { + MOBase::log::warn("restoreBackedUpSaves: failed to rename '{}' -> '{}'", + backupUpper, liveUpper); return false; } if (QDir(backupLower).exists() && !QDir().rename(backupLower, liveLower)) { + MOBase::log::warn("restoreBackedUpSaves: failed to rename '{}' -> '{}'", + backupLower, liveLower); return false; } @@ -296,16 +325,21 @@ bool WinePrefix::deployProfileSaves(const QString& profileSaveDir, if ((QDir(backupUpper).exists() || QDir(backupLower).exists()) && !restoreBackedUpSaves(absoluteSaveDir, lowerSaveDir, backupUpper, backupLower)) { + MOBase::log::warn("deployProfileSaves: failed to restore stale backup"); return false; } if (QDir(absoluteSaveDir).exists() && !QDir().rename(absoluteSaveDir, backupUpper)) { + MOBase::log::warn("deployProfileSaves: failed to backup '{}' -> '{}'", + absoluteSaveDir, backupUpper); return false; } if (absoluteSaveDir != lowerSaveDir && QDir(lowerSaveDir).exists() && !QDir().rename(lowerSaveDir, backupLower)) { + MOBase::log::warn("deployProfileSaves: failed to backup '{}' -> '{}'", + lowerSaveDir, backupLower); return false; } } -- cgit v1.3.1