aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-20 23:53:26 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-20 23:53:26 -0600
commitd22d8c267b012729b6b84142def2a30ca4691d7a (patch)
tree4d04382b6c9a44fa72ade906e0bf51e8f97855a8 /src
parent5467e210668eb24de9a4429ef0ddcbc6442e8932 (diff)
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 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/src/commandline.cpp3
-rw-r--r--src/src/mainwindow.cpp162
-rw-r--r--src/src/moapplication.cpp129
-rw-r--r--src/src/organizercore.cpp129
-rw-r--r--src/src/plugincontainer.cpp326
-rw-r--r--src/src/processrunner.cpp412
-rw-r--r--src/src/protonlauncher.cpp201
-rw-r--r--src/src/protonlauncher.h2
-rw-r--r--src/src/savestab.cpp655
-rw-r--r--src/src/shared/appconfig.cpp22
-rw-r--r--src/src/shared/appconfig.h10
-rw-r--r--src/src/spawn.cpp550
-rw-r--r--src/src/wineprefix.cpp36
13 files changed, 1427 insertions, 1210 deletions
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<int> ReloadPluginCommand::runPostOrganizer(OrganizerCore& core)
const QString name = QString::fromStdString(vm()["PLUGIN"].as<std::string>());
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("<Edit...>"), {});
-
- 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("<Edit...>"), {});
+
+ 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 <http://www.gnu.org/licenses/>.
#include "shared/util.h"
#include "thread_utils.h"
#include "tutorialmanager.h"
-#include <QDebug>
-#include <QFile>
-#include <QPainter>
+#include <QDebug>
+#include <QFile>
+#include <QPainter>
#include <QProxyStyle>
#include <QSslSocket>
#include <QStringList>
@@ -51,10 +51,10 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QStyleOption>
#include <iplugingame.h>
#include <log.h>
-#include <report.h>
-#include <scopeguard.h>
-#include <utility.h>
-#include <cstdio>
+#include <report.h>
+#include <scopeguard.h>
+#include <utility.h>
+#include <cstdio>
#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<PluginContainer>(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<PluginContainer>(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<LocalSavegames>();
- 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<LocalSavegames>();
+ 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<QPair<QString, QString>> 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 <QAction>
-#include <QCoreApplication>
-#include <QDirIterator>
-#include <QMessageBox>
-#include <QToolButton>
-#include <cstdio>
+#include <QAction>
+#include <QCoreApplication>
+#include <QDirIterator>
+#include <QMessageBox>
+#include <QToolButton>
+#include <cstdio>
#include <boost/fusion/algorithm/iteration/for_each.hpp>
#include <boost/fusion/include/at_key.hpp>
#include <boost/fusion/include/for_each.hpp>
@@ -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<IPluginProxy*> proxyList = bf::at_key<IPluginProxy>(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<IPluginProxy*>(plugin);
- if (initPlugin(proxy, pluginProxy, skipInit)) {
- bf::at_key<IPluginProxy>(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<IPluginProxy*>(plugin);
+ if (initPlugin(proxy, pluginProxy, skipInit)) {
+ bf::at_key<IPluginProxy>(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<QObject*>& plugins) con
}
}
-std::vector<QObject*> PluginContainer::loadProxied(const QString& filepath,
- IPluginProxy* proxy)
-{
- std::vector<QObject*> proxiedPlugins;
-
- try {
- log::warn("loading proxied plugin candidate '{}' via proxy '{}'",
- QDir::toNativeSeparators(filepath),
- (proxy ? proxy->name() : QStringLiteral("<null>")));
- printPluginDiagToStderr(
- QString("loading proxied plugin candidate '%1' via proxy '%2'")
- .arg(QDir::toNativeSeparators(filepath))
- .arg(proxy ? proxy->name() : QStringLiteral("<null>")));
-
- // 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<QObject*> 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<QString, std::vector<IPlugin*>> 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<QObject*> PluginContainer::loadProxied(const QString& filepath,
+ IPluginProxy* proxy)
+{
+ std::vector<QObject*> proxiedPlugins;
+
+ try {
+ log::warn("loading proxied plugin candidate '{}' via proxy '{}'",
+ QDir::toNativeSeparators(filepath),
+ (proxy ? proxy->name() : QStringLiteral("<null>")));
+ printPluginDiagToStderr(
+ QString("loading proxied plugin candidate '%1' via proxy '%2'")
+ .arg(QDir::toNativeSeparators(filepath))
+ .arg(proxy ? proxy->name() : QStringLiteral("<null>")));
+
+ // 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<QObject*> 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<QString, std::vector<IPlugin*>> 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<QObject*> 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("<null>")), e.what());
- printPluginDiagToStderr(
- QString("failed to initialize proxied candidate '%1' via proxy '%2': %3")
- .arg(QDir::toNativeSeparators(filepath))
- .arg(proxy ? proxy->name() : QStringLiteral("<null>"))
- .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("<null>")));
- printPluginDiagToStderr(
- QString("failed to initialize proxied candidate '%1' via proxy '%2': unknown "
- "exception")
- .arg(QDir::toNativeSeparators(filepath))
- .arg(proxy ? proxy->name() : QStringLiteral("<null>")));
- 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("<null>")), e.what());
+ printPluginDiagToStderr(
+ QString("failed to initialize proxied candidate '%1' via proxy '%2': %3")
+ .arg(QDir::toNativeSeparators(filepath))
+ .arg(proxy ? proxy->name() : QStringLiteral("<null>"))
+ .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("<null>")));
+ printPluginDiagToStderr(
+ QString("failed to initialize proxied candidate '%1' via proxy '%2': unknown "
+ "exception")
+ .arg(QDir::toNativeSeparators(filepath))
+ .arg(proxy ? proxy->name() : QStringLiteral("<null>")));
+ 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<IPluginProxy>()) {
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<IPluginDiagnose>(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 <dirent.h>
#include <fstream>
#include <signal.h>
+#include <sys/wait.h>
#include <unordered_map>
#include <unordered_set>
-#include <sys/wait.h>
+
#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<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid)
-{
+std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid) {
if (handle == INVALID_HANDLE_VALUE) {
return ProcessRunner::Error;
}
@@ -116,7 +117,7 @@ std::optional<ProcessRunner::Results> 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<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid)
}
}
#else
-std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid)
-{
+std::optional<ProcessRunner::Results> 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<QString> 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<ProcessRunner::Results> timedWait(HANDLE handle, DWORD pid,
- UILocker::Session* ls,
+ UILocker::Session *ls,
std::chrono::milliseconds wait,
- std::atomic<bool>& interrupt)
-{
+ std::atomic<bool> &interrupt) {
using namespace std::chrono;
high_resolution_clock::time_point start;
@@ -310,7 +298,7 @@ std::optional<ProcessRunner::Results> 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<ProcessRunner::Results> timedWait(HANDLE handle, DWORD pid,
return ProcessRunner::ForceUnlocked;
}
-ProcessRunner::Results waitForProcessesThreadImpl(HANDLE job, UILocker::Session* ls,
- std::atomic<bool>& interrupt)
-{
+ProcessRunner::Results
+waitForProcessesThreadImpl(HANDLE job, UILocker::Session *ls,
+ std::atomic<bool> &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<bool>& interrupt)
-{
+void waitForProcessesThread(ProcessRunner::Results &result, HANDLE job,
+ UILocker::Session *ls,
+ std::atomic<bool> &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<HANDLE>& initialProcesses,
- UILocker::Session* ls)
-{
+ProcessRunner::Results
+waitForProcesses(const std::vector<HANDLE> &initialProcesses,
+ UILocker::Session *ls) {
if (initialProcesses.empty()) {
// nothing to wait for
return ProcessRunner::Completed;
@@ -428,7 +416,7 @@ ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& 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<HANDLE>& initialProces
auto results = ProcessRunner::Running;
std::atomic<bool> 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<HANDLE>& initialProces
}
ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode,
- UILocker::Session* ls)
-{
+ UILocker::Session *ls) {
std::vector<HANDLE> 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<pid_t>(reinterpret_cast<intptr_t>(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<pid_t, std::vector<pid_t>> buildProcChildrenMap()
-{
+std::unordered_map<pid_t, std::vector<pid_t>> buildProcChildrenMap() {
std::unordered_map<pid_t, std::vector<pid_t>> 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<unsigned char>(*name))) {
continue;
}
@@ -556,9 +539,8 @@ std::unordered_map<pid_t, std::vector<pid_t>> buildProcChildrenMap()
return children;
}
-std::unordered_set<pid_t>
-collectDescendants(pid_t root, const std::unordered_map<pid_t, std::vector<pid_t>>& children)
-{
+std::unordered_set<pid_t> collectDescendants(
+ pid_t root, const std::unordered_map<pid_t, std::vector<pid_t>> &children) {
std::unordered_set<pid_t> out;
std::deque<pid_t> q;
q.push_back(root);
@@ -582,8 +564,8 @@ collectDescendants(pid_t root, const std::unordered_map<pid_t, std::vector<pid_t
return out;
}
-QStringList buildExpectedExecutables(const QFileInfo& binary, const QString& arguments)
-{
+QStringList buildExpectedExecutables(const QFileInfo &binary,
+ const QString &arguments) {
QStringList expected;
auto addName = [&](QString name) {
name = name.trimmed().toLower();
@@ -595,7 +577,7 @@ QStringList buildExpectedExecutables(const QFileInfo& binary, const QString& arg
addName(binary.fileName());
const auto args = QProcess::splitCommand(arguments);
- for (const QString& arg : args) {
+ for (const QString &arg : args) {
const QFileInfo fi(arg);
const QString base = fi.fileName();
if (base.endsWith(".exe", Qt::CaseInsensitive)) {
@@ -603,17 +585,18 @@ QStringList buildExpectedExecutables(const QFileInfo& binary, const QString& arg
}
}
+ log::debug("buildExpectedExecutables: returning [{}]",
+ expected.join(", ").toStdString());
return expected;
}
-pid_t findTrackedProcess(pid_t rootPid, const QStringList& expected,
- QString* trackedNameOut)
-{
+pid_t findTrackedProcess(pid_t rootPid, const QStringList &expected,
+ QString *trackedNameOut) {
if (expected.isEmpty()) {
return 0;
}
- const auto children = buildProcChildrenMap();
+ const auto children = buildProcChildrenMap();
const auto descendants = collectDescendants(rootPid, children);
if (descendants.empty()) {
return 0;
@@ -636,12 +619,16 @@ pid_t findTrackedProcess(pid_t rootPid, const QStringList& expected,
if (best > 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<DWORD>(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<DWORD>(std::max<pid_t>(0, displayPid)), displayName);
+ ls->setInfo(static_cast<DWORD>(std::max<pid_t>(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<HANDLE>& initialProcesses,
- UILocker::Session* ls,
- const QStringList& expected)
-{
+ProcessRunner::Results
+waitForProcesses(const std::vector<HANDLE> &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<HANDLE>& 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::Results> ProcessRunner::runShell()
-{
- const auto file = MOBase::normalizePathForHost(m_shellOpen.absoluteFilePath());
+std::optional<ProcessRunner::Results> ProcessRunner::runShell() {
+ const auto file =
+ MOBase::normalizePathForHost(m_shellOpen.absoluteFilePath());
log::debug("executing from shell: '{}'", file);
@@ -1122,8 +1099,7 @@ std::optional<ProcessRunner::Results> ProcessRunner::runShell()
return {};
}
-std::optional<ProcessRunner::Results> ProcessRunner::runBinary()
-{
+std::optional<ProcessRunner::Results> 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::Results> 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::Results> 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::Results> ProcessRunner::runBinary()
#ifdef _WIN32
m_handle.reset(startBinary(parent, m_sp));
#else
- m_handle.reset(reinterpret_cast<HANDLE>(static_cast<intptr_t>(startBinary(parent, m_sp))));
+ m_handle.reset(reinterpret_cast<HANDLE>(
+ static_cast<intptr_t>(startBinary(parent, m_sp))));
#endif
if (m_handle.get() == INVALID_HANDLE_VALUE) {
return Error;
@@ -1184,8 +1162,7 @@ std::optional<ProcessRunner::Results> 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<pid_t>(reinterpret_cast<intptr_t>(m_handle.get()));
+ const pid_t pid =
+ static_cast<pid_t>(reinterpret_cast<intptr_t>(m_handle.get()));
const QFileInfo binary = m_sp.binary;
QPointer<OrganizerCore> 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<HANDLE>(static_cast<intptr_t>(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<pid_t>(reinterpret_cast<intptr_t>(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<void(UILocker::Session&)> f)
-{
+void ProcessRunner::withLock(std::function<void(UILocker::Session &)> 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 <nak_ffi.h>
#include <QCoreApplication>
#include <QDir>
+#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QProcessEnvironment>
#include <QStandardPaths>
#include <log.h>
+
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", "<unset>"));
+}
+
+// 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-<AppID>" 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("<unset>")
- : QStringLiteral("umu-") +
- QString::number(effectiveSteamAppId)),
- steamPath);
+ MOBase::log::info("UMU launch: '{}' '{}' (GAMEID={}, STORE={}, steam_drm={})",
+ umuRun, m_binary,
+ env.value("GAMEID"),
+ env.value("STORE", "<unset>"),
+ 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<QString, QString> m_envVars;
QMap<QString, QString> 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 <iplugingame.h>
#include <isavegameinfowidget.h>
@@ -8,12 +8,55 @@
#include <new>
#include <report.h>
+#include <QFile>
+#include <QTextStream>
+
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<QKeyEvent*>(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<QKeyEvent*>(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<SaveGameInfo>();
-
- 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<void*>(newItem)));
-}
-
-void SavesTab::saveSelectionChanged(QTreeWidgetItem* newItem)
-{
- if (newItem == nullptr) {
- hideSaveGameInfo();
- } else if (m_CurrentSaveView == nullptr ||
- newItem != m_CurrentSaveView->property("displayItem").value<void*>()) {
- 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<LocalSavegames>() == 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<void*>(newItem)));
+}
+
+void SavesTab::saveSelectionChanged(QTreeWidgetItem* newItem)
+{
+ if (newItem == nullptr) {
+ hideSaveGameInfo();
+ } else if (m_CurrentSaveView == nullptr ||
+ newItem != m_CurrentSaveView->property("displayItem").value<void*>()) {
+ 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<LocalSavegames>() == 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<SaveGameInfo>();
-
- 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 +=
- "<li>" + QFileInfo(saveGame->getFilepath()).completeBaseName() + "</li>";
- }
- ++count;
-
- deleteFiles += saveGame->allFiles();
- }
-
- if (count > 10) {
- savesMsgLabel += "<li><i>... " + tr("%1 more").arg(count - 10) + "</i></li>";
- }
-
- if (QMessageBox::question(
- m_window, tr("Confirm"),
- tr("Are you sure you want to remove the following %n save(s)?<br>"
- "<ul>%1</ul><br>"
- "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<SaveGameInfo>();
- 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<QString> modsToActivate = dialog.getModsToActivate();
- for (std::set<QString>::iterator iter = modsToActivate.begin();
- iter != modsToActivate.end(); ++iter) {
- if ((*iter != "<data>") && (*iter != "<overwrite>")) {
- unsigned int modIndex = ModInfo::getIndex(*iter);
- m_core.currentProfile()->setModEnabled(modIndex, true);
- }
- }
-
- m_core.currentProfile()->writeModlist();
- m_core.refreshLists();
-
- std::set<QString> espsToActivate = dialog.getESPsToActivate();
- for (std::set<QString>::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<SaveGameInfo>();
-
- 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<SaveGameInfo>();
+
+ 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 +=
+ "<li>" + QFileInfo(saveGame->getFilepath()).completeBaseName() + "</li>";
+ }
+ ++count;
+
+ deleteFiles += saveGame->allFiles();
+ }
+
+ if (count > 10) {
+ savesMsgLabel += "<li><i>... " + tr("%1 more").arg(count - 10) + "</i></li>";
+ }
+
+ if (QMessageBox::question(
+ m_window, tr("Confirm"),
+ tr("Are you sure you want to remove the following %n save(s)?<br>"
+ "<ul>%1</ul><br>"
+ "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<SaveGameInfo>();
+ 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<QString> modsToActivate = dialog.getModsToActivate();
+ for (std::set<QString>::iterator iter = modsToActivate.begin();
+ iter != modsToActivate.end(); ++iter) {
+ if ((*iter != "<data>") && (*iter != "<overwrite>")) {
+ unsigned int modIndex = ModInfo::getIndex(*iter);
+ m_core.currentProfile()->setModEnabled(modIndex, true);
+ }
+ }
+
+ m_core.currentProfile()->writeModlist();
+ m_core.refreshLists();
+
+ std::set<QString> espsToActivate = dialog.getESPsToActivate();
+ for (std::set<QString>::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<SaveGameInfo>();
+
+ 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 <http://www.gnu.org/licenses/>.
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<wchar_t*>(wcommandLine.c_str()), nullptr, nullptr,
+ nullptr, const_cast<wchar_t *>(wcommandLine.c_str()), nullptr, nullptr,
inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi);
} else {
- success = ::CreateProcess(nullptr, const_cast<wchar_t*>(wcommandLine.c_str()),
- nullptr, nullptr, inheritHandles, flags, nullptr,
- wcwd.c_str(), &si, &pi);
+ success = ::CreateProcess(
+ nullptr, const_cast<wchar_t *>(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<QString> steamFiles = {"steam_api.dll", "steam_api64.dll"};
+ static const std::vector<QString> steamFiles = {"steam_api.dll",
+ "steam_api64.dll"};
#else
static const std::vector<QString> 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<int>(reinterpret_cast<std::uintptr_t>(hinst));
+ const auto r = static_cast<int>(reinterpret_cast<std::uintptr_t>(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 <log.h>
#include <uibase/filesystemutilities.h>
+#ifndef _WIN32
+#include <sys/stat.h>
+#include <utime.h>
+#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(), &times);
+ }
+#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;
}
}