aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-19 17:25:48 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-19 17:25:48 -0600
commit5467e210668eb24de9a4429ef0ddcbc6442e8932 (patch)
tree6c672183779c800963d73db5bee6742bf08c65fd /src
parent6d70b7fa6a9cdf8ea59c7aa98f92249c5a4e0060 (diff)
Add Python init diagnostics, AppImage build system, misc Linux fixes
- python: switch Py_InitializeEx → Py_InitializeFromConfig for explicit error reporting; add dladdr + fprintf diagnostics to trace which DSO Python symbols resolve to and whether Py_IsInitialized succeeds - python: log RTLD_NOLOAD vs fresh-load outcome when promoting libpython to RTLD_GLOBAL - build: add Docker-based AppImage build script (build.sh), update Dockerfile and build-inner.sh for AppImage-only workflow - build: remove Flatpak support entirely; move AppImage metadata (desktop/metainfo/icon) from flatpak/ to data/ - Add Monster Hunter Wilds basic_games plugin - .gitignore: exclude squashfs-root, build-source, flatpak-repo, __pycache__, fomod-plus-settings.ini - Various Linux port fixes across wineprefix, processrunner, proton launcher, FUSE connector, instance manager, download manager, sanitychecks, and NaK Rust paths/runtime_wrap Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/plugins/rootbuilder.py52
-rw-r--r--src/src/CMakeLists.txt45
-rw-r--r--src/src/commandline.cpp9
-rw-r--r--src/src/createinstancedialog.cpp5
-rw-r--r--src/src/createinstancedialogpages.cpp19
-rw-r--r--src/src/downloadlist.cpp12
-rw-r--r--src/src/downloadlist.h2
-rw-r--r--src/src/downloadlistview.cpp5
-rw-r--r--src/src/downloadmanager.cpp150
-rw-r--r--src/src/filedialogmemory.cpp9
-rw-r--r--src/src/fluorinepaths.cpp43
-rw-r--r--src/src/fluorinepaths.h6
-rw-r--r--src/src/fuseconnector.cpp180
-rw-r--r--src/src/fuseconnector.h11
-rw-r--r--src/src/instancemanager.cpp14
-rw-r--r--src/src/instancemanagerdialog.cpp33
-rw-r--r--src/src/instancemanagerdialog.ui10
-rw-r--r--src/src/mainwindow.cpp110
-rw-r--r--src/src/mainwindow.h7
-rw-r--r--src/src/moapplication.cpp85
-rw-r--r--src/src/nxmhandler_linux.cpp68
-rw-r--r--src/src/organizercore.cpp120
-rw-r--r--src/src/plugincontainer.cpp214
-rw-r--r--src/src/process_helper_main.cpp359
-rw-r--r--src/src/processrunner.cpp28
-rw-r--r--src/src/processrunner.h3
-rw-r--r--src/src/protonlauncher.cpp275
-rw-r--r--src/src/protonlauncher.h9
-rw-r--r--src/src/sanitychecks.cpp5
-rw-r--r--src/src/settingsdialogpaths.cpp24
-rw-r--r--src/src/settingsdialogproton.cpp58
-rw-r--r--src/src/spawn.cpp2
-rw-r--r--src/src/spawn.h1
-rw-r--r--src/src/vfs/vfs_helper_main.cpp337
-rw-r--r--src/src/wineprefix.cpp10
35 files changed, 641 insertions, 1679 deletions
diff --git a/src/plugins/rootbuilder.py b/src/plugins/rootbuilder.py
index dcc6706..342e1a0 100644
--- a/src/plugins/rootbuilder.py
+++ b/src/plugins/rootbuilder.py
@@ -57,18 +57,6 @@ def _walk_files(root_dir: str):
yield os.path.join(dirpath, name)
-def _host_cp(src: str, dst: str) -> bool:
- """Copy a file via the host OS (bypasses Flatpak sandbox restrictions)."""
- try:
- result = subprocess.run(
- ["flatpak-spawn", "--host", "cp", "-f", "--reflink=auto", "--", src, dst],
- capture_output=True, timeout=30,
- )
- return result.returncode == 0
- except (subprocess.SubprocessError, OSError):
- return False
-
-
def _ensure_readable(path: str):
"""Ensure a file has owner-read permission (mod archives sometimes strip it)."""
try:
@@ -99,8 +87,6 @@ def _reflink_copy(src: str, dst: str):
return
except OSError as e:
last_err = f"{e.strerror} (errno {e.errno})"
- if _IN_FLATPAK and _host_cp(src, dst):
- return
raise OSError(f"Root Builder: failed to copy {src} -> {dst}: {last_err}")
@@ -113,33 +99,6 @@ def _ensure_writable(path: str):
pass
-_IN_FLATPAK = os.path.exists("/.flatpak-info")
-
-
-def _host_rm(path: str) -> bool:
- """Remove a file via the host OS (bypasses Flatpak sandbox restrictions)."""
- try:
- result = subprocess.run(
- ["flatpak-spawn", "--host", "rm", "-f", "--", path],
- capture_output=True, timeout=10,
- )
- return result.returncode == 0
- except (subprocess.SubprocessError, OSError):
- return False
-
-
-def _host_mv(src: str, dst: str) -> bool:
- """Move a file via the host OS (bypasses Flatpak sandbox restrictions)."""
- try:
- result = subprocess.run(
- ["flatpak-spawn", "--host", "mv", "-f", "--", src, dst],
- capture_output=True, timeout=10,
- )
- return result.returncode == 0
- except (subprocess.SubprocessError, OSError):
- return False
-
-
def _force_remove(path: str) -> bool:
"""Remove a file, fixing permissions if needed. Returns True on success."""
try:
@@ -154,8 +113,6 @@ def _force_remove(path: str) -> bool:
return True
except OSError:
pass
- if _IN_FLATPAK and _host_rm(path):
- return True
qWarning(f"Root Builder: could not remove {path}")
return False
@@ -556,11 +513,10 @@ class RootBuilder(mobase.IPluginTool):
_force_remove(dst)
shutil.move(bak, dst)
except OSError:
- if not (_IN_FLATPAK and _host_mv(bak, dst)):
- qWarning(
- f"Root Builder: could not restore backup "
- f"{bak} -> {dst}"
- )
+ qWarning(
+ f"Root Builder: could not restore backup "
+ f"{bak} -> {dst}"
+ )
# Clean up backup dir
backup_dir = os.path.join(storage, _BACKUP_SUBDIR)
diff --git a/src/src/CMakeLists.txt b/src/src/CMakeLists.txt
index ab8b6c3..e85f730 100644
--- a/src/src/CMakeLists.txt
+++ b/src/src/CMakeLists.txt
@@ -48,14 +48,6 @@ list(REMOVE_ITEM ORGANIZER_SOURCES
list(REMOVE_ITEM ORGANIZER_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/usvfsconnector.h)
-# VFS helper has its own main() — exclude from organizer
-list(REMOVE_ITEM ORGANIZER_SOURCES
- ${CMAKE_CURRENT_SOURCE_DIR}/vfs/vfs_helper_main.cpp)
-
-# Process helper has its own main() — exclude from organizer
-list(REMOVE_ITEM ORGANIZER_SOURCES
- ${CMAKE_CURRENT_SOURCE_DIR}/process_helper_main.cpp)
-
# Remove WebEngine-dependent sources when not available
if(NOT Qt6WebEngineWidgets_FOUND)
list(REMOVE_ITEM ORGANIZER_SOURCES
@@ -127,43 +119,6 @@ target_compile_definitions(organizer PRIVATE
$<$<BOOL:${Qt6WebEngineWidgets_FOUND}>:MO2_WEBENGINE>)
if(NOT WIN32)
- # ── Standalone VFS helper for Flatpak (runs on host via flatpak-spawn) ──
- add_executable(mo2-vfs-helper
- vfs/vfs_helper_main.cpp
- vfs/vfstree.cpp
- vfs/mo2filesystem.cpp
- vfs/inodetable.cpp
- vfs/overwritemanager.cpp)
- # Prefer static libfuse3 so the helper is fully self-contained (Flatpak
- # runs it on the host via flatpak-spawn where SDK .so files don't exist).
- # Fall back to shared linking for source builds where libfuse3.a isn't
- # available (the helper still works since libfuse3.so is on the host).
- find_library(_fuse3_static libfuse3.a PATHS ${FUSE3_LIBRARY_DIRS} NO_DEFAULT_PATH)
- if(NOT _fuse3_static)
- find_library(_fuse3_static libfuse3.a)
- endif()
- target_link_directories(mo2-vfs-helper PRIVATE ${FUSE3_LIBRARY_DIRS})
- if(_fuse3_static)
- target_link_libraries(mo2-vfs-helper PRIVATE
- -Wl,-Bstatic -lfuse3 -Wl,-Bdynamic
- Threads::Threads)
- else()
- message(STATUS "libfuse3.a not found, linking mo2-vfs-helper against shared libfuse3")
- target_link_libraries(mo2-vfs-helper PRIVATE
- PkgConfig::FUSE3
- Threads::Threads)
- endif()
- target_include_directories(mo2-vfs-helper PRIVATE ${FUSE3_INCLUDE_DIRS})
- target_compile_definitions(mo2-vfs-helper PRIVATE FUSE_USE_VERSION=35)
- target_compile_features(mo2-vfs-helper PRIVATE cxx_std_23)
-
- # ── Standalone process helper for Flatpak game launching ──
- # Keeps the flatpak-spawn proxy alive while monitoring the game process tree.
- add_executable(mo2-process-helper
- process_helper_main.cpp)
- target_link_libraries(mo2-process-helper PRIVATE Threads::Threads)
- target_compile_features(mo2-process-helper PRIVATE cxx_std_23)
-
option(MO2_BUNDLE_7Z_RUNTIME "Copy a Linux 7z module into organizer/dlls" ON)
option(MO2_STAGE_PYTHON_PLUGIN_PAYLOAD
"Stage shipped Python plugin payload into build plugins/ for Linux runs" ON)
diff --git a/src/src/commandline.cpp b/src/src/commandline.cpp
index 09aabb8..4613350 100644
--- a/src/src/commandline.cpp
+++ b/src/src/commandline.cpp
@@ -1025,7 +1025,6 @@ std::optional<int> CreatePortableCommand::runEarly()
// Generate ModOrganizer.sh
{
- const bool flatpak = QFileInfo::exists(QStringLiteral("/.flatpak-info"));
const QString launchPath = QDir(instanceDir).filePath("ModOrganizer.sh");
QFile launchFile(launchPath);
if (launchFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
@@ -1033,12 +1032,8 @@ std::optional<int> CreatePortableCommand::runEarly()
out << "#!/bin/bash\n";
out << "# Auto-generated by fluorine-manager\n";
out << "INSTANCE_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n";
- if (flatpak) {
- out << "exec flatpak run com.fluorine.manager --instance \"${INSTANCE_DIR}\" \"$@\"\n";
- } else {
- out << "MO2_BIN=\"$(which ModOrganizer 2>/dev/null || echo ModOrganizer)\"\n";
- out << "exec \"${MO2_BIN}\" --instance \"${INSTANCE_DIR}\" \"$@\"\n";
- }
+ out << "MO2_BIN=\"$(which ModOrganizer 2>/dev/null || echo ModOrganizer)\"\n";
+ out << "exec \"${MO2_BIN}\" --instance \"${INSTANCE_DIR}\" \"$@\"\n";
launchFile.close();
QFile::setPermissions(launchPath,
QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner |
diff --git a/src/src/createinstancedialog.cpp b/src/src/createinstancedialog.cpp
index 89506e6..3bce099 100644
--- a/src/src/createinstancedialog.cpp
+++ b/src/src/createinstancedialog.cpp
@@ -67,8 +67,9 @@ private:
return;
}
- // root directory
- QDir d(cs[0]);
+ // root directory — on Unix, splitting "/home/user" produces ["", "home", "user"]
+ // so cs[0] is empty; use "/" as the root to keep paths absolute.
+ QDir d(cs[0].isEmpty() ? QStringLiteral("/") : cs[0]);
// for each directory after the root
for (int i = 1; i < cs.size(); ++i) {
diff --git a/src/src/createinstancedialogpages.cpp b/src/src/createinstancedialogpages.cpp
index 3138a17..5f134a0 100644
--- a/src/src/createinstancedialogpages.cpp
+++ b/src/src/createinstancedialogpages.cpp
@@ -14,19 +14,6 @@
namespace cid
{
-// On Flatpak the native file dialog returns XDG portal FUSE paths that may
-// not properly expose directory contents. Returns options that force the
-// Qt built-in dialog when running inside Flatpak.
-static QFileDialog::Options flatpakSafeOptions()
-{
- QFileDialog::Options opts;
-#ifndef _WIN32
- if (qEnvironmentVariableIsSet("FLATPAK_ID"))
- opts |= QFileDialog::DontUseNativeDialog;
-#endif
- return opts;
-}
-
using namespace MOBase;
using MOBase::TaskDialog;
@@ -329,7 +316,7 @@ void GamePage::select(IPluginGame* game, const QString& dir)
const auto path = QFileDialog::getExistingDirectory(
&m_dlg,
QObject::tr("Find game installation for %1").arg(game->displayGameName()),
- {}, flatpakSafeOptions());
+ {}, {});
if (path.isEmpty()) {
// cancelled
@@ -377,7 +364,7 @@ void GamePage::select(IPluginGame* game, const QString& dir)
void GamePage::selectCustom()
{
const auto path = QFileDialog::getExistingDirectory(
- &m_dlg, QObject::tr("Find game installation"), {}, flatpakSafeOptions());
+ &m_dlg, QObject::tr("Find game installation"), {}, {});
if (path.isEmpty()) {
// reselect the previous button
@@ -1115,7 +1102,7 @@ void PathsPage::onChanged()
void PathsPage::browse(QLineEdit* e)
{
const auto s =
- QFileDialog::getExistingDirectory(&m_dlg, {}, e->text(), flatpakSafeOptions());
+ QFileDialog::getExistingDirectory(&m_dlg, {}, e->text(), {});
if (s.isNull() || s.isEmpty()) {
return;
}
diff --git a/src/src/downloadlist.cpp b/src/src/downloadlist.cpp
index dd35ac2..457fab2 100644
--- a/src/src/downloadlist.cpp
+++ b/src/src/downloadlist.cpp
@@ -37,6 +37,7 @@ DownloadList::DownloadList(OrganizerCore& core, QObject* parent)
{
connect(&m_manager, SIGNAL(update(int)), this, SLOT(update(int)));
connect(&m_manager, SIGNAL(aboutToUpdate()), this, SLOT(aboutToUpdate()));
+ connect(&m_manager, SIGNAL(stateChanged(int,int)), this, SLOT(rowChanged(int)));
}
int DownloadList::rowCount(const QModelIndex& parent) const
@@ -107,6 +108,9 @@ QMimeData* DownloadList::mimeData(const QModelIndexList& indexes) const
QVariant DownloadList::data(const QModelIndex& index, int role) const
{
+ if (!index.isValid() || index.row() < 0 || index.row() >= rowCount())
+ return QVariant();
+
bool pendingDownload = index.row() >= m_manager.numTotalDownloads();
if (role == Qt::DisplayRole) {
if (pendingDownload) {
@@ -256,6 +260,14 @@ void DownloadList::update(int row)
log::error("invalid row {} in download list, update failed", row);
}
+void DownloadList::rowChanged(int row)
+{
+ if (row >= 0 && row < rowCount()) {
+ emit dataChanged(index(row, 0, QModelIndex()),
+ index(row, columnCount(QModelIndex()) - 1, QModelIndex()));
+ }
+}
+
bool DownloadList::lessThanPredicate(const QModelIndex& left, const QModelIndex& right)
{
int leftIndex = left.row();
diff --git a/src/src/downloadlist.h b/src/src/downloadlist.h
index 73e499d..1be2ea5 100644
--- a/src/src/downloadlist.h
+++ b/src/src/downloadlist.h
@@ -94,6 +94,8 @@ public slots:
void aboutToUpdate();
+ void rowChanged(int row);
+
private:
DownloadManager& m_manager;
Settings& m_settings;
diff --git a/src/src/downloadlistview.cpp b/src/src/downloadlistview.cpp
index 8928e6c..961af97 100644
--- a/src/src/downloadlistview.cpp
+++ b/src/src/downloadlistview.cpp
@@ -50,6 +50,11 @@ void DownloadProgressDelegate::paint(QPainter* painter,
sourceIndex = index;
}
+ if (sourceIndex.row() < 0 || sourceIndex.row() >= m_Manager->numTotalDownloads() + m_Manager->numPendingDownloads()) {
+ QStyledItemDelegate::paint(painter, option, index);
+ return;
+ }
+
bool pendingDownload = (sourceIndex.row() >= m_Manager->numTotalDownloads());
if (sourceIndex.column() == DownloadList::COL_STATUS && !pendingDownload &&
m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_DOWNLOADING) {
diff --git a/src/src/downloadmanager.cpp b/src/src/downloadmanager.cpp
index ae3b94e..457a58e 100644
--- a/src/src/downloadmanager.cpp
+++ b/src/src/downloadmanager.cpp
@@ -229,13 +229,13 @@ DownloadManager::DownloadManager(NexusInterface* nexusInterface, QObject* parent
m_ParentWidget(nullptr)
{
m_OrganizerCore = dynamic_cast<OrganizerCore*>(parent);
- connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this,
- SLOT(directoryChanged(QString)));
- m_TimeoutTimer.setSingleShot(false);
- connect(&m_TimeoutTimer, &QTimer::timeout, this,
- &DownloadManager::checkDownloadTimeout);
- m_TimeoutTimer.start(5 * 1000);
-}
+ connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this,
+ SLOT(directoryChanged(QString)));
+ m_TimeoutTimer.setSingleShot(false);
+ connect(&m_TimeoutTimer, &QTimer::timeout, this,
+ &DownloadManager::checkDownloadTimeout);
+ m_TimeoutTimer.start(5 * 1000);
+}
DownloadManager::~DownloadManager()
{
@@ -953,19 +953,19 @@ void DownloadManager::removeDownload(int index, bool deleteFile)
refreshList();
}
-void DownloadManager::cancelDownload(int index)
-{
- if ((index < 0) || (index >= m_ActiveDownloads.size())) {
- reportError(tr("cancel: invalid download index %1").arg(index));
- return;
- }
-
- DownloadInfo* info = m_ActiveDownloads.at(index);
- if (info->m_State == STATE_DOWNLOADING || info->m_State == STATE_STARTED ||
- info->m_State == STATE_PAUSING) {
- setState(info, STATE_CANCELING);
- }
-}
+void DownloadManager::cancelDownload(int index)
+{
+ if ((index < 0) || (index >= m_ActiveDownloads.size())) {
+ reportError(tr("cancel: invalid download index %1").arg(index));
+ return;
+ }
+
+ DownloadInfo* info = m_ActiveDownloads.at(index);
+ if (info->m_State == STATE_DOWNLOADING || info->m_State == STATE_STARTED ||
+ info->m_State == STATE_PAUSING) {
+ setState(info, STATE_CANCELING);
+ }
+}
void DownloadManager::pauseDownload(int index)
{
@@ -1613,20 +1613,20 @@ void DownloadManager::setState(DownloadManager::DownloadInfo* info,
row = i;
break;
}
- }
- info->m_State = state;
- switch (state) {
- case STATE_CANCELING: {
- // Force termination so the download transitions through finished().
- if (info->m_Reply != nullptr && info->m_Reply->isRunning()) {
- info->m_Reply->abort();
- }
- } break;
- case STATE_PAUSED: {
- info->m_Reply->abort();
- info->m_Output.close();
- m_DownloadPaused(row);
- } break;
+ }
+ info->m_State = state;
+ switch (state) {
+ case STATE_CANCELING: {
+ // Force termination so the download transitions through finished().
+ if (info->m_Reply != nullptr && info->m_Reply->isRunning()) {
+ info->m_Reply->abort();
+ }
+ } break;
+ case STATE_PAUSED: {
+ info->m_Reply->abort();
+ info->m_Output.close();
+ m_DownloadPaused(row);
+ } break;
case STATE_ERROR: {
info->m_Reply->abort();
info->m_Output.close();
@@ -2313,12 +2313,10 @@ void DownloadManager::downloadFinished(int index)
"There may be an issue with the Nexus servers."));
emit update(-1);
} else if (info->isPausedState() || info->m_State == STATE_PAUSING) {
- emit aboutToUpdate();
info->m_Output.close();
createMetaFile(info);
emit update(index);
} else {
- emit aboutToUpdate();
QString url = info->m_Urls[info->m_CurrentUrl];
if (info->m_FileInfo->userData.contains("downloadMap")) {
foreach (const QVariant& server,
@@ -2420,46 +2418,46 @@ void DownloadManager::managedGameChanged(MOBase::IPluginGame const* managedGame)
m_ManagedGame = managedGame;
}
-void DownloadManager::checkDownloadTimeout()
-{
- for (int i = 0; i < m_ActiveDownloads.size(); ++i) {
- DownloadInfo* info = m_ActiveDownloads[i];
- if (info->m_State != STATE_DOWNLOADING || info->m_Reply == nullptr ||
- !info->m_Reply->isOpen()) {
- continue;
- }
-
- const bool stalled =
- (info->m_StartTime.elapsed() - info->m_DownloadTimeLast) > (5 * 1000);
- if (!stalled) {
- continue;
- }
-
- pauseDownload(i);
- downloadFinished(i);
-
- // downloadFinished() can remove the download entry, so find it again.
- const int index = indexByInfo(info);
- if (index < 0) {
- continue;
- }
-
- if (info->m_Tries <= 0) {
- emit showMessage(tr("Download stalled repeatedly and was paused. "
- "Please retry manually."));
- continue;
- }
-
- --info->m_Tries;
- log::warn("download '{}' stalled, retrying ({} retries left)", info->m_FileName,
- info->m_Tries);
- resumeDownloadInt(index);
- emit update(index);
- }
-}
-
-void DownloadManager::writeData(DownloadInfo* info)
-{
+void DownloadManager::checkDownloadTimeout()
+{
+ for (int i = 0; i < m_ActiveDownloads.size(); ++i) {
+ DownloadInfo* info = m_ActiveDownloads[i];
+ if (info->m_State != STATE_DOWNLOADING || info->m_Reply == nullptr ||
+ !info->m_Reply->isOpen()) {
+ continue;
+ }
+
+ const bool stalled =
+ (info->m_StartTime.elapsed() - info->m_DownloadTimeLast) > (5 * 1000);
+ if (!stalled) {
+ continue;
+ }
+
+ pauseDownload(i);
+ downloadFinished(i);
+
+ // downloadFinished() can remove the download entry, so find it again.
+ const int index = indexByInfo(info);
+ if (index < 0) {
+ continue;
+ }
+
+ if (info->m_Tries <= 0) {
+ emit showMessage(tr("Download stalled repeatedly and was paused. "
+ "Please retry manually."));
+ continue;
+ }
+
+ --info->m_Tries;
+ log::warn("download '{}' stalled, retrying ({} retries left)", info->m_FileName,
+ info->m_Tries);
+ resumeDownloadInt(index);
+ emit update(index);
+ }
+}
+
+void DownloadManager::writeData(DownloadInfo* info)
+{
if (info != nullptr) {
qint64 ret = info->m_Output.write(info->m_Reply->readAll());
if (ret < info->m_Reply->size()) {
diff --git a/src/src/filedialogmemory.cpp b/src/src/filedialogmemory.cpp
index 3c4ea11..390843f 100644
--- a/src/src/filedialogmemory.cpp
+++ b/src/src/filedialogmemory.cpp
@@ -72,15 +72,6 @@ QString FileDialogMemory::getExistingDirectory(const QString& dirID, QWidget* pa
}
}
-#ifndef _WIN32
- // On Flatpak, the native file dialog goes through the XDG Desktop Portal,
- // which returns FUSE paths (/run/user/.../doc/...) that may not properly
- // expose directory contents. Use the Qt built-in dialog to get real paths.
- if (qEnvironmentVariableIsSet("FLATPAK_ID")) {
- options |= QFileDialog::DontUseNativeDialog;
- }
-#endif
-
QString result =
QFileDialog::getExistingDirectory(parent, caption, currentDir, options);
diff --git a/src/src/fluorinepaths.cpp b/src/src/fluorinepaths.cpp
index 98cb7aa..b6e6633 100644
--- a/src/src/fluorinepaths.cpp
+++ b/src/src/fluorinepaths.cpp
@@ -3,16 +3,18 @@
#include <QDir>
#include <QFile>
+#include <QFileInfo>
+#include <QStandardPaths>
#include <QTextStream>
#include <cstdio>
-static const QString OldRoot =
- QDir::homePath() + "/.local/share/fluorine";
+static const QString OldFlatpakRoot =
+ QDir::homePath() + "/.var/app/com.fluorine.manager";
QString fluorineDataDir()
{
- return QDir::homePath() + "/.var/app/com.fluorine.manager";
+ return QDir::homePath() + "/.local/share/fluorine";
}
void fluorineMigrateDataDir()
@@ -21,9 +23,42 @@ void fluorineMigrateDataDir()
return;
#endif
- const QString oldRoot = OldRoot;
+ const QString oldRoot = OldFlatpakRoot;
const QString newRoot = fluorineDataDir();
+ // Always check for config.json migration, even if data was already moved.
+ // The Flatpak stored config.json under its sandboxed XDG_CONFIG_HOME
+ // (~/.var/app/com.fluorine.manager/config/), but outside Flatpak
+ // QStandardPaths returns ~/.config/.
+ {
+ const QString oldConfigJson = oldRoot + "/config/fluorine/config.json";
+ QString configRoot = QStandardPaths::writableLocation(
+ QStandardPaths::ConfigLocation);
+ if (configRoot.isEmpty()) {
+ configRoot = QDir::homePath() + "/.config";
+ }
+ const QString newConfigJson =
+ QDir(configRoot).filePath("fluorine/config.json");
+ if (QFile::exists(oldConfigJson) && !QFile::exists(newConfigJson)) {
+ QDir().mkpath(QFileInfo(newConfigJson).dir().absolutePath());
+ if (QFile::copy(oldConfigJson, newConfigJson)) {
+ fprintf(stderr, "[fluorine] Migrated config.json to %s\n",
+ qUtf8Printable(newConfigJson));
+ // Update prefix_path if it references the old Flatpak root
+ if (auto cfg = FluorineConfig::load()) {
+ if (cfg->prefix_path.startsWith(oldRoot)) {
+ cfg->prefix_path.replace(oldRoot, newRoot);
+ cfg->save();
+ fprintf(stderr, "[fluorine] updated config prefix_path\n");
+ }
+ }
+ } else {
+ fprintf(stderr, "[fluorine] FAILED to copy config.json to %s\n",
+ qUtf8Printable(newConfigJson));
+ }
+ }
+ }
+
// Already migrated or old path never existed
if (QFile::exists(oldRoot + "/MOVED.txt")) {
return;
diff --git a/src/src/fluorinepaths.h b/src/src/fluorinepaths.h
index ab7f119..2bdde33 100644
--- a/src/src/fluorinepaths.h
+++ b/src/src/fluorinepaths.h
@@ -3,11 +3,11 @@
#include <QString>
-/// Returns the Fluorine data directory: ~/.var/app/com.fluorine.manager
+/// Returns the Fluorine data directory: ~/.local/share/fluorine
QString fluorineDataDir();
-/// One-time migration from ~/.local/share/fluorine/ back to
-/// ~/.var/app/com.fluorine.manager/. Call before initLogging().
+/// One-time migration from ~/.var/app/com.fluorine.manager/ to
+/// ~/.local/share/fluorine/. Call before initLogging().
void fluorineMigrateDataDir();
#endif // FLUORINEPATHS_H
diff --git a/src/src/fuseconnector.cpp b/src/src/fuseconnector.cpp
index 2c5454f..3ba5c69 100644
--- a/src/src/fuseconnector.cpp
+++ b/src/src/fuseconnector.cpp
@@ -1,6 +1,5 @@
#include "fuseconnector.h"
-#include "fluorinepaths.h"
#include "settings.h"
#include "vfs/vfstree.h"
@@ -49,53 +48,6 @@ namespace
{
namespace fs = std::filesystem;
-bool isFlatpak()
-{
- static const bool result = QFile::exists(QStringLiteral("/.flatpak-info"));
- return result;
-}
-
-bool waitForHelperLine(QProcess* proc, const char* expected, int timeoutMs)
-{
- const QByteArray target(expected);
- const auto deadline =
- std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
-
- while (proc->state() == QProcess::Running) {
- if (proc->canReadLine()) {
- const QByteArray line = proc->readLine().trimmed();
- if (line == target) {
- return true;
- }
- if (line.startsWith("error:")) {
- log::error("VFS helper: {}", QString::fromUtf8(line));
- return false;
- }
- continue;
- }
-
- const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
- deadline - std::chrono::steady_clock::now())
- .count();
- if (remaining <= 0) {
- break;
- }
- proc->waitForReadyRead(static_cast<int>(remaining));
- }
-
- return false;
-}
-
-bool sendHelperCommand(QProcess* proc, const char* command, int timeoutMs)
-{
- proc->write(command);
- proc->write("\n");
- if (!proc->waitForBytesWritten(1000)) {
- return false;
- }
- return waitForHelperLine(proc, "ok", timeoutMs);
-}
-
std::string decodeProcMountField(const std::string& in)
{
std::string out;
@@ -158,17 +110,6 @@ bool runUnmountCommand(const QString& program, const QStringList& args)
return p.exitStatus() == QProcess::NormalExit && p.exitCode() == 0;
};
- // In Flatpak: try sandbox-local unmount first (mount was likely created
- // inside the sandbox), then fall back to host-side unmount.
- if (isFlatpak()) {
- if (tryRun(program, args)) {
- return true;
- }
- QStringList spawnArgs = {QStringLiteral("--host"), program};
- spawnArgs.append(args);
- return tryRun(QStringLiteral("flatpak-spawn"), spawnArgs);
- }
-
return tryRun(program, args);
}
@@ -268,10 +209,6 @@ bool FuseConnector::mount(
tryCleanupStaleMount(QString::fromStdString(m_mountPoint));
- if (isFlatpak()) {
- return mountViaHelper(overwrite_dir, game_dir, data_dir_name, mods);
- }
-
const fs::path overwritePath(m_overwriteDir);
m_stagingDir = (overwritePath.parent_path() / "VFS_staging").string();
@@ -370,23 +307,6 @@ void FuseConnector::unmount()
return;
}
- if (m_helperProcess) {
- sendHelperCommand(m_helperProcess, "quit", 10000);
- m_helperProcess->waitForFinished(5000);
- if (m_helperProcess->state() != QProcess::NotRunning) {
- m_helperProcess->kill();
- m_helperProcess->waitForFinished(2000);
- }
- delete m_helperProcess;
- m_helperProcess = nullptr;
- m_mounted = false;
- setFuseMountPointForCrashCleanup(nullptr);
- cleanupExternalMappings();
- log::debug("VFS helper stopped, FUSE unmounted from {}",
- QString::fromStdString(m_mountPoint));
- return;
- }
-
if (m_session != nullptr) {
fuse_session_exit(m_session);
fuse_session_unmount(m_session);
@@ -435,16 +355,6 @@ void FuseConnector::rebuild(
m_dataDirName = data_dir_name.toStdString();
m_lastMods = mods;
- if (m_helperProcess) {
- const QString dataDir = fluorineDataDir();
- const QString configPath = QDir(dataDir).filePath("vfs.cfg");
- writeVfsConfig(configPath, QString::fromStdString(m_mountPoint),
- overwrite_dir, QString::fromStdString(m_gameDir),
- data_dir_name, mods);
- sendHelperCommand(m_helperProcess, "rebuild", 10000);
- return;
- }
-
if (m_context == nullptr) {
return;
}
@@ -700,11 +610,6 @@ void FuseConnector::flushStagingLive()
return;
}
- if (m_helperProcess) {
- sendHelperCommand(m_helperProcess, "flush", 30000);
- return;
- }
-
if (m_context == nullptr) {
return;
}
@@ -784,88 +689,3 @@ void FuseConnector::tryCleanupStaleMount(const QString& path)
doUnmount(path);
}
-bool FuseConnector::mountViaHelper(
- const QString& overwrite_dir, const QString& game_dir,
- const QString& data_dir_name,
- const std::vector<std::pair<std::string, std::string>>& mods)
-{
- const QString dataDir = fluorineDataDir();
- const QString configPath = QDir(dataDir).filePath("vfs.cfg");
- const QString helperBin =
- QDir(dataDir).filePath("bin/mo2-vfs-helper");
-
- if (!QFile::exists(helperBin)) {
- throw FuseConnectorException(
- QObject::tr("VFS helper not found: %1").arg(helperBin));
- }
-
- writeVfsConfig(configPath, QString::fromStdString(m_mountPoint), overwrite_dir,
- game_dir, data_dir_name, mods);
-
- m_helperProcess = new QProcess(this);
- m_helperProcess->setProcessChannelMode(QProcess::SeparateChannels);
- m_helperProcess->start(QStringLiteral("flatpak-spawn"),
- {QStringLiteral("--host"), helperBin, configPath});
-
- if (!m_helperProcess->waitForStarted(5000)) {
- const QString err = QString::fromUtf8(m_helperProcess->readAllStandardError());
- delete m_helperProcess;
- m_helperProcess = nullptr;
- throw FuseConnectorException(
- QObject::tr("Failed to start VFS helper process. %1").arg(err));
- }
-
- if (!waitForHelperLine(m_helperProcess, "mounted", 10000)) {
- const QString err = QString::fromUtf8(m_helperProcess->readAllStandardError());
- const QString out = QString::fromUtf8(m_helperProcess->readAllStandardOutput());
- log::error("VFS helper stderr: {}", err);
- log::error("VFS helper stdout: {}", out);
- m_helperProcess->kill();
- m_helperProcess->waitForFinished(2000);
- delete m_helperProcess;
- m_helperProcess = nullptr;
- throw FuseConnectorException(
- QObject::tr("VFS helper failed to mount FUSE. %1").arg(err));
- }
-
- m_mounted = true;
- setFuseMountPointForCrashCleanup(m_mountPoint.c_str());
- log::debug("FUSE mounted via helper on {}",
- QString::fromStdString(m_mountPoint));
- return true;
-}
-
-void FuseConnector::writeVfsConfig(
- const QString& configPath, const QString& mount_point,
- const QString& overwrite_dir, const QString& game_dir,
- const QString& data_dir_name,
- const std::vector<std::pair<std::string, std::string>>& mods)
-{
- QDir().mkpath(QFileInfo(configPath).absolutePath());
-
- QFile file(configPath);
- if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
- throw FuseConnectorException(
- QObject::tr("Failed to write VFS config: %1").arg(configPath));
- }
-
- QTextStream out(&file);
- out << "mount_point=" << mount_point << "\n";
- out << "game_dir=" << game_dir << "\n";
- out << "data_dir_name=" << data_dir_name << "\n";
- out << "overwrite_dir=" << overwrite_dir << "\n";
-
- if (!m_customOutputDir.empty()) {
- out << "output_dir=" << QString::fromStdString(m_customOutputDir) << "\n";
- }
-
- for (const auto& [name, path] : mods) {
- out << "mod=" << QString::fromStdString(name) << "|"
- << QString::fromStdString(path) << "\n";
- }
-
- for (const auto& [relPath, realPath] : m_extraVfsFiles) {
- out << "extra_file=" << QString::fromStdString(relPath) << "|"
- << QString::fromStdString(realPath) << "\n";
- }
-}
diff --git a/src/src/fuseconnector.h b/src/src/fuseconnector.h
index cf99d64..9a34e55 100644
--- a/src/src/fuseconnector.h
+++ b/src/src/fuseconnector.h
@@ -7,8 +7,6 @@
#include <QObject>
#include <QString>
-class QProcess;
-
#include <exception>
#include <memory>
#include <thread>
@@ -88,15 +86,6 @@ private:
struct fuse_session* m_session = nullptr;
std::thread m_fuseThread;
bool m_mounted = false;
-
- QProcess* m_helperProcess = nullptr;
- bool mountViaHelper(const QString& overwrite_dir, const QString& game_dir,
- const QString& data_dir_name,
- const std::vector<std::pair<std::string, std::string>>& mods);
- void writeVfsConfig(const QString& configPath, const QString& mount_point,
- const QString& overwrite_dir, const QString& game_dir,
- const QString& data_dir_name,
- const std::vector<std::pair<std::string, std::string>>& mods);
};
#endif
diff --git a/src/src/instancemanager.cpp b/src/src/instancemanager.cpp
index 1269d26..7821761 100644
--- a/src/src/instancemanager.cpp
+++ b/src/src/instancemanager.cpp
@@ -55,21 +55,15 @@ Instance::Instance(QString dir, bool portable, QString profileName)
if (m_portable && !m_dir.isEmpty()) {
const QString launchPath = QDir(m_dir).filePath("ModOrganizer.sh");
if (!QFileInfo::exists(launchPath)) {
- const bool flatpak = QFileInfo::exists(QStringLiteral("/.flatpak-info"));
- QFile launchFile(launchPath);
+ QFile launchFile(launchPath);
if (launchFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&launchFile);
out << "#!/bin/bash\n";
out << "# Auto-generated by fluorine-manager\n";
out << "INSTANCE_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n";
- if (flatpak) {
- out << "exec flatpak run com.fluorine.manager --instance "
- "\"${INSTANCE_DIR}\" \"$@\"\n";
- } else {
- out << "MO2_BIN=\"$(which ModOrganizer 2>/dev/null || echo "
- "ModOrganizer)\"\n";
- out << "exec \"${MO2_BIN}\" --instance \"${INSTANCE_DIR}\" \"$@\"\n";
- }
+ out << "MO2_BIN=\"$(which ModOrganizer 2>/dev/null || echo "
+ "ModOrganizer)\"\n";
+ out << "exec \"${MO2_BIN}\" --instance \"${INSTANCE_DIR}\" \"$@\"\n";
launchFile.close();
QFile::setPermissions(
launchPath,
diff --git a/src/src/instancemanagerdialog.cpp b/src/src/instancemanagerdialog.cpp
index cd4b865..5cdaecf 100644
--- a/src/src/instancemanagerdialog.cpp
+++ b/src/src/instancemanagerdialog.cpp
@@ -8,6 +8,7 @@
#include "shared/appconfig.h"
#include "shared/util.h"
#include "ui_instancemanagerdialog.h"
+#include <QCheckBox>
#include <QFile>
#include <QFileDialog>
#include <QSettings>
@@ -214,6 +215,15 @@ InstanceManagerDialog::InstanceManagerDialog(PluginContainer& pc, QWidget* paren
deleteInstance();
});
+ connect(ui->steamDrmCheckBox, &QCheckBox::toggled, [&](bool checked) {
+ const auto* inst = singleSelection();
+ if (!inst) return;
+ const QString ini = inst->iniPath();
+ if (ini.isEmpty()) return;
+ QSettings s(ini, QSettings::IniFormat);
+ s.setValue("fluorine/steam_drm", checked);
+ });
+
connect(ui->switchToInstance, &QPushButton::clicked, [&] {
openSelectedInstance();
});
@@ -772,16 +782,23 @@ void InstanceManagerDialog::fillData(const Instance& ii)
ui->gameName->setText(ii.gameName());
ui->gameDir->setText(ii.gameDirectory());
- // read prefix info from the instance's INI
+ // read prefix info and fluorine settings from the instance's INI
{
const QString ini = ii.iniPath();
if (!ini.isEmpty() && QFile::exists(ini)) {
QSettings s(ini, QSettings::IniFormat);
ui->prefixPath->setText(s.value("Settings/proton_prefix_path").toString());
ui->protonVersion->setText(s.value("fluorine/proton_name").toString());
+
+ ui->steamDrmCheckBox->blockSignals(true);
+ ui->steamDrmCheckBox->setChecked(s.value("fluorine/steam_drm", true).toBool());
+ ui->steamDrmCheckBox->blockSignals(false);
} else {
ui->prefixPath->clear();
ui->protonVersion->clear();
+ ui->steamDrmCheckBox->blockSignals(true);
+ ui->steamDrmCheckBox->setChecked(false);
+ ui->steamDrmCheckBox->blockSignals(false);
}
}
@@ -822,6 +839,9 @@ void InstanceManagerDialog::clearData()
ui->gameDir->clear();
ui->prefixPath->clear();
ui->protonVersion->clear();
+ ui->steamDrmCheckBox->blockSignals(true);
+ ui->steamDrmCheckBox->setChecked(false);
+ ui->steamDrmCheckBox->blockSignals(false);
setButtonsEnabled(false);
@@ -845,18 +865,9 @@ void InstanceManagerDialog::setButtonsEnabled(bool b)
void InstanceManagerDialog::openExistingPortable()
{
// On Flatpak, the native file dialog goes through the XDG Desktop Portal,
- // which returns FUSE paths (/run/user/.../doc/...) that may not properly
- // expose directory contents. Use the Qt built-in dialog to get real paths.
- QFileDialog::Options opts;
-#ifndef _WIN32
- if (qEnvironmentVariableIsSet("FLATPAK_ID")) {
- opts |= QFileDialog::DontUseNativeDialog;
- }
-#endif
-
const QString dir = QFileDialog::getExistingDirectory(
this, tr("Select portable instance folder"),
- QStandardPaths::writableLocation(QStandardPaths::HomeLocation), opts);
+ QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
if (dir.isEmpty()) {
return;
diff --git a/src/src/instancemanagerdialog.ui b/src/src/instancemanagerdialog.ui
index 2b94a8d..2a1d136 100644
--- a/src/src/instancemanagerdialog.ui
+++ b/src/src/instancemanagerdialog.ui
@@ -314,6 +314,16 @@
</property>
</widget>
</item>
+ <item row="10" column="0" colspan="2">
+ <widget class="QCheckBox" name="steamDrmCheckBox">
+ <property name="text">
+ <string>Steam DRM integration</string>
+ </property>
+ <property name="toolTip">
+ <string>Enable Steam DRM bridge for this instance. Disable for GOG or DRM-free games to prevent crashes.</string>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
</item>
diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp
index 425d20c..7aa6964 100644
--- a/src/src/mainwindow.cpp
+++ b/src/src/mainwindow.cpp
@@ -544,11 +544,12 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore,
QApplication::instance()->installEventFilter(this);
- scheduleCheckForProblems();
- refreshExecutablesList();
- updatePinnedExecutables();
- resetActionIcons();
- processUpdates();
+ scheduleCheckForProblems();
+ refreshExecutablesList();
+ updatePinnedExecutables();
+ resetActionIcons();
+ resetButtonIcons();
+ processUpdates();
ui->modList->updateModCount();
ui->espList->updatePluginCount();
@@ -572,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
@@ -635,12 +636,38 @@ void MainWindow::resetActionIcons()
}
}
- // update the button for the potentially new icon
- updateProblemsButton();
-}
-
-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();
@@ -712,10 +739,11 @@ void MainWindow::allowListResize()
ui->espList->header()->setStretchLastSection(true);
}
-void MainWindow::updateStyle(const QString&)
-{
- resetActionIcons();
-}
+void MainWindow::updateStyle(const QString&)
+{
+ resetActionIcons();
+ resetButtonIcons();
+}
void MainWindow::resizeEvent(QResizeEvent* event)
{
@@ -1885,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()) {
@@ -1904,16 +1932,36 @@ void MainWindow::refreshExecutablesList()
Qt::SizeHintRole);
};
- ui->executablesListBox->setEnabled(false);
- ui->executablesListBox->clear();
-
- add(tr("<Edit...>"), {});
-
- for (const auto& exe : *m_OrganizerCore.executablesList()) {
- if (!exe.hide()) {
- 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
diff --git a/src/src/mainwindow.h b/src/src/mainwindow.h
index b931c35..3e22fd1 100644
--- a/src/src/mainwindow.h
+++ b/src/src/mainwindow.h
@@ -440,9 +440,10 @@ private slots:
void toolBar_customContextMenuRequested(const QPoint& point);
void removeFromToolbar(QAction* action);
- void about();
-
- void resetActionIcons();
+ void about();
+
+ void resetActionIcons();
+ void resetButtonIcons();
private slots: // ui slots
// actions
diff --git a/src/src/moapplication.cpp b/src/src/moapplication.cpp
index 29e21c4..fd60745 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,9 +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 <report.h>
+#include <scopeguard.h>
+#include <utility.h>
+#include <cstdio>
#ifdef _WIN32
// see addDllsToPath() below
@@ -269,30 +270,43 @@ void MOApplication::firstTimeSetup(MOMultiProcess& multiProcess)
Qt::QueuedConnection);
}
-int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect)
-{
- TimeThis tt("MOApplication setup()");
+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) {
- return 1;
- }
+ 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)) {
- reportError(tr("Failed to create log folder."));
- InstanceManager::singleton().clearCurrentInstance();
- return 1;
- }
+ 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()));
@@ -322,8 +336,11 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect)
}
// loading settings
- m_settings.reset(new Settings(m_instance->iniPath(), true));
- 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());
@@ -358,20 +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));
- if (!m_core->bootstrap()) {
- reportError(tr("Failed to set up data paths."));
- InstanceManager::singleton().clearCurrentInstance();
- return 1;
- }
+ 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());
- m_plugins->loadPlugins();
- 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...");
diff --git a/src/src/nxmhandler_linux.cpp b/src/src/nxmhandler_linux.cpp
index a93826d..1203585 100644
--- a/src/src/nxmhandler_linux.cpp
+++ b/src/src/nxmhandler_linux.cpp
@@ -174,8 +174,6 @@ QString NxmHandlerLinux::socketPath()
void NxmHandlerLinux::registerHandler() const
{
- const bool flatpak = QFile::exists(QStringLiteral("/.flatpak-info"));
-
const QString home = QDir::homePath();
if (home.isEmpty()) {
log::error("cannot register nxm handler: home path is empty");
@@ -190,37 +188,30 @@ void NxmHandlerLinux::registerHandler() const
return;
}
- QString execLine;
-
- if (flatpak) {
- // In Flatpak the desktop file is invoked on the HOST, so use flatpak run
- execLine = "flatpak run com.fluorine.manager nxm-handle %u";
- } else {
- // Non-Flatpak: create a wrapper script and point the desktop file at it
- const QString localBin = ensureDir(home + "/.local/bin");
- if (localBin.isEmpty()) {
- log::error("cannot register nxm handler: failed to create ~/.local/bin");
- return;
- }
+ // Create a wrapper script and point the desktop file at it
+ const QString localBin = ensureDir(home + "/.local/bin");
+ if (localBin.isEmpty()) {
+ log::error("cannot register nxm handler: failed to create ~/.local/bin");
+ return;
+ }
- const QString wrapperPath = localBin + "/mo2-nxm-handler";
- const QString executable = QCoreApplication::applicationFilePath();
- const QString wrapper =
- QString("#!/bin/sh\nexec \"%1\" nxm-handle \"$@\"\n").arg(executable);
+ const QString wrapperPath = localBin + "/mo2-nxm-handler";
+ const QString executable = QCoreApplication::applicationFilePath();
+ const QString wrapper =
+ QString("#!/bin/sh\nexec \"%1\" nxm-handle \"$@\"\n").arg(executable);
- if (!writeTextFile(wrapperPath, wrapper)) {
- log::error("failed to write nxm wrapper script '{}'", wrapperPath);
- return;
- }
+ if (!writeTextFile(wrapperPath, wrapper)) {
+ log::error("failed to write nxm wrapper script '{}'", wrapperPath);
+ return;
+ }
- QFile::setPermissions(wrapperPath,
- QFileDevice::ReadOwner | QFileDevice::WriteOwner |
- QFileDevice::ExeOwner | QFileDevice::ReadGroup |
- QFileDevice::ExeGroup | QFileDevice::ReadOther |
- QFileDevice::ExeOther);
+ QFile::setPermissions(wrapperPath,
+ QFileDevice::ReadOwner | QFileDevice::WriteOwner |
+ QFileDevice::ExeOwner | QFileDevice::ReadGroup |
+ QFileDevice::ExeGroup | QFileDevice::ReadOther |
+ QFileDevice::ExeOther);
- execLine = "mo2-nxm-handler nxm-handle %u";
- }
+ const QString execLine = "mo2-nxm-handler nxm-handle %u";
const QString desktopPath = appsDir + "/mo2-nxm-handler.desktop";
const QString desktop = QString("[Desktop Entry]\n"
@@ -240,21 +231,10 @@ void NxmHandlerLinux::registerHandler() const
updateMimeAppsList(appsDir + "/mimeapps.list", "x-scheme-handler/nxm",
"mo2-nxm-handler.desktop");
- if (flatpak) {
- // update-desktop-database doesn't exist inside the sandbox; run it on the host
- QProcess p;
- p.start(QStringLiteral("flatpak-spawn"),
- {QStringLiteral("--host"), QStringLiteral("update-desktop-database"),
- appsDir});
- if (!p.waitForFinished(5000) || p.exitCode() != 0) {
- log::warn("update-desktop-database on host exited with code {}", p.exitCode());
- }
- } else {
- const auto result =
- QProcess::execute("update-desktop-database", QStringList() << appsDir);
- if (result != 0) {
- log::warn("update-desktop-database exited with code {}", result);
- }
+ const auto result =
+ QProcess::execute("update-desktop-database", QStringList() << appsDir);
+ if (result != 0) {
+ log::warn("update-desktop-database exited with code {}", result);
}
}
diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp
index e1bdda1..630f23b 100644
--- a/src/src/organizercore.cpp
+++ b/src/src/organizercore.cpp
@@ -2193,22 +2193,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);
-
- // 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()) {
@@ -2218,12 +2219,17 @@ bool OrganizerCore::beforeRun(
}
}
pluginsFile.close();
-
- if (!plugins.isEmpty()) {
- 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);
@@ -2238,18 +2244,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::debug("INI deploy check: source='{}' exists={}, target='{}'",
- sourceIni, QFileInfo::exists(sourceIni), targetIni);
- 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,
@@ -2261,15 +2267,15 @@ bool OrganizerCore::beforeRun(
managedGame()->documentsDirectory().absolutePath());
}
- if (m_CurrentProfile->localSavesEnabled()) {
- const QString profileSavesDir =
- QDir(m_CurrentProfile->absolutePath()).filePath("saves");
- log::debug("Resolved local save mapping: profile='{}', 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);
}
@@ -2315,29 +2321,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::debug("Syncing local save mapping: profile='{}', 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::debug("Sync profile INI '{}' <- '{}'", 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 98665fb..dad0117 100644
--- a/src/src/plugincontainer.cpp
+++ b/src/src/plugincontainer.cpp
@@ -4,11 +4,12 @@
#include "organizerproxy.h"
#include "report.h"
#include "shared/appconfig.h"
-#include <QAction>
-#include <QCoreApplication>
-#include <QDirIterator>
-#include <QMessageBox>
-#include <QToolButton>
+#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>
@@ -21,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);
@@ -55,8 +56,14 @@ static void ensureBundledPluginsLinked(const QString& bundledDir,
QFile::link(it.filePath(), target);
}
-}
-#endif
+}
+#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!
//
@@ -636,22 +643,33 @@ 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);
-
- QStringList filepaths = proxy->pluginList(
- m_PluginPath.isEmpty()
- ? (AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath()))
- : m_PluginPath);
- for (const QString& filepath : filepaths) {
- 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::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;
+ }
+ }
{ // dummy plugins
// only initialize these, no processing otherwise
@@ -835,44 +853,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 {
- // 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);
-
- // 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) {
-
- 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("plugin \"{}\" failed to load. If this plugin is for an older "
- "version of MO "
- "you have to update it or delete it if no update exists.",
- filepath);
- }
- }
- }
+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),
@@ -884,15 +928,43 @@ std::vector<QObject*> PluginContainer::loadProxied(const QString& filepath,
m_Requirements.at(proxiedPlugin).setMaster(*it);
}
}
- }
- }
- } catch (const std::exception& e) {
- reportError(
- QObject::tr("failed to initialize plugin %1: %2").arg(filepath).arg(e.what()));
- }
-
- 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)
{
diff --git a/src/src/process_helper_main.cpp b/src/src/process_helper_main.cpp
deleted file mode 100644
index 185111c..0000000
--- a/src/src/process_helper_main.cpp
+++ /dev/null
@@ -1,359 +0,0 @@
-// Standalone process helper for Flatpak game launching.
-// Runs on the host via flatpak-spawn --host, keeping the flatpak-spawn
-// proxy alive for MO2's PID polling while the game process tree is running.
-//
-// Protocol (stdin/stdout, line-oriented):
-// Config phase: MO2 writes key=value lines terminated by a blank line
-// program=<path>, arg=<value> (repeatable), env=KEY=VALUE (repeatable),
-// workdir=<path>
-// Helper responds: "started <pid>" or "error <message>"
-// Runtime commands (MO2→helper): "kill" (SIGTERM child tree), "quit"
-// Helper reports: "exited <code>" when game process tree exits
-
-#include <dirent.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <poll.h>
-#include <signal.h>
-#include <sys/wait.h>
-#include <unistd.h>
-
-#include <cstdio>
-#include <cstdlib>
-#include <cstring>
-#include <fstream>
-#include <string>
-#include <unordered_set>
-#include <vector>
-
-// ── Helpers ──
-
-static void writeResponse(const std::string& msg)
-{
- std::string line = msg + "\n";
- ::write(STDOUT_FILENO, line.data(), line.size());
-}
-
-static bool readLine(std::string& out, int timeoutMs)
-{
- out.clear();
-
- struct pollfd pfd{};
- pfd.fd = STDIN_FILENO;
- pfd.events = POLLIN;
-
- while (true) {
- int ret = ::poll(&pfd, 1, timeoutMs);
- if (ret < 0) {
- if (errno == EINTR)
- continue;
- return false;
- }
- if (ret == 0) {
- // timeout
- return false;
- }
-
- // Try reading if data is available, even if HUP is also set
- // (pipe can have buffered data when the writer closes).
- if (!(pfd.revents & POLLIN)) {
- // No data available — must be HUP or ERR only
- return false;
- }
-
- char ch = 0;
- ssize_t n = ::read(STDIN_FILENO, &ch, 1);
- if (n <= 0) {
- return false;
- }
- if (ch == '\n') {
- return true;
- }
- out.push_back(ch);
- }
-}
-
-// Collect all descendant PIDs of a given root by scanning /proc.
-static std::unordered_set<pid_t> collectDescendants(pid_t root)
-{
- std::unordered_set<pid_t> descendants;
-
- // Build parent→children map
- struct ProcEntry {
- pid_t pid;
- pid_t ppid;
- };
- std::vector<ProcEntry> entries;
-
- DIR* proc = opendir("/proc");
- if (!proc) {
- return descendants;
- }
-
- struct dirent* entry = nullptr;
- while ((entry = readdir(proc)) != nullptr) {
- if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) {
- continue;
- }
-
- char* end = nullptr;
- long pidLong = strtol(entry->d_name, &end, 10);
- if (end == entry->d_name || *end != '\0' || pidLong <= 0) {
- continue;
- }
-
- pid_t pid = static_cast<pid_t>(pidLong);
-
- char statusPath[64];
- snprintf(statusPath, sizeof(statusPath), "/proc/%ld/status", pidLong);
-
- std::ifstream status(statusPath);
- if (!status.is_open()) {
- continue;
- }
-
- std::string line;
- pid_t ppid = 0;
- while (std::getline(status, line)) {
- if (line.rfind("PPid:", 0) == 0) {
- ppid = static_cast<pid_t>(strtol(line.c_str() + 5, nullptr, 10));
- break;
- }
- }
-
- if (ppid > 0) {
- entries.push_back({pid, ppid});
- }
- }
- closedir(proc);
-
- // BFS from root
- std::vector<pid_t> queue;
- queue.push_back(root);
-
- while (!queue.empty()) {
- pid_t cur = queue.back();
- queue.pop_back();
-
- for (const auto& e : entries) {
- if (e.ppid == cur && descendants.find(e.pid) == descendants.end()) {
- descendants.insert(e.pid);
- queue.push_back(e.pid);
- }
- }
- }
-
- return descendants;
-}
-
-// Check if any process in the given set is still alive.
-static bool anyAlive(const std::unordered_set<pid_t>& pids)
-{
- for (pid_t p : pids) {
- if (::kill(p, 0) == 0 || errno == EPERM) {
- return true;
- }
- }
- return false;
-}
-
-// ── Main ──
-
-int main()
-{
- // Make stdout line-buffered for reliable protocol messages.
- setvbuf(stdout, nullptr, _IOLBF, 0);
-
- // ── Config phase: read key=value lines until blank line ──
- std::string program;
- std::vector<std::string> args;
- std::vector<std::string> envVars; // "KEY=VALUE"
- std::string workdir;
-
- while (true) {
- std::string line;
- if (!readLine(line, 30000)) {
- writeResponse("error stdin closed or timeout during config");
- return 1;
- }
-
- if (line.empty()) {
- break; // blank line = end of config
- }
-
- auto eq = line.find('=');
- if (eq == std::string::npos) {
- continue;
- }
-
- std::string key = line.substr(0, eq);
- std::string val = line.substr(eq + 1);
-
- if (key == "program") {
- program = val;
- } else if (key == "arg") {
- args.push_back(val);
- } else if (key == "env") {
- envVars.push_back(val);
- } else if (key == "workdir") {
- workdir = val;
- }
- }
-
- if (program.empty()) {
- writeResponse("error no program specified");
- return 1;
- }
-
- // ── Pipe for exec error reporting ──
- // Child writes errno to this pipe if execvp fails; parent reads it.
- int errPipe[2];
- if (::pipe2(errPipe, O_CLOEXEC) != 0) {
- writeResponse("error pipe2 failed: " + std::string(strerror(errno)));
- return 1;
- }
-
- // ── Fork ──
- pid_t child = ::fork();
- if (child < 0) {
- writeResponse("error fork failed: " + std::string(strerror(errno)));
- return 1;
- }
-
- if (child == 0) {
- // ── Child ──
- ::close(errPipe[0]); // close read end
-
- // New session so we can kill the whole process group later.
- ::setsid();
-
- // Set environment variables.
- for (const auto& ev : envVars) {
- ::putenv(const_cast<char*>(ev.c_str()));
- }
-
- // Change working directory.
- if (!workdir.empty()) {
- if (::chdir(workdir.c_str()) != 0) {
- int err = errno;
- (void)::write(errPipe[1], &err, sizeof(err));
- ::_exit(127);
- }
- }
-
- // Build argv for execvp.
- std::vector<const char*> argv;
- argv.push_back(program.c_str());
- for (const auto& a : args) {
- argv.push_back(a.c_str());
- }
- argv.push_back(nullptr);
-
- ::execvp(program.c_str(), const_cast<char* const*>(argv.data()));
-
- // If we get here, exec failed.
- int err = errno;
- (void)::write(errPipe[1], &err, sizeof(err));
- ::_exit(127);
- }
-
- // ── Parent ──
- ::close(errPipe[1]); // close write end
-
- // Check if exec succeeded (pipe closes on successful exec due to O_CLOEXEC).
- int execErr = 0;
- ssize_t n = ::read(errPipe[0], &execErr, sizeof(execErr));
- ::close(errPipe[0]);
-
- if (n > 0) {
- // exec failed in child
- ::waitpid(child, nullptr, 0);
- writeResponse("error exec failed: " + std::string(strerror(execErr)));
- return 1;
- }
-
- // Success - report PID.
- writeResponse("started " + std::to_string(child));
-
- // ── Monitor loop ──
- // Wait for the direct child and then monitor descendants (handles Proton
- // chain: proton → wine → game.exe).
- bool childExited = false;
- int childStatus = 0;
- bool quit = false;
-
- while (!quit) {
- // Check for commands on stdin.
- std::string cmd;
- // Non-blocking: if readLine returns false due to timeout, that's fine.
- // If it returns false due to pipe close, MO2 crashed — kill child group.
- struct pollfd pfd{};
- pfd.fd = STDIN_FILENO;
- pfd.events = POLLIN;
-
- int pollRet = ::poll(&pfd, 1, 200);
-
- if (pollRet > 0) {
- if (pfd.revents & (POLLHUP | POLLERR)) {
- // MO2 crashed or closed pipe — kill child group and exit.
- ::kill(-child, SIGTERM);
- break;
- }
-
- if (pfd.revents & POLLIN) {
- if (readLine(cmd, 0)) {
- if (cmd == "kill") {
- ::kill(-child, SIGTERM);
- } else if (cmd == "quit") {
- quit = true;
- break;
- }
- } else {
- // read failed = pipe closed
- ::kill(-child, SIGTERM);
- break;
- }
- }
- }
-
- // Check child status.
- if (!childExited) {
- int status = 0;
- pid_t ret = ::waitpid(child, &status, WNOHANG);
- if (ret == child) {
- childExited = true;
- childStatus = status;
- } else if (ret < 0 && errno != EINTR) {
- // Child somehow lost
- childExited = true;
- childStatus = 0;
- }
- }
-
- if (childExited) {
- // Check for surviving descendants (e.g., game.exe still running
- // after the proton wrapper exits).
- auto desc = collectDescendants(child);
- if (desc.empty() || !anyAlive(desc)) {
- // All done.
- int exitCode = 0;
- if (WIFEXITED(childStatus)) {
- exitCode = WEXITSTATUS(childStatus);
- } else if (WIFSIGNALED(childStatus)) {
- exitCode = 128 + WTERMSIG(childStatus);
- }
- writeResponse("exited " + std::to_string(exitCode));
- return 0;
- }
- // Descendants still alive, keep monitoring.
- }
- }
-
- // If we broke out of the loop, reap child if needed.
- if (!childExited) {
- ::waitpid(child, nullptr, 0);
- }
-
- writeResponse("exited 0");
- return 0;
-}
diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp
index ac3c9eb..e7354f3 100644
--- a/src/src/processrunner.cpp
+++ b/src/src/processrunner.cpp
@@ -1175,7 +1175,6 @@ std::optional<ProcessRunner::Results> ProcessRunner::runBinary()
#ifdef _WIN32
m_handle.reset(startBinary(parent, m_sp));
#else
- m_sp.helperProcessOut = &m_processHelper;
m_handle.reset(reinterpret_cast<HANDLE>(static_cast<intptr_t>(startBinary(parent, m_sp))));
#endif
if (m_handle.get() == INVALID_HANDLE_VALUE) {
@@ -1265,12 +1264,9 @@ ProcessRunner::Results ProcessRunner::postRun()
const QFileInfo binary = m_sp.binary;
QPointer<OrganizerCore> core = &m_core;
- QProcess* helper = m_processHelper;
- m_processHelper = nullptr;
-
- std::thread([core, binary, pid, helper]() {
- // For detached processes (including flatpak-spawn helper),
- // waitpid will fail with ECHILD. Use kill(0) polling instead.
+ std::thread([core, binary, pid]() {
+ // For detached processes, waitpid will fail with ECHILD.
+ // Use kill(0) polling instead.
int status = 0;
pid_t waited = -1;
do {
@@ -1294,16 +1290,6 @@ ProcessRunner::Results ProcessRunner::postRun()
errno);
}
- // Clean up helper QProcess on the main thread.
- if (helper) {
- QMetaObject::invokeMethod(
- QCoreApplication::instance(), [helper]() {
- helper->waitForFinished(1000);
- delete helper;
- },
- Qt::QueuedConnection);
- }
-
if (!core) {
return;
}
@@ -1352,14 +1338,6 @@ ProcessRunner::Results ProcessRunner::postRun()
});
}
-#ifndef _WIN32
- // Clean up the process helper (keeps flatpak-spawn alive during game).
- if (m_processHelper) {
- m_processHelper->waitForFinished(1000);
- delete m_processHelper;
- m_processHelper = nullptr;
- }
-#endif
if (shouldRefresh(r)) {
QEventLoop loop;
diff --git a/src/src/processrunner.h b/src/src/processrunner.h
index e825e98..2736c86 100644
--- a/src/src/processrunner.h
+++ b/src/src/processrunner.h
@@ -185,9 +185,6 @@ private:
QFileInfo m_shellOpen;
env::HandlePtr m_handle;
DWORD m_exitCode;
-#ifndef _WIN32
- QProcess* m_processHelper = nullptr;
-#endif
bool shouldRunShell() const;
bool shouldRefresh(Results r) const;
diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp
index a2aa042..f2bd7a0 100644
--- a/src/src/protonlauncher.cpp
+++ b/src/src/protonlauncher.cpp
@@ -1,5 +1,4 @@
#include "protonlauncher.h"
-#include "fluorinepaths.h"
#include <nak_ffi.h>
#include <QCoreApplication>
@@ -102,43 +101,6 @@ void maybeWrapWithSteamRun(bool useSteamRun, QString& program, QStringList& argu
arguments = wrappedArgs;
}
-bool isFlatpak()
-{
- return QFileInfo::exists(QStringLiteral("/.flatpak-info"));
-}
-
-// In Flatpak, Wine/Proton binaries can't execute inside the sandbox (they need
-// the Steam Runtime's linker and 32-bit libs). Wrap them with flatpak-spawn
-// --host so they run on the host system instead.
-//
-// flatpak-spawn --host runs via the Flatpak portal D-Bus interface, which does
-// NOT reliably forward the caller's process environment. We must pass any
-// custom env vars explicitly with --env= flags.
-void maybeWrapForFlatpak(QString& program, QStringList& arguments,
- const QProcessEnvironment& env)
-{
- if (!isFlatpak()) {
- return;
- }
-
- QStringList wrappedArgs;
- wrappedArgs.append(QStringLiteral("--host"));
-
- // Pass every env var that differs from the inherited system environment.
- const QProcessEnvironment sysEnv = QProcessEnvironment::systemEnvironment();
- for (const QString& key : env.keys()) {
- const QString val = env.value(key);
- if (val != sysEnv.value(key)) {
- wrappedArgs.append(QStringLiteral("--env=%1=%2").arg(key, val));
- }
- }
-
- wrappedArgs.append(program);
- wrappedArgs.append(arguments);
- program = QStringLiteral("flatpak-spawn");
- arguments = wrappedArgs;
-}
-
bool isValidEnvKey(const QString& key)
{
if (key.isEmpty()) {
@@ -179,7 +141,7 @@ bool parseEnvAssignment(const QString& token, QString& keyOut, QString& valueOut
ProtonLauncher::ProtonLauncher()
: m_steamAppId(0), m_useUmu(false), m_preferSystemUmu(false),
- m_useSteamRun(false)
+ m_useSteamRun(false), m_useSteamDrm(true)
{}
ProtonLauncher& ProtonLauncher::setBinary(const QString& path)
@@ -258,6 +220,12 @@ ProtonLauncher& ProtonLauncher::setUseSteamRun(bool useSteamRun)
return *this;
}
+ProtonLauncher& ProtonLauncher::setSteamDrm(bool useSteamDrm)
+{
+ m_useSteamDrm = useSteamDrm;
+ return *this;
+}
+
ProtonLauncher& ProtonLauncher::addEnvVar(const QString& key, const QString& value)
{
if (!key.isEmpty()) {
@@ -267,12 +235,6 @@ ProtonLauncher& ProtonLauncher::addEnvVar(const QString& key, const QString& val
return *this;
}
-ProtonLauncher& ProtonLauncher::setHelperProcessOut(QProcess** out)
-{
- m_helperProcessOut = out;
- return *this;
-}
-
std::pair<bool, qint64> ProtonLauncher::launch() const
{
qint64 pid = -1;
@@ -297,7 +259,9 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const
return false;
}
- ensureSteamRunning();
+ if (m_useSteamDrm) {
+ ensureSteamRunning();
+ }
QString protonScript = m_protonPath;
if (QFileInfo(protonScript).isDir()) {
@@ -311,7 +275,6 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const
wrapProgram(m_wrapperCommands, protonScript, protonArgs, program, arguments);
maybeWrapWithSteamRun(m_useSteamRun, program, arguments);
- // Build environment BEFORE flatpak wrapping (flatpak-spawn needs --env= flags).
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.remove("PYTHONHOME");
@@ -357,11 +320,6 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const
MOBase::log::info("Proton launch: '{}' run '{}'", protonScript, m_binary);
- if (isFlatpak()) {
- return launchViaProcessHelper(program, arguments, env, m_workingDir, pid);
- }
-
- maybeWrapForFlatpak(program, arguments, env);
return startDetachedWithEnv(program, arguments, m_workingDir, env, pid);
}
@@ -373,61 +331,51 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const
// Steam must be running for games with Steamworks DRM (Application Load
// Error 5:0000065434 occurs otherwise).
- ensureSteamRunning();
+ if (m_useSteamDrm) {
+ ensureSteamRunning();
+ }
// Resolve umu-run according to user preference (bundled vs system).
- // In Flatpak, umu-run must run on the host (it needs Steam Runtime).
- // Use the full path to our copied umu-run since the host PATH won't include it.
- QString umuRun;
- if (isFlatpak()) {
- const QString flatpakUmu = fluorineDataDir() + QStringLiteral("/umu-run");
- if (QFileInfo::exists(flatpakUmu)) {
- umuRun = flatpakUmu;
- } else {
- // Fall back to bare name (requires host to have umu-run in PATH)
- umuRun = QStringLiteral("umu-run");
- }
- } else {
- const QString appDir = QCoreApplication::applicationDirPath();
- const QString bundled = appDir + QStringLiteral("/umu-run");
+ const QString appDir = QCoreApplication::applicationDirPath();
+ const QString bundled = appDir + QStringLiteral("/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
- // bundled copy otherwise).
- QString system;
- const QStringList pathDirs =
- QString::fromLocal8Bit(qgetenv("PATH")).split(QLatin1Char(':'), Qt::SkipEmptyParts);
- for (const QString& dir : pathDirs) {
- if (QDir(dir) == QDir(appDir))
- continue;
- const QString candidate = QStandardPaths::findExecutable(
- QStringLiteral("umu-run"), {dir});
- if (!candidate.isEmpty()) {
- system = candidate;
- break;
- }
+ // Search PATH for a system umu-run, excluding our own app directory
+ // (the launcher prepends it to PATH, so findExecutable would find the
+ // bundled copy otherwise).
+ QString system;
+ const QStringList pathDirs =
+ QString::fromLocal8Bit(qgetenv("PATH")).split(QLatin1Char(':'), Qt::SkipEmptyParts);
+ for (const QString& dir : pathDirs) {
+ if (QDir(dir) == QDir(appDir))
+ continue;
+ const QString candidate = QStandardPaths::findExecutable(
+ QStringLiteral("umu-run"), {dir});
+ if (!candidate.isEmpty()) {
+ system = candidate;
+ break;
}
+ }
- 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;
- }
+ 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;
}
-
- MOBase::log::info("umu-run: preferSystem={}, bundled='{}' (exists={}), system='{}', selected='{}'",
- m_preferSystemUmu, bundled, QFileInfo::exists(bundled), system, umuRun);
}
+ MOBase::log::info("umu-run: preferSystem={}, bundled='{}' (exists={}), system='{}', selected='{}'",
+ m_preferSystemUmu, bundled, QFileInfo::exists(bundled), system, umuRun);
+
if (umuRun.isEmpty()) {
MOBase::log::warn("umu-run not found (bundled or in PATH)");
return false;
@@ -440,7 +388,6 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const
wrapProgram(m_wrapperCommands, umuRun, umuArgs, program, arguments);
maybeWrapWithSteamRun(m_useSteamRun, program, arguments);
- // Build environment BEFORE flatpak wrapping (flatpak-spawn needs --env= flags).
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.remove("PYTHONHOME");
@@ -471,11 +418,15 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const
}
}
- if (effectiveSteamAppId != 0) {
+ if (m_useSteamDrm && effectiveSteamAppId != 0) {
// umu-run expects GAMEID in "umu-<AppID>" format to extract SteamAppId.
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"));
}
for (auto it = m_wrapperEnvVars.cbegin(); it != m_wrapperEnvVars.cend(); ++it) {
@@ -503,11 +454,6 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const
QString::number(effectiveSteamAppId)),
steamPath);
- if (isFlatpak()) {
- return launchViaProcessHelper(program, arguments, env, m_workingDir, pid);
- }
-
- maybeWrapForFlatpak(program, arguments, env);
return startDetachedWithEnv(program, arguments, m_workingDir, env, pid);
}
@@ -531,130 +477,21 @@ bool ProtonLauncher::launchDirect(qint64& pid) const
env.insert(it.key(), it.value());
}
- if (isFlatpak()) {
- return launchViaProcessHelper(program, arguments, env, m_workingDir, pid);
- }
-
- maybeWrapForFlatpak(program, arguments, env);
return startDetachedWithEnv(program, arguments, m_workingDir, env, pid);
}
-bool ProtonLauncher::launchViaProcessHelper(
- const QString& program, const QStringList& arguments,
- const QProcessEnvironment& env, const QString& workingDir,
- qint64& pid) const
-{
- const QString helperBin = fluorineDataDir() + QStringLiteral("/bin/mo2-process-helper");
- if (!QFileInfo::exists(helperBin)) {
- MOBase::log::warn("mo2-process-helper not found at '{}', falling back to "
- "flatpak-spawn direct launch", helperBin);
- // Fall back to old direct flatpak-spawn path.
- QString prog = program;
- QStringList args = arguments;
- QProcessEnvironment envCopy = env;
- maybeWrapForFlatpak(prog, args, envCopy);
- return startDetachedWithEnv(prog, args, workingDir, envCopy, pid);
- }
-
- auto* proc = new QProcess();
- proc->setProcessChannelMode(QProcess::SeparateChannels);
- proc->start(QStringLiteral("flatpak-spawn"),
- {QStringLiteral("--host"), helperBin});
-
- if (!proc->waitForStarted(5000)) {
- MOBase::log::error("Failed to start flatpak-spawn for process helper");
- delete proc;
- return false;
- }
-
- // Write config block to helper's stdin.
- auto writeLine = [&](const QString& line) {
- proc->write(line.toUtf8());
- proc->write("\n");
- };
-
- writeLine(QStringLiteral("program=") + program);
- for (const QString& arg : arguments) {
- writeLine(QStringLiteral("arg=") + arg);
- }
-
- // Send env vars that differ from system environment.
- const QProcessEnvironment sysEnv = QProcessEnvironment::systemEnvironment();
- for (const QString& key : env.keys()) {
- const QString val = env.value(key);
- if (val != sysEnv.value(key)) {
- writeLine(QStringLiteral("env=") + key + QStringLiteral("=") + val);
- }
- }
-
- if (!workingDir.isEmpty()) {
- writeLine(QStringLiteral("workdir=") + workingDir);
- }
-
- // Blank line terminates config.
- proc->write("\n");
- proc->waitForBytesWritten(2000);
-
- // Read response: "started <pid>" or "error <message>"
- if (!proc->waitForReadyRead(10000)) {
- MOBase::log::error("Process helper did not respond in time");
- proc->kill();
- proc->waitForFinished(2000);
- delete proc;
- return false;
- }
-
- const QString response = QString::fromUtf8(proc->readLine()).trimmed();
- if (response.startsWith(QStringLiteral("started "))) {
- MOBase::log::info("Process helper: {}", response);
- } else {
- MOBase::log::error("Process helper error: {}", response);
- proc->kill();
- proc->waitForFinished(2000);
- delete proc;
- return false;
- }
-
- // Use the flatpak-spawn PID for kill(pid,0) polling.
- pid = proc->processId();
-
- // Store the QProcess so it stays alive (keeping flatpak-spawn alive).
- if (m_helperProcessOut) {
- *m_helperProcessOut = proc;
- } else {
- // No owner provided — leak intentionally to keep the pipe alive.
- // The process will clean up when the game exits.
- MOBase::log::debug("No helper process owner set, helper will self-manage");
- }
-
- return true;
-}
-
bool ProtonLauncher::ensureSteamRunning()
{
QProcess pgrep;
- if (isFlatpak()) {
- // In Flatpak, check for Steam on the host.
- pgrep.start("flatpak-spawn", {"--host", "pgrep", "-x", "steam"});
- } else {
- pgrep.start("pgrep", {"-x", "steam"});
- }
+ pgrep.start("pgrep", {"-x", "steam"});
if (pgrep.waitForFinished(2000) && pgrep.exitCode() == 0) {
return true;
}
qint64 pid = -1;
- if (isFlatpak()) {
- if (QProcess::startDetached("flatpak-spawn",
- {"--host", "steam", "-silent"}, QString(), &pid)) {
- MOBase::log::warn("Steam was not running, started it on host in silent mode");
- return true;
- }
- } else {
- if (QProcess::startDetached("steam", {"-silent"}, QString(), &pid)) {
- MOBase::log::warn("Steam was not running, started it in silent mode");
- return true;
- }
+ if (QProcess::startDetached("steam", {"-silent"}, QString(), &pid)) {
+ MOBase::log::warn("Steam was not running, started it in silent mode");
+ return true;
}
return false;
diff --git a/src/src/protonlauncher.h b/src/src/protonlauncher.h
index d0213f6..5480c69 100644
--- a/src/src/protonlauncher.h
+++ b/src/src/protonlauncher.h
@@ -8,8 +8,6 @@
#include <cstdint>
#include <utility>
-class QProcess;
-
class ProtonLauncher
{
public:
@@ -25,8 +23,8 @@ public:
ProtonLauncher& setUmu(bool useUmu);
ProtonLauncher& setPreferSystemUmu(bool preferSystemUmu);
ProtonLauncher& setUseSteamRun(bool useSteamRun);
+ ProtonLauncher& setSteamDrm(bool useSteamDrm);
ProtonLauncher& addEnvVar(const QString& key, const QString& value);
- ProtonLauncher& setHelperProcessOut(QProcess** out);
// Launch dispatch: UMU -> Proton -> Direct
std::pair<bool, qint64> launch() const;
@@ -35,9 +33,6 @@ private:
bool launchWithProton(qint64& pid) const;
bool launchWithUmu(qint64& pid) const;
bool launchDirect(qint64& pid) const;
- bool launchViaProcessHelper(const QString& program, const QStringList& arguments,
- const QProcessEnvironment& env,
- const QString& workingDir, qint64& pid) const;
static bool ensureSteamRunning();
QString m_binary;
@@ -50,9 +45,9 @@ private:
bool m_useUmu;
bool m_preferSystemUmu;
bool m_useSteamRun;
+ bool m_useSteamDrm;
QMap<QString, QString> m_envVars;
QMap<QString, QString> m_wrapperEnvVars;
- QProcess** m_helperProcessOut = nullptr;
};
#endif // PROTONLAUNCHER_H
diff --git a/src/src/sanitychecks.cpp b/src/src/sanitychecks.cpp
index d6b285e..df0d785 100644
--- a/src/src/sanitychecks.cpp
+++ b/src/src/sanitychecks.cpp
@@ -385,9 +385,8 @@ int checkMissingFiles()
log::debug(" . checking Linux dependencies");
int n = 0;
- // Check for FUSE (skip in Flatpak — the VFS helper runs on the host)
- if (!QFileInfo::exists("/.flatpak-info") &&
- !QFileInfo::exists("/usr/lib/libfuse3.so") &&
+ // Check for FUSE
+ if (!QFileInfo::exists("/usr/lib/libfuse3.so") &&
!QFileInfo::exists("/usr/lib64/libfuse3.so") &&
!QFileInfo::exists("/usr/lib/x86_64-linux-gnu/libfuse3.so")) {
log::warn("libfuse3 not found - FUSE VFS will not work");
diff --git a/src/src/settingsdialogpaths.cpp b/src/src/settingsdialogpaths.cpp
index dee5362..788ad60 100644
--- a/src/src/settingsdialogpaths.cpp
+++ b/src/src/settingsdialogpaths.cpp
@@ -4,18 +4,6 @@
#include <QFileDialog>
#include <iplugingame.h>
-namespace {
-QFileDialog::Options flatpakSafeOptions()
-{
- QFileDialog::Options opts;
-#ifndef _WIN32
- if (qEnvironmentVariableIsSet("FLATPAK_ID"))
- opts |= QFileDialog::DontUseNativeDialog;
-#endif
- return opts;
-}
-} // namespace
-
PathsSettingsTab::PathsSettingsTab(Settings& s, SettingsDialog& d)
: SettingsTab(s, d), m_gameDir(settings().game().plugin()->gameDirectory())
{
@@ -143,7 +131,7 @@ void PathsSettingsTab::on_browseBaseDirBtn_clicked()
{
QString temp = QFileDialog::getExistingDirectory(
&dialog(), QObject::tr("Select base directory"), ui->baseDirEdit->text(),
- flatpakSafeOptions());
+ {});
if (!temp.isEmpty()) {
ui->baseDirEdit->setText(temp);
}
@@ -156,7 +144,7 @@ void PathsSettingsTab::on_browseDownloadDirBtn_clicked()
QString temp = QFileDialog::getExistingDirectory(
&dialog(), QObject::tr("Select download directory"), searchPath,
- flatpakSafeOptions());
+ {});
if (!temp.isEmpty()) {
ui->downloadDirEdit->setText(temp);
}
@@ -169,7 +157,7 @@ void PathsSettingsTab::on_browseModDirBtn_clicked()
QString temp = QFileDialog::getExistingDirectory(
&dialog(), QObject::tr("Select mod directory"), searchPath,
- flatpakSafeOptions());
+ {});
if (!temp.isEmpty()) {
ui->modDirEdit->setText(temp);
}
@@ -182,7 +170,7 @@ void PathsSettingsTab::on_browseCacheDirBtn_clicked()
QString temp = QFileDialog::getExistingDirectory(
&dialog(), QObject::tr("Select cache directory"), searchPath,
- flatpakSafeOptions());
+ {});
if (!temp.isEmpty()) {
ui->cacheDirEdit->setText(temp);
}
@@ -195,7 +183,7 @@ void PathsSettingsTab::on_browseProfilesDirBtn_clicked()
QString temp = QFileDialog::getExistingDirectory(
&dialog(), QObject::tr("Select profiles directory"), searchPath,
- flatpakSafeOptions());
+ {});
if (!temp.isEmpty()) {
ui->profilesDirEdit->setText(temp);
}
@@ -208,7 +196,7 @@ void PathsSettingsTab::on_browseOverwriteDirBtn_clicked()
QString temp = QFileDialog::getExistingDirectory(
&dialog(), QObject::tr("Select overwrite directory"), searchPath,
- flatpakSafeOptions());
+ {});
if (!temp.isEmpty()) {
ui->overwriteDirEdit->setText(temp);
}
diff --git a/src/src/settingsdialogproton.cpp b/src/src/settingsdialogproton.cpp
index e9d1bec..f202f5f 100644
--- a/src/src/settingsdialogproton.cpp
+++ b/src/src/settingsdialogproton.cpp
@@ -313,14 +313,8 @@ void ProtonSettingsTab::onOpenPrefixFolder()
void ProtonSettingsTab::onBrowsePrefixLocation()
{
- QFileDialog::Options opts;
-#ifndef _WIN32
- if (qEnvironmentVariableIsSet("FLATPAK_ID"))
- opts |= QFileDialog::DontUseNativeDialog;
-#endif
const QString dir = QFileDialog::getExistingDirectory(
- parentWidget(), tr("Select Prefix Location"), ui->prefixLocationEdit->text(),
- opts);
+ parentWidget(), tr("Select Prefix Location"), ui->prefixLocationEdit->text());
if (!dir.isEmpty()) {
ui->prefixLocationEdit->setText(dir);
}
@@ -425,41 +419,22 @@ void ProtonSettingsTab::onWinetricks()
}
}
- // In Flatpak, wine/winetricks must run on the host via flatpak-spawn --host
- // because Proton binaries need the host's linker and Steam Runtime libs.
- const bool flatpak = QFileInfo::exists(QStringLiteral("/.flatpak-info"));
-
- QString program;
+ QString program = winetricksPath;
QStringList arguments;
+ arguments << QStringLiteral("--gui");
- if (flatpak) {
- program = QStringLiteral("flatpak-spawn");
- arguments << QStringLiteral("--host");
- for (const QString& flag : envFlags) {
- arguments << QStringLiteral("--env=%1").arg(flag);
- }
- arguments << winetricksPath << QStringLiteral("--gui");
- } else {
- program = winetricksPath;
- arguments << QStringLiteral("--gui");
-
- // For non-Flatpak, set env vars directly on the process
- QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
- for (const QString& flag : envFlags) {
- const int eq = flag.indexOf('=');
- if (eq > 0) {
- env.insert(flag.left(eq), flag.mid(eq + 1));
- }
+ QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
+ for (const QString& flag : envFlags) {
+ const int eq = flag.indexOf('=');
+ if (eq > 0) {
+ env.insert(flag.left(eq), flag.mid(eq + 1));
}
- QProcess proc;
- proc.setProcessEnvironment(env);
- proc.setProgram(program);
- proc.setArguments(arguments);
- proc.startDetached();
- return;
}
-
- QProcess::startDetached(program, arguments);
+ QProcess proc;
+ proc.setProcessEnvironment(env);
+ proc.setProgram(program);
+ proc.setArguments(arguments);
+ proc.startDetached();
}
void ProtonSettingsTab::onFixGameRegistries()
@@ -516,14 +491,9 @@ void ProtonSettingsTab::showGameRegistryDialog()
layout->addLayout(pathLayout);
QObject::connect(browseBtn, &QPushButton::clicked, &dialog, [&dialog, pathEdit]() {
- QFileDialog::Options opts;
-#ifndef _WIN32
- if (qEnvironmentVariableIsSet("FLATPAK_ID"))
- opts |= QFileDialog::DontUseNativeDialog;
-#endif
const QString dir = QFileDialog::getExistingDirectory(
&dialog, QObject::tr("Select Game Installation Folder"),
- pathEdit->text().isEmpty() ? QDir::homePath() : pathEdit->text(), opts);
+ pathEdit->text().isEmpty() ? QDir::homePath() : pathEdit->text());
if (!dir.isEmpty()) {
pathEdit->setText(dir);
}
diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp
index c2f15eb..05e07bd 100644
--- a/src/src/spawn.cpp
+++ b/src/src/spawn.cpp
@@ -644,7 +644,7 @@ 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())
- .setHelperProcessOut(sp.helperProcessOut);
+ .setSteamDrm(QSettings().value("fluorine/steam_drm", true).toBool());
const QString prefixPath = resolvePrefixPath();
if (prefixPath.isEmpty()) {
diff --git a/src/src/spawn.h b/src/src/spawn.h
index 72c81be..d922ddc 100644
--- a/src/src/spawn.h
+++ b/src/src/spawn.h
@@ -63,7 +63,6 @@ struct SpawnParameters
#else
int stdOut = -1;
int stdErr = -1;
- QProcess** helperProcessOut = nullptr;
#endif
};
diff --git a/src/src/vfs/vfs_helper_main.cpp b/src/src/vfs/vfs_helper_main.cpp
deleted file mode 100644
index d983574..0000000
--- a/src/src/vfs/vfs_helper_main.cpp
+++ /dev/null
@@ -1,337 +0,0 @@
-// Standalone VFS helper for Flatpak FUSE support.
-// Runs on the host via flatpak-spawn --host, where FUSE works normally.
-// Communicates with MO2 GUI via stdin/stdout pipes.
-
-#include "inodetable.h"
-#include "mo2filesystem.h"
-#include "overwritemanager.h"
-#include "vfstree.h"
-
-#include <fuse3/fuse_lowlevel.h>
-
-#include <fcntl.h>
-#include <signal.h>
-#include <sys/wait.h>
-#include <unistd.h>
-
-#include <cstring>
-#include <filesystem>
-#include <fstream>
-#include <iostream>
-#include <memory>
-#include <string>
-#include <thread>
-#include <vector>
-
-namespace fs = std::filesystem;
-
-struct HelperConfig
-{
- std::string mount_point;
- std::string game_dir;
- std::string data_dir_name;
- std::string overwrite_dir;
- std::string output_dir;
- std::vector<std::pair<std::string, std::string>> mods;
- std::vector<std::pair<std::string, std::string>> extra_files;
-};
-
-static HelperConfig readConfig(const std::string& path)
-{
- HelperConfig cfg;
- std::ifstream in(path);
- std::string line;
-
- while (std::getline(in, line)) {
- if (line.empty() || line[0] == '#') {
- continue;
- }
-
- const auto eq = line.find('=');
- if (eq == std::string::npos) {
- continue;
- }
-
- const std::string key = line.substr(0, eq);
- const std::string val = line.substr(eq + 1);
-
- if (key == "mount_point") {
- cfg.mount_point = val;
- } else if (key == "game_dir") {
- cfg.game_dir = val;
- } else if (key == "data_dir_name") {
- cfg.data_dir_name = val;
- } else if (key == "overwrite_dir") {
- cfg.overwrite_dir = val;
- } else if (key == "output_dir") {
- cfg.output_dir = val;
- } else if (key == "mod") {
- const auto pipe = val.find('|');
- if (pipe != std::string::npos) {
- cfg.mods.emplace_back(val.substr(0, pipe), val.substr(pipe + 1));
- }
- } else if (key == "extra_file") {
- const auto pipe = val.find('|');
- if (pipe != std::string::npos) {
- cfg.extra_files.emplace_back(val.substr(0, pipe), val.substr(pipe + 1));
- }
- }
- }
-
- return cfg;
-}
-
-static void tryUnmountStale(const std::string& path)
-{
- pid_t pid = fork();
- if (pid == 0) {
- int devnull = open("/dev/null", O_WRONLY);
- if (devnull >= 0) {
- dup2(devnull, STDERR_FILENO);
- close(devnull);
- }
- execlp("fusermount3", "fusermount3", "-u", path.c_str(), nullptr);
- _exit(1);
- }
- if (pid > 0) {
- int status;
- waitpid(pid, &status, 0);
- }
-}
-
-static void flushStaging(const std::string& stagingDir,
- const std::string& overwriteDir,
- const std::string& outputDir = {})
-{
- const fs::path staging(stagingDir);
- const fs::path overwrite = outputDir.empty()
- ? fs::path(overwriteDir)
- : fs::path(outputDir);
- if (!fs::exists(staging)) {
- return;
- }
-
- std::error_code ec;
- for (auto it = fs::recursive_directory_iterator(
- staging, fs::directory_options::skip_permission_denied);
- it != fs::recursive_directory_iterator(); ++it) {
- const auto& entry = *it;
- const fs::path rel = fs::relative(entry.path(), staging, ec);
- if (ec || rel.empty()) {
- continue;
- }
-
- const fs::path dest = overwrite / rel;
- if (entry.is_directory(ec)) {
- fs::create_directories(dest, ec);
- continue;
- }
-
- if (!entry.is_regular_file(ec)) {
- continue;
- }
-
- fs::create_directories(dest.parent_path(), ec);
- fs::rename(entry.path(), dest, ec);
- if (ec) {
- ec.clear();
- fs::copy_file(entry.path(), dest, fs::copy_options::overwrite_existing, ec);
- if (!ec) {
- fs::remove(entry.path(), ec);
- }
- }
- }
-
- fs::remove_all(staging, ec);
-}
-
-static void setupFuseOps(struct fuse_lowlevel_ops* ops)
-{
- std::memset(ops, 0, sizeof(struct fuse_lowlevel_ops));
- ops->lookup = mo2_lookup;
- ops->getattr = mo2_getattr;
- ops->readdir = mo2_readdir;
- ops->open = mo2_open;
- ops->read = mo2_read;
- ops->write = mo2_write;
- ops->create = mo2_create;
- ops->rename = mo2_rename;
- ops->setattr = mo2_setattr;
- ops->unlink = mo2_unlink;
- ops->mkdir = mo2_mkdir;
- ops->release = mo2_release;
-}
-
-static struct fuse_session* g_session = nullptr;
-
-static void signalHandler(int /*sig*/)
-{
- if (g_session) {
- fuse_session_exit(g_session);
- }
-}
-
-int main(int argc, char* argv[])
-{
- if (argc < 2) {
- std::cerr << "Usage: mo2-vfs-helper <config-file>\n";
- return 1;
- }
-
- const std::string configPath = argv[1];
- auto config = readConfig(configPath);
-
- if (config.mount_point.empty()) {
- std::cout << "error: mount_point not set in config" << std::endl;
- return 1;
- }
-
- const std::string dataDirPath = config.mount_point;
- const std::string stagingDir =
- (fs::path(config.overwrite_dir).parent_path() / "VFS_staging").string();
-
- if (!fs::exists(dataDirPath)) {
- std::cout << "error: data directory does not exist: " << dataDirPath
- << std::endl;
- return 1;
- }
-
- std::error_code ec;
- fs::create_directories(stagingDir, ec);
- fs::create_directories(config.overwrite_dir, ec);
- if (!config.output_dir.empty()) {
- fs::create_directories(config.output_dir, ec);
- }
-
- // Scan base game files BEFORE mounting (after mount they're hidden)
- auto baseFileCache = scanDataDir(dataDirPath);
-
- // Open fd to data dir BEFORE mounting so we can access original files
- int backingFd = open(dataDirPath.c_str(), O_RDONLY | O_DIRECTORY);
- if (backingFd < 0) {
- std::cout << "error: failed to open backing fd for " << dataDirPath
- << std::endl;
- return 1;
- }
-
- // Clean up any stale FUSE mount
- tryUnmountStale(dataDirPath);
-
- // Build VFS tree
- auto tree = std::make_shared<VfsTree>(
- buildDataDirVfs(baseFileCache, dataDirPath, config.mods,
- config.overwrite_dir));
- injectExtraFiles(*tree, config.extra_files);
-
- auto context = std::make_shared<Mo2FsContext>();
- context->tree = tree;
- context->inodes = std::make_unique<InodeTable>();
- context->overwrite =
- std::make_unique<OverwriteManager>(stagingDir, config.overwrite_dir);
- context->backing_dir_fd = backingFd;
- context->uid = ::getuid();
- context->gid = ::getgid();
-
- // Setup FUSE
- std::vector<std::string> argvStorage = {
- "mo2-vfs-helper", "-o", "fsname=mo2linux", "-o", "default_permissions",
- "-o", "noatime"};
-
- std::vector<char*> fuseArgv;
- fuseArgv.reserve(argvStorage.size());
- for (auto& s : argvStorage) {
- fuseArgv.push_back(s.data());
- }
-
- struct fuse_args args =
- FUSE_ARGS_INIT(static_cast<int>(fuseArgv.size()), fuseArgv.data());
-
- struct fuse_lowlevel_ops ops;
- setupFuseOps(&ops);
-
- struct fuse_session* session =
- fuse_session_new(&args, &ops, sizeof(ops), context.get());
- if (session == nullptr) {
- close(backingFd);
- std::cout << "error: failed to create FUSE session" << std::endl;
- return 1;
- }
-
- if (fuse_session_mount(session, dataDirPath.c_str()) != 0) {
- fuse_session_destroy(session);
- close(backingFd);
- std::cout << "error: failed to mount FUSE at " << dataDirPath << std::endl;
- return 1;
- }
-
- g_session = session;
-
- // Handle signals for clean shutdown
- struct sigaction sa;
- sa.sa_handler = signalHandler;
- sigemptyset(&sa.sa_mask);
- sa.sa_flags = 0;
- sigaction(SIGINT, &sa, nullptr);
- sigaction(SIGTERM, &sa, nullptr);
-
- // Start FUSE event loop in background thread
- std::thread fuseThread([session]() {
- fuse_session_loop_mt(session, nullptr);
- });
-
- std::cout << "mounted" << std::endl;
-
- // Command loop: read commands from stdin
- std::string line;
- while (std::getline(std::cin, line)) {
- if (line == "rebuild") {
- auto newConfig = readConfig(configPath);
- auto newTree = std::make_shared<VfsTree>(buildDataDirVfs(
- baseFileCache, dataDirPath, newConfig.mods, newConfig.overwrite_dir));
- injectExtraFiles(*newTree, newConfig.extra_files);
-
- {
- std::unique_lock lock(context->tree_mutex);
- context->tree.swap(newTree);
- }
-
- config = newConfig;
- std::cout << "ok" << std::endl;
- } else if (line == "flush") {
- flushStaging(stagingDir, config.overwrite_dir, config.output_dir);
- fs::create_directories(stagingDir, ec);
-
- auto newTree = std::make_shared<VfsTree>(buildDataDirVfs(
- baseFileCache, dataDirPath, config.mods, config.overwrite_dir));
- injectExtraFiles(*newTree, config.extra_files);
-
- {
- std::unique_lock lock(context->tree_mutex);
- context->tree.swap(newTree);
- }
-
- context->overwrite =
- std::make_unique<OverwriteManager>(stagingDir, config.overwrite_dir);
- std::cout << "ok" << std::endl;
- } else if (line == "quit") {
- break;
- }
- }
-
- // Clean shutdown
- fuse_session_exit(session);
- fuse_session_unmount(session);
-
- if (fuseThread.joinable()) {
- fuseThread.join();
- }
-
- fuse_session_destroy(session);
- g_session = nullptr;
-
- flushStaging(stagingDir, config.overwrite_dir, config.output_dir);
- close(backingFd);
-
- std::cout << "ok" << std::endl;
- return 0;
-}
diff --git a/src/src/wineprefix.cpp b/src/src/wineprefix.cpp
index 8ff20e6..bb4e166 100644
--- a/src/src/wineprefix.cpp
+++ b/src/src/wineprefix.cpp
@@ -145,8 +145,8 @@ bool WinePrefix::deployPlugins(const QStringList& plugins, const QString& dataDi
}
const QString pluginsDir = QDir(appdataLocal()).filePath(dataDir);
- MOBase::log::debug("deployPlugins: target dir='{}', {} plugins to deploy",
- pluginsDir, plugins.size());
+ MOBase::log::info("deployPlugins: target dir='{}', count={}", pluginsDir,
+ plugins.size());
if (!QDir().mkpath(pluginsDir)) {
MOBase::log::error("deployPlugins: failed to create directory '{}'", pluginsDir);
@@ -179,8 +179,8 @@ bool WinePrefix::deployPlugins(const QStringList& plugins, const QString& dataDi
pluginsStream << plugin << "\r\n";
}
pluginsFile.close();
- MOBase::log::debug("deployPlugins: wrote {} plugins to '{}'", plugins.size(),
- pluginsPath);
+ MOBase::log::info("deployPlugins: wrote {} plugins to '{}'", plugins.size(),
+ pluginsPath);
// Also write lowercase "plugins.txt" for games that expect it (e.g. FalloutNV).
const QString pluginsLower = QDir(pluginsDir).filePath("plugins.txt");
@@ -208,7 +208,7 @@ bool WinePrefix::deployPlugins(const QStringList& plugins, const QString& dataDi
loadOrderStream << line << "\r\n";
}
- MOBase::log::debug("deployPlugins: wrote loadorder.txt to '{}'", loadOrderPath);
+ MOBase::log::info("deployPlugins: wrote loadorder.txt to '{}'", loadOrderPath);
return true;
}