aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore21
-rwxr-xr-xbuild-flatpak.sh9
-rwxr-xr-xbuild.sh58
-rw-r--r--data/com.fluorine.manager.desktop (renamed from flatpak/com.fluorine.manager.desktop)0
-rw-r--r--data/com.fluorine.manager.metainfo.xml (renamed from flatpak/com.fluorine.manager.metainfo.xml)0
-rw-r--r--data/com.fluorine.manager.png (renamed from flatpak/com.fluorine.manager.png)bin51022 -> 51022 bytes
-rw-r--r--docker/Dockerfile1
-rwxr-xr-xdocker/build-inner.sh279
-rw-r--r--flatpak/com.fluorine.manager.yml371
-rwxr-xr-xflatpak/flatpak-install.sh60
-rwxr-xr-xflatpak/fluorine-manager-wrapper.sh114
-rw-r--r--libs/basic_games/games/baldursgate3/pak_parser.py21
-rw-r--r--libs/basic_games/games/game_monsterhunterwilds.py15
-rw-r--r--libs/game_bethesda/src/*.cpp0
-rw-r--r--libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp4
-rw-r--r--libs/game_bethesda/src/gamebryo/gamegamebryo.cpp6
-rw-r--r--libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.cpp4
-rw-r--r--libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp2
-rw-r--r--libs/installer_fomod_plus/installer/lib/Logger.h2
-rw-r--r--libs/installer_fomod_plus/installer/ui/UIHelper.cpp1
-rw-r--r--libs/installer_fomod_plus/patchwizard/lib/PatchFinder.cpp1
-rw-r--r--libs/installer_fomod_plus/scanner/archiveparser.h1
-rw-r--r--libs/installer_fomod_plus/share/FOMODData/FomodDB.h2
-rw-r--r--libs/nak/src/paths.rs6
-rw-r--r--libs/nak/src/runtime_wrap.rs36
-rw-r--r--libs/plugin_python/src/proxy/proxypython.cpp7
-rw-r--r--libs/plugin_python/src/runner/pythonrunner.cpp178
-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
62 files changed, 1108 insertions, 2411 deletions
diff --git a/.gitignore b/.gitignore
index da898a0..96bb944 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,12 +7,8 @@ build-container-2404-gcc14/
# Rust build artifacts
target/
-# Flatpak build cache and artifacts
-.flatpak-builder/
-.flatpak-build/
-flatpak-repo/
-flatpak/builddir/
-*.flatpak
+# AppImage build artifacts
+*.AppImage
# Logs
*.log
@@ -26,3 +22,16 @@ compile_commands.json
# OS junk
.DS_Store
Thumbs.db
+
+# AppImage extraction/build leftovers
+squashfs-root/
+build-source/
+flatpak-repo/
+*.flatpak
+
+# Python bytecode
+__pycache__/
+*.pyc
+
+# Generated settings files
+fomod-plus-settings.ini
diff --git a/build-flatpak.sh b/build-flatpak.sh
deleted file mode 100755
index 68e58a5..0000000
--- a/build-flatpak.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env bash
-# build-flatpak.sh — Build the Flatpak package.
-# Usage: ./build-flatpak.sh [install|bundle] (default: install)
-set -euo pipefail
-
-SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-MODE="${1:-install}"
-
-exec bash "${SCRIPT_DIR}/flatpak/flatpak-install.sh" "${MODE}"
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..766e8d7
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Build Fluorine Manager using Docker.
+#
+# Usage:
+# ./build.sh # Build AppImage + staging dir
+# ./build.sh shell # Drop into the build container for debugging
+#
+# Prerequisites: Docker or Podman
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+
+# Auto-detect container runtime
+if command -v podman >/dev/null 2>&1; then
+ DOCKER=podman
+elif command -v docker >/dev/null 2>&1; then
+ DOCKER=docker
+else
+ echo "ERROR: Neither podman nor docker found in PATH"
+ exit 1
+fi
+IMAGE_NAME="fluorine-builder"
+CONTAINER_NAME="fluorine-build-$$"
+
+cd "${SCRIPT_DIR}"
+
+# Build the Docker image if it doesn't exist or Dockerfile changed
+echo "=== Ensuring build image is up to date ==="
+${DOCKER} build -t "${IMAGE_NAME}" docker/
+
+if [ "${1:-}" = "shell" ]; then
+ echo "=== Dropping into build container shell ==="
+ exec ${DOCKER} run --rm -it \
+ -v "${SCRIPT_DIR}:/src:rw" \
+ -w /src \
+ --device /dev/fuse \
+ --cap-add SYS_ADMIN \
+ "${IMAGE_NAME}" \
+ bash
+fi
+
+echo "=== Starting build ==="
+${DOCKER} run --rm \
+ -v "${SCRIPT_DIR}:/src:rw" \
+ -w /src \
+ --device /dev/fuse \
+ --cap-add SYS_ADMIN \
+ --name "${CONTAINER_NAME}" \
+ "${IMAGE_NAME}" \
+ bash /src/docker/build-inner.sh
+
+echo ""
+echo "=== Done ==="
+if ls build/*.AppImage >/dev/null 2>&1; then
+ echo "AppImage: $(ls build/*.AppImage)"
+fi
+echo "Staging: build/staging/"
diff --git a/flatpak/com.fluorine.manager.desktop b/data/com.fluorine.manager.desktop
index ff62c76..ff62c76 100644
--- a/flatpak/com.fluorine.manager.desktop
+++ b/data/com.fluorine.manager.desktop
diff --git a/flatpak/com.fluorine.manager.metainfo.xml b/data/com.fluorine.manager.metainfo.xml
index 1200d3e..1200d3e 100644
--- a/flatpak/com.fluorine.manager.metainfo.xml
+++ b/data/com.fluorine.manager.metainfo.xml
diff --git a/flatpak/com.fluorine.manager.png b/data/com.fluorine.manager.png
index 3a9fbdd..3a9fbdd 100644
--- a/flatpak/com.fluorine.manager.png
+++ b/data/com.fluorine.manager.png
Binary files differ
diff --git a/docker/Dockerfile b/docker/Dockerfile
index aee1635..f4098af 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -26,6 +26,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
qt6-webengine-dev \
libqt6websockets6-dev \
qt6-wayland \
+ kde-style-breeze \
# Python
python3 python3-pyqt6 \
# Misc
diff --git a/docker/build-inner.sh b/docker/build-inner.sh
index 0483aa2..dda9b14 100755
--- a/docker/build-inner.sh
+++ b/docker/build-inner.sh
@@ -31,17 +31,10 @@ mkdir -p "${OUT_DIR}/plugins" "${OUT_DIR}/dlls" "${OUT_DIR}/lib"
# ── Main binary + helpers ──
cp -f "${RUNDIR}/ModOrganizer" "${OUT_DIR}/ModOrganizer-core"
if [ -f "${RUNDIR}/umu-run" ]; then
- # Patch umu-run to fix two issues with Steamworks DRM:
- #
- # 1. Preserve STEAM_COMPAT_CLIENT_INSTALL_PATH from the parent environment.
- # Upstream umu-run initialises this to "" and never picks up the caller's
- # value, which prevents the Steam client libraries from being found.
- #
- # 2. Remove UMU_ID from the environment before Proton runs. GE-Proton
- # treats games with UMU_ID as non-Steam titles and skips the steam.exe
- # DRM bridge, causing "Application load error 5:0000065434". For actual
- # Steam games (SteamAppId != "0") we delete UMU_ID so Proton uses the
- # correct steam.exe launch path.
+ # Patch umu-run to preserve STEAM_COMPAT_CLIENT_INSTALL_PATH from the
+ # parent environment. Upstream umu-run initialises this to "" and never
+ # picks up the caller's value, which prevents the Steam client libraries
+ # from being found.
UMU_PATCH_DIR="$(mktemp -d)"
(cd "${UMU_PATCH_DIR}" && "${BUILD_PY}" << PATCHEOF
import zipfile, pathlib
@@ -59,16 +52,6 @@ new1 = (old1
if old1 in src and 'STEAM_COMPAT_CLIENT_INSTALL_PATH"] = os.environ' not in src:
src = src.replace(old1, new1)
-# Patch 2: delete UMU_ID before Proton launch for Steam games
-old2 = ' os.environ[key] = val'
-new2 = (old2
- + '\n\n # GE-Proton treats games with UMU_ID as non-Steam titles and skips'
- + '\n # the steam.exe DRM bridge. Remove it for Steam games.'
- + '\n if "UMU_ID" in os.environ and env.get("SteamAppId", "0") != "0":'
- + '\n del os.environ["UMU_ID"]')
-if old2 in src and 'del os.environ["UMU_ID"]' not in src:
- src = src.replace(old2, new2, 1)
-
run_py.write_text(src)
PATCHEOF
)
@@ -91,6 +74,7 @@ done
find build/libs -type f \( \
-name "libgame_*.so" -o \
-name "libinstaller_*.so" -o \
+ -name "libfomod_plus_*.so" -o \
-name "libpreview_*.so" -o \
-name "libdiagnose_*.so" -o \
-name "libcheck_*.so" -o \
@@ -117,6 +101,12 @@ for f in /src/src/plugins/*.py; do
[ -f "${f}" ] && cp -f "${f}" "${OUT_DIR}/plugins/"
done
+# ── Stylesheets (themes) ──
+if [ -d "build/src/src/stylesheets" ]; then
+ cp -a "build/src/src/stylesheets" "${OUT_DIR}/"
+ echo "Bundled stylesheets"
+fi
+
# ── 7z runtime ──
SO7="build/src/src/dlls/7z.so"
if [ -f "${SO7}" ]; then
@@ -140,6 +130,80 @@ for boost_lib in /lib/x86_64-linux-gnu/libboost_program_options.so* \
[ -f "${boost_lib}" ] && cp -Lf "${boost_lib}" "${OUT_DIR}/lib/"
done
+# ── Bundle ALL shared library dependencies ──
+# Collect every .so the binary and plugins link against, then bundle
+# everything except core glibc/system libs (which must come from the host).
+echo "Bundling shared library dependencies..."
+
+# Libraries that MUST come from the host (glibc, GPU drivers, etc.)
+SKIP_PATTERN="linux-vdso|ld-linux|libc\.so|libm\.so|libdl\.so|librt\.so|libpthread|libresolv|libnss|libgcc_s|libstdc\+\+"
+# GPU/graphics drivers must be host-provided
+SKIP_PATTERN="${SKIP_PATTERN}|libGL\.so|libEGL|libGLX|libGLdispatch|libdrm|libvulkan|libX11|libxcb|libwayland-client|libwayland-server|libwayland-cursor|libwayland-egl|libxkbcommon"
+# libpython is shipped by the portable Python runtime in python/lib/ — do NOT
+# copy the container's version into lib/ or it will shadow the portable one.
+SKIP_PATTERN="${SKIP_PATTERN}|libpython"
+
+collect_deps() {
+ ldd "$1" 2>/dev/null | grep "=>" | awk '{print $3}' | grep "^/" | sort -u
+}
+
+ALL_DEPS=$(mktemp)
+# Main binary
+collect_deps "${OUT_DIR}/ModOrganizer-core" >> "${ALL_DEPS}"
+# All plugin .so files
+find "${OUT_DIR}/plugins" -name "*.so" -exec sh -c 'ldd "$1" 2>/dev/null | grep "=>" | awk "{print \$3}" | grep "^/"' _ {} \; >> "${ALL_DEPS}"
+# Our own libs
+find "${OUT_DIR}/lib" -name "*.so*" -exec sh -c 'ldd "$1" 2>/dev/null | grep "=>" | awk "{print \$3}" | grep "^/"' _ {} \; >> "${ALL_DEPS}"
+# lootcli
+[ -f "${OUT_DIR}/lootcli" ] && collect_deps "${OUT_DIR}/lootcli" >> "${ALL_DEPS}"
+
+sort -u "${ALL_DEPS}" | while read -r dep; do
+ dep_name="$(basename "${dep}")"
+ # Skip system libs
+ if echo "${dep_name}" | grep -qE "${SKIP_PATTERN}"; then
+ continue
+ fi
+ # Skip if already bundled
+ if [ -f "${OUT_DIR}/lib/${dep_name}" ]; then
+ continue
+ fi
+ cp -Lf "${dep}" "${OUT_DIR}/lib/" 2>/dev/null || true
+done
+rm -f "${ALL_DEPS}"
+# Remove any libpython that leaked into lib/ — must come from python/lib/ only.
+rm -f "${OUT_DIR}/lib"/libpython*.so* 2>/dev/null || true
+echo "Dependencies bundled."
+
+# ── Qt6 platform plugins ──
+QT6_PLUGIN_DIR="/usr/lib/x86_64-linux-gnu/qt6/plugins"
+if [ ! -d "${QT6_PLUGIN_DIR}" ]; then
+ QT6_PLUGIN_DIR="$(qtpaths6 --plugin-dir 2>/dev/null || echo "")"
+fi
+if [ -d "${QT6_PLUGIN_DIR}" ]; then
+ mkdir -p "${OUT_DIR}/qt6plugins"
+ for plugin_type in platforms tls networkinformation styles \
+ wayland-shell-integration \
+ wayland-decoration-client wayland-graphics-integration-client \
+ platformthemes imageformats iconengines xcbglintegrations \
+ egldeviceintegrations; do
+ if [ -d "${QT6_PLUGIN_DIR}/${plugin_type}" ]; then
+ cp -a "${QT6_PLUGIN_DIR}/${plugin_type}" "${OUT_DIR}/qt6plugins/"
+ fi
+ done
+ # Bundle deps of Qt plugins too
+ find "${OUT_DIR}/qt6plugins" -name "*.so" -exec sh -c '
+ ldd "$1" 2>/dev/null | grep "=>" | awk "{print \$3}" | grep "^/" | while read dep; do
+ dep_name="$(basename "${dep}")"
+ echo "${dep_name}" | grep -qE "'"${SKIP_PATTERN}"'" && continue
+ [ -f "'"${OUT_DIR}"'/lib/${dep_name}" ] && continue
+ cp -Lf "${dep}" "'"${OUT_DIR}"'/lib/" 2>/dev/null || true
+ done
+ ' _ {} \;
+ echo "Bundled Qt6 plugins from ${QT6_PLUGIN_DIR}"
+else
+ echo "WARNING: Could not find Qt6 plugin directory"
+fi
+
# libloot (custom-built, never on user systems).
if [ -f /usr/local/lib/libloot.so.0 ]; then
cp -Lf /usr/local/lib/libloot.so.0 "${OUT_DIR}/lib/"
@@ -242,41 +306,156 @@ export MO2_PYTHON_DIR="${HERE}/python"
MO2_PYTHONHOME="${HERE}/python"
unset PYTHONPATH PYTHONNOUSERSITE PYTHONHOME
-# Find system Qt6 plugins (compiled-in path won't match across distros).
-if [ -z "${QT_PLUGIN_PATH:-}" ]; then
- for qt_dir in /usr/lib/qt6/plugins \
- /usr/lib/x86_64-linux-gnu/qt6/plugins \
- /usr/lib64/qt6/plugins; do
- if [ -d "${qt_dir}/platforms" ]; then
- export QT_PLUGIN_PATH="${qt_dir}"
- break
- fi
- done
- if [ -z "${QT_PLUGIN_PATH:-}" ] && command -v qtpaths6 >/dev/null 2>&1; then
- export QT_PLUGIN_PATH="$(qtpaths6 --plugin-dir)"
- fi
-fi
-
-# Quick dependency check.
-missing="$(ldd "${HERE}/ModOrganizer-core" 2>/dev/null | grep "not found" || true)"
-if [ -n "${missing}" ]; then
- echo "ERROR: Missing system libraries:"
- echo "${missing}"
- echo ""
- echo "On Arch/CachyOS: sudo pacman -S qt6-base qt6-websockets qt6-wayland"
- echo "On Fedora: sudo dnf install qt6-qtbase qt6-qtwebsockets qt6-qtwayland"
- echo "On Ubuntu/Debian: sudo apt install qt6-base-dev libqt6websockets6-dev qt6-wayland"
- exit 1
-fi
+# Use bundled Qt6 plugins.
+export QT_PLUGIN_PATH="${HERE}/qt6plugins"
cd "${HERE}"
exec env PYTHONHOME="${MO2_PYTHONHOME}" "${HERE}/ModOrganizer-core" "$@"
LAUNCH
chmod +x "${OUT_DIR}/fluorine-manager"
-# ── Summary ──
+# ── Desktop integration files (for AppImage) ──
+cp -f /src/data/com.fluorine.manager.desktop "${OUT_DIR}/"
+cp -f /src/data/com.fluorine.manager.png "${OUT_DIR}/"
+cp -f /src/data/com.fluorine.manager.metainfo.xml "${OUT_DIR}/"
+
+# ── Build AppImage ──
+echo ""
+echo "=== Building AppImage ==="
+
+APPDIR="/src/build/AppDir"
+rm -rf "${APPDIR}"
+mkdir -p "${APPDIR}/usr/bin" "${APPDIR}/usr/lib" "${APPDIR}/usr/share/applications" \
+ "${APPDIR}/usr/plugins" \
+ "${APPDIR}/usr/share/icons/hicolor/256x256/apps" \
+ "${APPDIR}/usr/share/metainfo"
+
+# Copy staging into AppDir layout
+cp -a "${OUT_DIR}"/. "${APPDIR}/usr/bin/"
+# Move libs to standard location
+mv "${APPDIR}/usr/bin/lib"/* "${APPDIR}/usr/lib/" 2>/dev/null || true
+rmdir "${APPDIR}/usr/bin/lib" 2>/dev/null || true
+# Flatpak runtime exposed Qt plugins from a stable system location. Mirror that
+# layout in AppImage by placing bundled Qt plugins under /usr/plugins.
+if [ -d "${APPDIR}/usr/bin/qt6plugins" ]; then
+ cp -a "${APPDIR}/usr/bin/qt6plugins"/. "${APPDIR}/usr/plugins/"
+fi
+
+# Desktop integration
+cp -f "${OUT_DIR}/com.fluorine.manager.desktop" "${APPDIR}/usr/share/applications/"
+cp -f "${OUT_DIR}/com.fluorine.manager.png" "${APPDIR}/usr/share/icons/hicolor/256x256/apps/"
+cp -f "${OUT_DIR}/com.fluorine.manager.metainfo.xml" "${APPDIR}/usr/share/metainfo/"
+
+# Bundle icon themes so QIcon::fromTheme() calls resolve inside the AppImage.
+# Flatpak runtime provided this globally; AppImage must carry it explicitly.
+mkdir -p "${APPDIR}/usr/share/icons"
+for theme_dir in /usr/share/icons/*; do
+ [ -d "${theme_dir}" ] || continue
+ cp -a "${theme_dir}" "${APPDIR}/usr/share/icons/"
+done
+if [ -f "/usr/share/icons/default/index.theme" ]; then
+ mkdir -p "${APPDIR}/usr/share/icons/default"
+ cp -f "/usr/share/icons/default/index.theme" "${APPDIR}/usr/share/icons/default/"
+fi
+
+# Update RPATH for new lib location
+patchelf --set-rpath '$ORIGIN/../lib' "${APPDIR}/usr/bin/ModOrganizer-core"
+[ -f "${APPDIR}/usr/bin/lootcli" ] && patchelf --set-rpath '$ORIGIN/../lib' "${APPDIR}/usr/bin/lootcli"
+find "${APPDIR}/usr/bin/plugins" -name "*.so" -exec patchelf --set-rpath '$ORIGIN/../../lib' {} \; 2>/dev/null || true
+
+# Create AppRun wrapper
+cat > "${APPDIR}/AppRun" <<'APPRUN'
+#!/usr/bin/env bash
+set -euo pipefail
+SELF="$(readlink -f "$0")"
+HERE="$(cd "$(dirname "$SELF")" && pwd)"
+BIN="${HERE}/usr/bin"
+APPIMAGE_DIR="$(dirname "$(readlink -f "${APPIMAGE:-$0}")")"
+
+sync_dir() {
+ local name="$1"
+ local src="${BIN}/${name}"
+ local dst="${APPIMAGE_DIR}/${name}"
+ [ -d "${src}" ] || return 0
+ mkdir -p "${dst}"
+ (cd "${src}" && tar --exclude-vcs -cf - .) | (cd "${dst}" && tar -xf -)
+}
+
+# Keep runtime payload outside the transient AppImage mount.
+sync_dir "plugins"
+sync_dir "dlls"
+sync_dir "python"
+
+export PATH="${BIN}:${PATH}"
+if [ -d "${APPIMAGE_DIR}/python/lib" ]; then
+ export LD_LIBRARY_PATH="${HERE}/usr/lib:${APPIMAGE_DIR}/python/lib:${LD_LIBRARY_PATH:-}"
+else
+ export LD_LIBRARY_PATH="${HERE}/usr/lib:${BIN}/python/lib:${LD_LIBRARY_PATH:-}"
+fi
+
+export MO2_BASE_DIR="${APPIMAGE_DIR}"
+export MO2_PLUGINS_DIR="${APPIMAGE_DIR}/plugins"
+export MO2_DLLS_DIR="${APPIMAGE_DIR}/dlls"
+if [ -d "${APPIMAGE_DIR}/python/lib" ]; then
+ export MO2_PYTHON_DIR="${APPIMAGE_DIR}/python"
+else
+ export MO2_PYTHON_DIR="${BIN}/python"
+fi
+
+unset PYTHONPATH PYTHONNOUSERSITE PYTHONHOME
+
+# Use bundled Qt6 plugins.
+export QT_PLUGIN_PATH="${HERE}/usr/plugins"
+export QT_QPA_PLATFORM_PLUGIN_PATH="${HERE}/usr/plugins/platforms"
+export XDG_DATA_DIRS="${HERE}/usr/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
+export QT_ICON_THEME_NAME="${QT_ICON_THEME_NAME:-breeze}"
+export QT_STYLE_OVERRIDE="${QT_STYLE_OVERRIDE:-Breeze}"
+export XDG_CURRENT_DESKTOP="${XDG_CURRENT_DESKTOP:-KDE}"
+
+cd "${APPIMAGE_DIR}"
+exec "${BIN}/ModOrganizer-core" "$@"
+APPRUN
+chmod +x "${APPDIR}/AppRun"
+
+# Symlinks required by AppImage spec
+ln -sf usr/share/applications/com.fluorine.manager.desktop "${APPDIR}/com.fluorine.manager.desktop"
+ln -sf usr/share/icons/hicolor/256x256/apps/com.fluorine.manager.png "${APPDIR}/com.fluorine.manager.png"
+ln -sf usr/share/icons/hicolor/256x256/apps/com.fluorine.manager.png "${APPDIR}/.DirIcon"
+
+# Extract linuxdeploy (can't use FUSE inside Docker)
+DEPLOY_DIR="/tmp/linuxdeploy-extract"
+if [ ! -d "${DEPLOY_DIR}" ]; then
+ cd /opt/linuxdeploy
+ ./linuxdeploy-x86_64.AppImage --appimage-extract >/dev/null
+ mv squashfs-root "${DEPLOY_DIR}"
+ chmod +x "${DEPLOY_DIR}/AppRun"
+fi
+
+# Use linuxdeploy to generate the AppImage.
+# We skip the Qt plugin since we already handle Qt plugin paths at runtime
+# and our deps are already staged.
+export ARCH=x86_64
+"${DEPLOY_DIR}/AppRun" \
+ --appdir "${APPDIR}" \
+ --output appimage \
+ --desktop-file "${APPDIR}/usr/share/applications/com.fluorine.manager.desktop" \
+ --icon-file "${APPDIR}/usr/share/icons/hicolor/256x256/apps/com.fluorine.manager.png" \
+ 2>&1 || {
+ echo "WARNING: linuxdeploy AppImage generation failed, falling back to staging dir output"
+}
+
+# Move AppImage to output
+APPIMAGE_FILE=$(ls -1 Fluorine*.AppImage 2>/dev/null || ls -1 *.AppImage 2>/dev/null || true)
+if [ -n "${APPIMAGE_FILE}" ]; then
+ mv "${APPIMAGE_FILE}" "/src/build/"
+ echo ""
+ echo "=== AppImage built ==="
+ ls -lh "/src/build/"*.AppImage
+else
+ echo ""
+ echo "=== Staging complete (AppImage generation skipped) ==="
+fi
+
echo ""
echo "=== Build Summary ==="
du -sh "${OUT_DIR}"/*/ "${OUT_DIR}"/ModOrganizer-core 2>/dev/null | sort -rh
-echo ""
-echo "Staging complete."
diff --git a/flatpak/com.fluorine.manager.yml b/flatpak/com.fluorine.manager.yml
deleted file mode 100644
index 5ed65dc..0000000
--- a/flatpak/com.fluorine.manager.yml
+++ /dev/null
@@ -1,371 +0,0 @@
-app-id: com.fluorine.manager
-runtime: org.kde.Platform
-runtime-version: '6.10'
-sdk: org.kde.Sdk
-sdk-extensions:
- - org.freedesktop.Sdk.Extension.rust-stable
-command: fluorine-manager
-
-finish-args:
- - --share=ipc
- - --share=network
- - --socket=x11
- - --socket=wayland
- - --device=all
- - --filesystem=home
- - --filesystem=/mnt
- - --filesystem=/media
- - --filesystem=/run/media
- - --filesystem=~/.steam:ro
- - --filesystem=~/.local/share/Steam
- - --filesystem=~/.var/app/com.valvesoftware.Steam:ro
- - --talk-name=org.freedesktop.Flatpak
-
-cleanup:
- - /include
- - /lib/cmake
- - /lib/pkgconfig
- - /share/doc
- - /share/man
- - '*.a'
- - '*.la'
-
-modules:
- # ── 1. Boost ──
- - name: boost
- buildsystem: simple
- build-commands:
- - ./bootstrap.sh --prefix=/app --with-libraries=program_options,thread,filesystem,system,locale
- - ./b2 install -j$FLATPAK_BUILDER_N_JOBS link=shared variant=release
- sources:
- - type: archive
- url: https://archives.boost.io/release/1.87.0/source/boost_1_87_0.tar.gz
- sha256: f55c340aa49763b1925ccf02b2e83f35fdcf634c9d5164a2acb87540173c741d
-
- # ── 2. spdlog ──
- - name: spdlog
- buildsystem: cmake-ninja
- config-opts:
- - -DCMAKE_BUILD_TYPE=Release
- - -DSPDLOG_BUILD_SHARED=ON
- sources:
- - type: git
- url: https://github.com/gabime/spdlog.git
- tag: v1.15.2
-
- # ── 3. tinyxml2 ──
- - name: tinyxml2
- buildsystem: cmake-ninja
- config-opts:
- - -DCMAKE_BUILD_TYPE=Release
- sources:
- - type: git
- url: https://github.com/leethomason/tinyxml2.git
- tag: 10.0.0
-
- # ── 4. tomlplusplus ──
- - name: tomlplusplus
- buildsystem: cmake-ninja
- config-opts:
- - -DCMAKE_BUILD_TYPE=Release
- sources:
- - type: git
- url: https://github.com/marzer/tomlplusplus.git
- tag: v3.4.0
-
- # ── 5. libfuse3 ──
- # Built manually to install fusermount3 without setuid (chown root fails in sandbox).
- # default_library=both produces libfuse3.a so the VFS helper can link statically.
- - name: libfuse3
- buildsystem: simple
- build-commands:
- - meson setup _build --prefix=/app -Dexamples=false -Dtests=false -Dutils=true -Ddefault_library=both
- - ninja -C _build
- # Install library and headers (skip meson install which runs chown).
- - ninja -C _build install || true
- # Ensure fusermount3 is installed even if install_helper.sh failed.
- - install -Dm755 _build/util/fusermount3 /app/bin/fusermount3 2>/dev/null || true
- sources:
- - type: git
- url: https://github.com/libfuse/libfuse.git
- tag: fuse-3.16.2
-
- # ── 6. libloot ──
- # libloot has a Rust crate at the root and a C++ cmake project in cpp/.
- # The Rust static library must be built first, then cmake links against it.
- - name: libloot
- buildsystem: simple
- build-options:
- append-path: /usr/lib/sdk/rust-stable/bin
- env:
- CARGO_HOME: /run/build/libloot/cargo
- # Network needed for cargo deps and cmake FetchContent (googletest).
- build-args:
- - --share=network
- build-commands:
- # Build Rust static library first.
- - cargo build --release
- # Build the C++ shared library (links against Rust static lib).
- - |
- cmake -S cpp -B cpp/build -G Ninja \
- -DCMAKE_BUILD_TYPE=Release \
- -DBUILD_SHARED_LIBS=ON \
- -DLIBLOOT_INSTALL_DOCS=OFF \
- -DCMAKE_INSTALL_PREFIX=/app
- - cmake --build cpp/build --parallel
- - cmake --install cpp/build
- # Create pkg-config metadata.
- - mkdir -p /app/lib/pkgconfig
- - |
- printf '%s\n' \
- 'prefix=/app' \
- 'exec_prefix=${prefix}' \
- 'libdir=${prefix}/lib' \
- 'includedir=${prefix}/include' \
- '' \
- 'Name: libloot' \
- 'Description: LOOT C++ API library' \
- 'Version: 0.29.0' \
- 'Libs: -L${libdir} -lloot' \
- 'Cflags: -I${includedir}' \
- > /app/lib/pkgconfig/libloot.pc
- sources:
- - type: git
- url: https://github.com/loot/libloot.git
- branch: master
-
- # ── 7. icoutils ──
- - name: icoutils
- buildsystem: autotools
- build-options:
- # icoutils 0.32.3 uses K&R-style () declarations that GCC 14+ rejects.
- cflags: -std=gnu17
- sources:
- - type: archive
- url: https://download.savannah.gnu.org/releases/icoutils/icoutils-0.32.3.tar.bz2
- sha256: 17abe02d043a253b68b47e3af69c9fc755b895db68fdc8811786125df564c6e0
-
- # ── 8. cabextract ──
- # Required by winetricks for extracting Windows cab archives.
- - name: cabextract
- buildsystem: autotools
- sources:
- - type: archive
- url: https://www.cabextract.org.uk/cabextract-1.11.tar.gz
- sha256: b5546db1155e4c718ff3d4b278573604f30dd64c3c5bfd4657cd089b823a3ac6
-
- # ── 9. Portable Python runtime ──
- # Pre-built Python used as the embedded interpreter for MO2's Python plugins.
- # pip packages are installed via network during build (--share=network).
- - name: portable-python
- buildsystem: simple
- build-options:
- build-args:
- - --share=network
- build-commands:
- # Extract, trim, symlink, and install pip packages in one step
- # (each build-command runs in its own shell).
- - |
- set -e
- mkdir -p /app/lib/fluorine/python
- unzip portable-python.zip -d /tmp/pp
- PP_DIR="$(find /tmp/pp -maxdepth 1 -mindepth 1 -type d | head -1)"
- cp -a "${PP_DIR}/." /app/lib/fluorine/python/
- rm -rf /tmp/pp
- # Versioned soname symlink (pybind11 links against libpython3.13.so.1.0).
- if [ -f /app/lib/fluorine/python/lib/libpython3.13.so ] && \
- [ ! -f /app/lib/fluorine/python/lib/libpython3.13.so.1.0 ]; then
- ln -sf libpython3.13.so /app/lib/fluorine/python/lib/libpython3.13.so.1.0
- fi
- # Trim test suites and unnecessary files.
- rm -rf /app/lib/fluorine/python/lib/python3.13/test \
- /app/lib/fluorine/python/lib/python3.13/unittest/test \
- /app/lib/fluorine/python/lib/python3.13/idlelib \
- /app/lib/fluorine/python/lib/python3.13/tkinter \
- /app/lib/fluorine/python/lib/python3.13/turtledemo \
- /app/lib/fluorine/python/include \
- /app/lib/fluorine/python/share \
- 2>/dev/null || true
- find /app/lib/fluorine/python -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
- find /app/lib/fluorine/python -name "*.pyc" -delete 2>/dev/null || true
- # Install uv for Python package management.
- curl -LsSf https://astral.sh/uv/install.sh | INSTALLER_NO_MODIFY_PATH=1 UV_INSTALL_DIR=/app/bin sh
- # Install Python packages via uv (PyQt6 for plugin UI, psutil, vdf).
- /app/bin/uv pip install --python /app/lib/fluorine/python/bin/python3 PyQt6 psutil vdf
- sources:
- - type: file
- url: https://github.com/bjia56/portable-python/releases/download/cpython-v3.13.9-build.0/python-headless-3.13.9-linux-x86_64.zip
- sha256: d03507d107da86d74aa38bbc4957b8db5753de567024c5724a254909ae6b86d6
- dest-filename: portable-python.zip
-
- # ── 10. Fluorine Manager (main project) ──
- # uv (installed in portable-python step) handles pybind11 + sip for the build.
- - name: fluorine
- buildsystem: simple
- build-options:
- append-path: /usr/lib/sdk/rust-stable/bin
- env:
- CARGO_HOME: /run/build/fluorine/cargo
- PYTHONPATH: /app/lib/python3/dist-packages
- # Network needed for FetchContent (7zip, etc.) and cargo deps.
- build-args:
- - --share=network
- build-commands:
- # ── Configure and build ──
- # Install pybind11 + sip into /app (SDK /usr is read-only).
- # Uses SDK's python3 directly (not a venv) so find_package(Python)
- # finds Development headers on cmake reconfiguration.
- - |
- /app/bin/uv pip install --target /app/lib/python3/dist-packages pybind11==2.13.6 sip
- PYBIND11_DIR="$(python3 -c 'import pybind11; print(pybind11.get_cmake_dir())')"
- cmake -S . -B _build -G Ninja \
- -DCMAKE_BUILD_TYPE=Release \
- -DCMAKE_PREFIX_PATH=/app \
- -DPython_EXECUTABLE=/usr/bin/python3 \
- -Dpybind11_DIR="${PYBIND11_DIR}" \
- -DBUILD_PLUGIN_PYTHON=ON
- - cmake --build _build --parallel
-
- # ── Create output layout ──
- - mkdir -p /app/lib/fluorine/plugins /app/lib/fluorine/dlls /app/lib/fluorine/lib
-
- # Main binary.
- - cp -f _build/src/src/ModOrganizer /app/lib/fluorine/ModOrganizer-core
-
- # umu-run (patch and repackage if present).
- - |
- if [ -f _build/src/src/umu-run ]; then
- UMU_PATCH_DIR="$(mktemp -d)"
- cd "${UMU_PATCH_DIR}"
- python3 << 'PATCHEOF'
- import zipfile, pathlib
- zf = zipfile.ZipFile('/run/build/fluorine/_build/src/src/umu-run')
- zf.extractall('src')
- run_py = pathlib.Path('src/umu/umu_run.py')
- src = run_py.read_text()
- old1 = ' env["STEAM_COMPAT_INSTALL_PATH"] = os.environ.get("STEAM_COMPAT_INSTALL_PATH", "")'
- new1 = (old1
- + '\n env["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = os.environ.get('
- + '\n "STEAM_COMPAT_CLIENT_INSTALL_PATH", ""'
- + '\n )')
- if old1 in src and 'STEAM_COMPAT_CLIENT_INSTALL_PATH"] = os.environ' not in src:
- src = src.replace(old1, new1)
- old2 = ' os.environ[key] = val'
- new2 = (old2
- + '\n\n # GE-Proton treats games with UMU_ID as non-Steam titles and skips'
- + '\n # the steam.exe DRM bridge. Remove it for Steam games.'
- + '\n if "UMU_ID" in os.environ and env.get("SteamAppId", "0") != "0":'
- + '\n del os.environ["UMU_ID"]')
- if old2 in src and 'del os.environ["UMU_ID"]' not in src:
- src = src.replace(old2, new2, 1)
- run_py.write_text(src)
- PATCHEOF
- python3 -m zipapp src -o /app/lib/fluorine/umu-run -p '/usr/bin/env python3'
- chmod +x /app/lib/fluorine/umu-run
- cd /run/build/fluorine
- rm -rf "${UMU_PATCH_DIR}"
- fi
-
- # VFS helper (standalone, runs on host for Flatpak FUSE support).
- # Statically linked against libfuse3 — no runtime deps beyond glibc.
- - test -f _build/src/src/mo2-vfs-helper && cp -f _build/src/src/mo2-vfs-helper /app/lib/fluorine/ || true
- - test -f _build/src/src/mo2-process-helper && cp -f _build/src/src/mo2-process-helper /app/lib/fluorine/ || true
-
- # lootcli.
- - test -f _build/libs/lootcli/src/lootcli && cp -f _build/libs/lootcli/src/lootcli /app/lib/fluorine/ || true
-
- # icoutils (symlink from /app/bin where the icoutils module installed them).
- - ln -sf /app/bin/wrestool /app/lib/fluorine/wrestool
- - ln -sf /app/bin/icotool /app/lib/fluorine/icotool
- - ln -sf /app/bin/fusermount3 /app/lib/fluorine/fusermount3
- - ln -sf /app/bin/cabextract /app/lib/fluorine/cabextract
-
- # fluorine-manager CLI helper.
- - test -f src/fluorine-manager && cp -f src/fluorine-manager /app/lib/fluorine/ || true
-
- # MO2 plugins (.so).
- - |
- find _build/libs -type f \( \
- -name "libgame_*.so" -o \
- -name "libinstaller_*.so" -o \
- -name "libfomod_plus_*.so" -o \
- -name "libpreview_*.so" -o \
- -name "libdiagnose_*.so" -o \
- -name "libcheck_*.so" -o \
- -name "libtool_*.so" -o \
- -name "libinieditor.so" -o \
- -name "libinibakery.so" -o \
- -name "libbsa_extractor.so" -o \
- -name "libbsa_packer.so" -o \
- -name "libproxy.so" \
- \) -exec cp -f {} /app/lib/fluorine/plugins/ \;
-
- # Python plugin payload.
- - |
- for f in libplugin_python.so lzokay.py winreg.py \
- DDSPreview.py Form43Checker.py ScriptExtenderPluginChecker.py; do
- [ -f "_build/src/src/plugins/${f}" ] && cp -f "_build/src/src/plugins/${f}" /app/lib/fluorine/plugins/
- done
- for d in basic_games data libs dlls; do
- [ -d "_build/src/src/plugins/${d}" ] && cp -a "_build/src/src/plugins/${d}" /app/lib/fluorine/plugins/
- done
- rm -f /app/lib/fluorine/plugins/FNIS*.py
-
- # Source-tree Python plugins (rootbuilder, omod installer, etc.).
- - |
- for f in src/plugins/*.py; do
- [ -f "${f}" ] && cp -f "${f}" /app/lib/fluorine/plugins/
- done
-
- # Stylesheets / themes.
- - cp -a src/src/stylesheets /app/lib/fluorine/stylesheets
-
- # 7z runtime.
- - |
- if [ -f _build/src/src/dlls/7z.so ]; then
- cp -f _build/src/src/dlls/7z.so /app/lib/fluorine/dlls/7z.so
- cp -f _build/src/src/dlls/7z.so /app/lib/fluorine/dlls/7zip.dll
- fi
-
- # Project shared libraries.
- - cp -f _build/libs/uibase/src/libuibase.so /app/lib/fluorine/lib/
- - cp -f _build/libs/libbsarch/liblibbsarch.so /app/lib/fluorine/lib/
- - cp -f _build/libs/archive/src/libarchive.so /app/lib/fluorine/lib/
- - test -f _build/libs/plugin_python/src/runner/librunner.so && cp -f _build/libs/plugin_python/src/runner/librunner.so /app/lib/fluorine/lib/ || true
- - test -f libs/bsa_ffi/target/release/libbsa_ffi.so && cp -f libs/bsa_ffi/target/release/libbsa_ffi.so /app/lib/fluorine/lib/ || true
- - test -f libs/nak_ffi/target/release/libnak_ffi.so && cp -f libs/nak_ffi/target/release/libnak_ffi.so /app/lib/fluorine/lib/ || true
-
- # Boost libraries (from /app/lib where the boost module installed them).
- - cp -Lf /app/lib/libboost_program_options.so* /app/lib/fluorine/lib/ 2>/dev/null || true
- - cp -Lf /app/lib/libboost_thread.so* /app/lib/fluorine/lib/ 2>/dev/null || true
-
- # libloot (from /app/lib where the libloot module installed it).
- - |
- if [ -f /app/lib/libloot.so.0 ]; then
- cp -Lf /app/lib/libloot.so.0 /app/lib/fluorine/lib/
- ln -sf libloot.so.0 /app/lib/fluorine/lib/libloot.so
- elif [ -f /app/lib/libloot.so ]; then
- cp -Lf /app/lib/libloot.so /app/lib/fluorine/lib/
- fi
-
- # Build-tree Python plugin payload.
- - test -d _build/src/src/python && cp -a _build/src/src/python/. /app/lib/fluorine/python/ || true
-
- # Strip binaries.
- - strip --strip-unneeded /app/lib/fluorine/ModOrganizer-core 2>/dev/null || true
- - find /app/lib/fluorine/plugins -name "*.so" -exec strip --strip-unneeded {} \; 2>/dev/null || true
- - find /app/lib/fluorine/dlls \( -name "*.so" -o -name "*.dll" \) -exec strip --strip-unneeded {} \; 2>/dev/null || true
- - find /app/lib/fluorine/lib -name "*.so*" -exec strip --strip-unneeded {} \; 2>/dev/null || true
- - test -f /app/lib/fluorine/lootcli && strip --strip-unneeded /app/lib/fluorine/lootcli 2>/dev/null || true
- - test -f /app/lib/fluorine/mo2-vfs-helper && strip --strip-unneeded /app/lib/fluorine/mo2-vfs-helper 2>/dev/null || true
- - test -f /app/lib/fluorine/mo2-process-helper && strip --strip-unneeded /app/lib/fluorine/mo2-process-helper 2>/dev/null || true
-
- # Install wrapper script, desktop file, metainfo, and icon.
- - install -Dm755 flatpak/fluorine-manager-wrapper.sh /app/bin/fluorine-manager
- - install -Dm644 flatpak/com.fluorine.manager.desktop /app/share/applications/com.fluorine.manager.desktop
- - install -Dm644 flatpak/com.fluorine.manager.metainfo.xml /app/share/metainfo/com.fluorine.manager.metainfo.xml
- - install -Dm644 flatpak/com.fluorine.manager.png /app/share/icons/hicolor/256x256/apps/com.fluorine.manager.png
-
- sources:
- - type: dir
- path: ..
diff --git a/flatpak/flatpak-install.sh b/flatpak/flatpak-install.sh
deleted file mode 100755
index 984d12b..0000000
--- a/flatpak/flatpak-install.sh
+++ /dev/null
@@ -1,60 +0,0 @@
-#!/usr/bin/env bash
-# Build the Fluorine Manager Flatpak.
-#
-# Usage:
-# cd flatpak && bash flatpak-install.sh # build & install locally
-# cd flatpak && bash flatpak-install.sh bundle # build a .flatpak file to share
-set -euo pipefail
-
-SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-PROJECT_DIR="$(dirname "${SCRIPT_DIR}")"
-MANIFEST="${SCRIPT_DIR}/com.fluorine.manager.yml"
-APP_ID="com.fluorine.manager"
-BUILD_DIR="${PROJECT_DIR}/.flatpak-build"
-MODE="${1:-install}"
-
-# ── Ensure flathub remote exists ──
-if ! flatpak remote-list --user | grep -q flathub; then
- echo "Adding flathub remote..."
- flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
-fi
-
-# ── Install required Flatpak runtime and SDK ──
-echo "Installing KDE Platform and SDK (may take a while on first run)..."
-flatpak install --user --noninteractive flathub org.kde.Platform//6.10 org.kde.Sdk//6.10
-
-echo "Installing Rust SDK extension..."
-flatpak install --user --noninteractive flathub org.freedesktop.Sdk.Extension.rust-stable//25.08
-
-cd "${PROJECT_DIR}"
-
-if [ "${MODE}" = "bundle" ]; then
- # ── Build a distributable .flatpak file ──
- echo ""
- echo "Building Flatpak bundle (this may take a while)..."
- flatpak-builder --disable-rofiles-fuse --repo="${PROJECT_DIR}/flatpak-repo" --force-clean --ccache \
- "${BUILD_DIR}" "${MANIFEST}"
- flatpak build-bundle \
- --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo \
- "${PROJECT_DIR}/flatpak-repo" "${PROJECT_DIR}/fluorine-manager.flatpak" "${APP_ID}"
- BUNDLE_SIZE="$(du -sh fluorine-manager.flatpak | cut -f1)"
- echo ""
- echo "=== Bundle created: fluorine-manager.flatpak (${BUNDLE_SIZE}) ==="
- echo ""
- echo "Share this file with testers. They install it with:"
- echo " flatpak install --user fluorine-manager.flatpak"
-else
- # ── Build and install locally ──
- echo ""
- echo "Building and installing Flatpak locally (this may take a while)..."
- flatpak-builder --disable-rofiles-fuse --install --user --force-clean --ccache \
- "${BUILD_DIR}" "${MANIFEST}"
- echo ""
- echo "=== Flatpak installed successfully ==="
-fi
-
-echo ""
-echo "Usage:"
-echo " flatpak run ${APP_ID} # launch GUI"
-echo " flatpak run ${APP_ID} create-portable --name myinstance --game falloutnv"
-echo " flatpak run ${APP_ID} list-instances"
diff --git a/flatpak/fluorine-manager-wrapper.sh b/flatpak/fluorine-manager-wrapper.sh
deleted file mode 100755
index eba2b77..0000000
--- a/flatpak/fluorine-manager-wrapper.sh
+++ /dev/null
@@ -1,114 +0,0 @@
-#!/bin/bash
-# Flatpak wrapper for Fluorine Manager (MO2 Linux)
-# Sets up a writable overlay so users can add custom plugins while still
-# loading the bundled ones from the read-only /app tree.
-#
-# For portable instances (--instance /path), the overlay is created INSIDE the
-# instance directory so each portable install is fully self-contained.
-# For the global instance (no --instance), a shared overlay is used.
-
-BUNDLED="/app/lib/fluorine"
-
-# ── Detect --instance argument ──
-# If launching a portable instance, use its directory as the overlay target.
-INSTANCE_DIR=""
-PREV=""
-for arg in "$@"; do
- if [ "$PREV" = "--instance" ] || [ "$PREV" = "-i" ]; then
- INSTANCE_DIR="$arg"
- break
- fi
- PREV="$arg"
-done
-
-if [ -n "$INSTANCE_DIR" ] && [ -d "$INSTANCE_DIR" ]; then
- # Portable instance: overlay goes into the instance directory itself.
- # Everything is self-contained: mods, profiles, plugins, dlls, libs.
- USER_DIR="$INSTANCE_DIR"
-else
- # Global instance: shared overlay at ~/.local/share/fluorine/
- # Use $HOME directly to bypass Flatpak's XDG_DATA_HOME remapping.
- USER_DIR="$HOME/.var/app/com.fluorine.manager"
-fi
-
-# ── Create writable overlay with symlinks to bundled files ──
-# This lets MO2 load bundled plugins AND any custom ones the user drops in.
-# Existing files are never overwritten (user overrides take priority).
-setup_overlay() {
- mkdir -p "${USER_DIR}/plugins" "${USER_DIR}/dlls" "${USER_DIR}/lib"
-
- # Symlink bundled plugins (skip existing - user overrides take priority)
- for f in "${BUNDLED}/plugins/"*; do
- [ -e "$f" ] || continue
- local base="$(basename "$f")"
- local target="${USER_DIR}/plugins/${base}"
- [ -e "$target" ] || [ -L "$target" ] || ln -sf "$f" "$target"
- done
-
- # Symlink bundled dlls
- for f in "${BUNDLED}/dlls/"*; do
- [ -e "$f" ] || continue
- local base="$(basename "$f")"
- local target="${USER_DIR}/dlls/${base}"
- [ -e "$target" ] || [ -L "$target" ] || ln -sf "$f" "$target"
- done
-
- # Symlink bundled libs
- for f in "${BUNDLED}/lib/"*; do
- [ -e "$f" ] || continue
- local base="$(basename "$f")"
- local target="${USER_DIR}/lib/${base}"
- [ -e "$target" ] || [ -L "$target" ] || ln -sf "$f" "$target"
- done
-
- # Symlink other bundled files (binaries, tools) directly
- for f in ModOrganizer-core lootcli wrestool icotool fusermount3 cabextract; do
- [ -e "${BUNDLED}/$f" ] || continue
- [ -e "${USER_DIR}/$f" ] || [ -L "${USER_DIR}/$f" ] || ln -sf "${BUNDLED}/$f" "${USER_DIR}/$f"
- done
-
- # umu-run must be a real copy (not symlink to /app/) because it runs on
- # the host via flatpak-spawn --host, where /app/ doesn't exist.
- # Remove any stale symlink first (cp -f follows symlinks, won't replace them).
- if [ -e "${BUNDLED}/umu-run" ]; then
- rm -f "${USER_DIR}/umu-run"
- cp "${BUNDLED}/umu-run" "${USER_DIR}/umu-run"
- chmod +x "${USER_DIR}/umu-run"
- fi
-
- # VFS helper must be a real binary copy (not a symlink to /app/) because
- # it runs on the host via flatpak-spawn --host, where /app/ doesn't exist.
- # libfuse3 is statically linked, so no extra .so files needed.
- VFS_HELPER_DIR="$HOME/.var/app/com.fluorine.manager/bin"
- mkdir -p "${VFS_HELPER_DIR}"
- if [ -e "${BUNDLED}/mo2-vfs-helper" ]; then
- cp -f "${BUNDLED}/mo2-vfs-helper" "${VFS_HELPER_DIR}/mo2-vfs-helper"
- chmod +x "${VFS_HELPER_DIR}/mo2-vfs-helper"
- fi
- if [ -e "${BUNDLED}/mo2-process-helper" ]; then
- cp -f "${BUNDLED}/mo2-process-helper" "${VFS_HELPER_DIR}/mo2-process-helper"
- chmod +x "${VFS_HELPER_DIR}/mo2-process-helper"
- fi
-}
-
-setup_overlay
-
-export PATH="${USER_DIR}:${BUNDLED}:${PATH}"
-export LD_LIBRARY_PATH="${USER_DIR}/lib:${BUNDLED}/lib:${BUNDLED}/python/lib:${LD_LIBRARY_PATH:-}"
-
-# MO2 resolves plugins/dlls relative to MO2_BASE_DIR (or basePath()).
-# Point it at the overlay so custom plugins are found.
-export MO2_BASE_DIR="${USER_DIR}"
-export MO2_PLUGINS_DIR="${USER_DIR}/plugins"
-export MO2_DLLS_DIR="${USER_DIR}/dlls"
-export MO2_PYTHON_DIR="${BUNDLED}/python"
-
-# Do NOT set PYTHONHOME globally -- it leaks into child processes (umu-run,
-# Proton, winetricks) and breaks their Python. The plugin_python runner reads
-# MO2_PYTHON_DIR and sets PYTHONHOME internally before loading the interpreter.
-unset PYTHONHOME PYTHONPATH PYTHONNOUSERSITE
-
-# Qt6 plugins from KDE runtime.
-export QT_PLUGIN_PATH="/usr/lib/plugins"
-
-exec "${BUNDLED}/ModOrganizer-core" "$@"
diff --git a/libs/basic_games/games/baldursgate3/pak_parser.py b/libs/basic_games/games/baldursgate3/pak_parser.py
index 4ebc83c..533c72e 100644
--- a/libs/basic_games/games/baldursgate3/pak_parser.py
+++ b/libs/basic_games/games/baldursgate3/pak_parser.py
@@ -281,21 +281,12 @@ class BG3PakParser:
except Exception:
pass
- if os.path.exists("/.flatpak-info"):
- # In Flatpak, host wine binaries can't execute directly
- # (missing host libraries). Use flatpak-spawn to run on host.
- spawn_cmd = ["flatpak-spawn", "--host"]
- if wine_prefix:
- spawn_cmd.append(f"--env=WINEPREFIX={wine_prefix}")
- spawn_cmd.extend(["bash", "-c", command])
- result = subprocess.run(spawn_cmd, **kwargs)
- else:
- env = os.environ.copy()
- if wine_prefix:
- env["WINEPREFIX"] = wine_prefix
- kwargs["env"] = env
- kwargs["shell"] = True
- result = subprocess.run(command, **kwargs)
+ env = os.environ.copy()
+ if wine_prefix:
+ env["WINEPREFIX"] = wine_prefix
+ kwargs["env"] = env
+ kwargs["shell"] = True
+ result = subprocess.run(command, **kwargs)
if result.returncode:
qWarning(
diff --git a/libs/basic_games/games/game_monsterhunterwilds.py b/libs/basic_games/games/game_monsterhunterwilds.py
new file mode 100644
index 0000000..419377c
--- /dev/null
+++ b/libs/basic_games/games/game_monsterhunterwilds.py
@@ -0,0 +1,15 @@
+from ..basic_game import BasicGame
+
+
+class MonsterHunterWildsGame(BasicGame):
+ Name = "Monster Hunter: Wilds Support Plugin"
+ Author = "AbyssDragnonModding"
+ Version = "1.0.0"
+
+ GameName = "Monster Hunter: Wilds"
+ GameShortName = "monsterhunterwilds"
+ GameBinary = "MonsterHunterWilds.exe"
+ GameDataPath = "%GAME_PATH%"
+ GameSaveExtension = "bin"
+ GameNexusId = 6993
+ GameSteamId = 2246340
diff --git a/libs/game_bethesda/src/*.cpp b/libs/game_bethesda/src/*.cpp
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/libs/game_bethesda/src/*.cpp
diff --git a/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp b/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp
index e4df0ea..4c1199c 100644
--- a/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp
+++ b/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp
@@ -56,7 +56,9 @@ QString GamebryoLocalSavegames::localSavesDummy() const
QDir GamebryoLocalSavegames::localSavesDirectory() const
{
- return QDir(m_Game->myGamesPath()).absoluteFilePath(localSavesDummy());
+ QString dummy = localSavesDummy();
+ dummy.replace("\\", "/");
+ return QDir(m_Game->myGamesPath()).absoluteFilePath(dummy);
}
QDir GamebryoLocalSavegames::localGameDirectory() const
diff --git a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
index b53d823..4a92440 100644
--- a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
+++ b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
@@ -494,11 +494,7 @@ QString GameGamebryo::myGamesPath() const
return findInRegistry(HKEY_LOCAL_MACHINE, L"Software\\LOOT", L"Installed Path") +
"/Loot.exe";
#else
- // On Linux, look for loot in common locations
- QString flatpakPath = QDir::homePath() + "/.var/app/io.github.loot.loot/data/LOOT/Loot";
- if (QFileInfo(flatpakPath).exists())
- return flatpakPath;
- // Try system PATH
+ // On Linux, look for loot in PATH
QString systemLoot = QStandardPaths::findExecutable("loot");
if (!systemLoot.isEmpty())
return systemLoot;
diff --git a/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.cpp b/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.cpp
index cf78eba..d0cc2ef 100644
--- a/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.cpp
+++ b/libs/game_gamebryo/src/gamebryo/gamebryolocalsavegames.cpp
@@ -44,7 +44,9 @@ QString GamebryoLocalSavegames::localSavesDummy() const
QDir GamebryoLocalSavegames::localSavesDirectory() const
{
- return QDir(m_Game->myGamesPath()).absoluteFilePath(localSavesDummy());
+ QString dummy = localSavesDummy();
+ dummy.replace("\\", "/");
+ return QDir(m_Game->myGamesPath()).absoluteFilePath(dummy);
}
QDir GamebryoLocalSavegames::localGameDirectory() const
diff --git a/libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp b/libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp
index 690bdbb..c748495 100644
--- a/libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp
+++ b/libs/installer_fomod_plus/installer/FomodPlusInstaller.cpp
@@ -34,8 +34,6 @@ bool FomodPlusInstaller::init(IOrganizer* organizer)
mOrganizer = organizer;
mFomodContent = make_shared<FomodDataContent>(organizer);
log.setLogFilePath(QDir::currentPath().toStdString() + "/logs/fomodplus.log");
- std::cout << "QDir::currentPath(): " << QDir::currentPath().toStdString() << std::endl;
- std::cout << "mOrganizer->basePath() : " << mOrganizer->basePath().toStdString() << std::endl;
// REMEMBER: This mFomodDB persists beyond the scope of an individual install. Do not do anything nasty to it.
mFomodDb = std::make_unique<FomodDB>(mOrganizer->basePath().toStdString());
diff --git a/libs/installer_fomod_plus/installer/lib/Logger.h b/libs/installer_fomod_plus/installer/lib/Logger.h
index fdeb1f0..53fb7d9 100644
--- a/libs/installer_fomod_plus/installer/lib/Logger.h
+++ b/libs/installer_fomod_plus/installer/lib/Logger.h
@@ -90,8 +90,6 @@ public:
if (mLogFile.is_open()) {
writeLog(mLogFile);
}
-
- writeLog(std::cout);
}
Logger& operator=(const Logger&) = delete;
diff --git a/libs/installer_fomod_plus/installer/ui/UIHelper.cpp b/libs/installer_fomod_plus/installer/ui/UIHelper.cpp
index 56aff85..5f34e5d 100644
--- a/libs/installer_fomod_plus/installer/ui/UIHelper.cpp
+++ b/libs/installer_fomod_plus/installer/ui/UIHelper.cpp
@@ -27,7 +27,6 @@ bool CtrlClickEventFilter::eventFilter(QObject* obj, QEvent* event)
if (mouseEvent->button() == Qt::LeftButton &&
mouseEvent->modifiers() & Qt::ControlModifier) {
// TODO: Add Ctrl+click handler logic here
- std::cout << "Ctrl+click detected on plugin: " << mPlugin->getName() << " in group: " << mGroup->getName() << std::endl;
// For now, just fall through to default behavior
}
}
diff --git a/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.cpp b/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.cpp
index cd06b3a..855fe2b 100644
--- a/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.cpp
+++ b/libs/installer_fomod_plus/patchwizard/lib/PatchFinder.cpp
@@ -77,7 +77,6 @@ void PatchFinder::populateInstalledPlugins()
const auto mod_tree = mod->fileTree();
for (auto it = mod_tree->begin(); it != mod_tree->end(); ++it) {
if ((*it)->isFile() && isPluginFile((*it)->name())) {
- std::cout << "Plugin: " << (*it)->name().toStdString() << std::endl;
m_installedPlugins[mod].emplace_back((*it)->name().toStdString());
m_installedPluginsCacheSet.insert((*it)->name().toStdString());
}
diff --git a/libs/installer_fomod_plus/scanner/archiveparser.h b/libs/installer_fomod_plus/scanner/archiveparser.h
index 4058d43..8819a84 100644
--- a/libs/installer_fomod_plus/scanner/archiveparser.h
+++ b/libs/installer_fomod_plus/scanner/archiveparser.h
@@ -83,7 +83,6 @@ public:
}
if (hasFomodFiles(archive->getFileList())) {
- std::cout << "Found FOMOD files in " << qualifiedInstallerPath.toStdString() << std::endl;
return ScanResult::HAS_FOMOD;
}
return ScanResult::NO_FOMOD;
diff --git a/libs/installer_fomod_plus/share/FOMODData/FomodDB.h b/libs/installer_fomod_plus/share/FOMODData/FomodDB.h
index d1a9aa2..f5ea6f5 100644
--- a/libs/installer_fomod_plus/share/FOMODData/FomodDB.h
+++ b/libs/installer_fomod_plus/share/FOMODData/FomodDB.h
@@ -33,8 +33,6 @@ public:
for (const auto &group: installStep.optionalFileGroups.groups) {
for (const auto &plugin: group.plugins.plugins) {
// Create a DB entry for the given plugin if it has an ESP
- std::cout << "\nPlugin: " << plugin.name << std::endl;
-
for (auto file: plugin.files.files) {
if (file.isFolder || !isPluginFile(file.source)) {
continue;
diff --git a/libs/nak/src/paths.rs b/libs/nak/src/paths.rs
index e86c284..f6dde6c 100644
--- a/libs/nak/src/paths.rs
+++ b/libs/nak/src/paths.rs
@@ -1,11 +1,11 @@
//! Shared data directory for Fluorine Manager.
//!
-//! All data lives under `~/.var/app/com.fluorine.manager/`.
+//! All data lives under `~/.local/share/fluorine/`.
use std::path::PathBuf;
-/// Returns the Fluorine data directory (`~/.var/app/com.fluorine.manager`).
+/// Returns the Fluorine data directory (`~/.local/share/fluorine`).
pub fn data_dir() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
- PathBuf::from(home).join(".var/app/com.fluorine.manager")
+ PathBuf::from(home).join(".local/share/fluorine")
}
diff --git a/libs/nak/src/runtime_wrap.rs b/libs/nak/src/runtime_wrap.rs
index d33bd59..9b5ff89 100644
--- a/libs/nak/src/runtime_wrap.rs
+++ b/libs/nak/src/runtime_wrap.rs
@@ -34,23 +34,6 @@ fn find_in_path(binary: &str) -> Option<PathBuf> {
}
pub fn resolve_umu_run() -> Option<PathBuf> {
- if is_flatpak() {
- // In Flatpak, umu-run must run on the host via flatpak-spawn --host.
- let host_copy = env::var("XDG_DATA_HOME")
- .ok()
- .map(PathBuf::from)
- .unwrap_or_else(|| {
- let home = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
- PathBuf::from(home).join(".local/share")
- })
- .join("fluorine/umu-run");
- if host_copy.exists() {
- return Some(host_copy);
- }
- // Fall back to bare name for host PATH resolution.
- return Some(PathBuf::from("umu-run"));
- }
-
let bundled = env::var("NAK_BUNDLED_UMU_RUN")
.ok()
.map(PathBuf::from)
@@ -64,14 +47,8 @@ pub fn resolve_umu_run() -> Option<PathBuf> {
}
}
-pub fn is_flatpak() -> bool {
- Path::new("/.flatpak-info").exists()
-}
-
/// Build a command to run `exe` with the given environment variables.
///
-/// In Flatpak mode, `flatpak-spawn --host` is used and env vars are passed as
-/// `--env=KEY=VALUE` flags (the host process does NOT inherit the sandbox env).
/// In steam-run mode, the command is wrapped with `steam-run`.
/// Otherwise the command runs directly.
pub fn build_command<S: AsRef<OsStr>>(
@@ -86,19 +63,6 @@ pub fn build_command<S: AsRef<OsStr>>(
}
return cmd;
}
- if is_flatpak() {
- let mut cmd = Command::new("flatpak-spawn");
- cmd.arg("--host");
- for (key, value) in envs {
- cmd.arg(format!(
- "--env={}={}",
- key,
- value.as_ref().to_string_lossy()
- ));
- }
- cmd.arg(exe.as_ref());
- return cmd;
- }
let mut cmd = Command::new(exe);
for (key, value) in envs {
cmd.env(key, value.as_ref());
diff --git a/libs/plugin_python/src/proxy/proxypython.cpp b/libs/plugin_python/src/proxy/proxypython.cpp
index 196bf23..c9c7ba2 100644
--- a/libs/plugin_python/src/proxy/proxypython.cpp
+++ b/libs/plugin_python/src/proxy/proxypython.cpp
@@ -224,6 +224,9 @@ bool ProxyPython::init(IOrganizer* moInfo)
if (!m_Runner || !m_Runner->isInitialized()) {
m_LoadFailure = FailureType::INITIALIZATION;
+ MOBase::log::error("Python proxy: interpreter failed to initialize, "
+ "Python plugins will be unavailable");
+ return true; // return true so the plugin stays loaded (shows diagnostic)
}
else {
m_Runner->addDllSearchPath(pluginDataRoot / "dlls");
@@ -296,7 +299,7 @@ QStringList ProxyPython::pluginList(const QDir& pluginPath) const
QList<QObject*> ProxyPython::load(const QString& identifier)
{
- if (!m_Runner) {
+ if (!m_Runner || !m_Runner->isInitialized()) {
return {};
}
return m_Runner->load(identifier);
@@ -304,7 +307,7 @@ QList<QObject*> ProxyPython::load(const QString& identifier)
void ProxyPython::unload(const QString& identifier)
{
- if (m_Runner) {
+ if (m_Runner && m_Runner->isInitialized()) {
return m_Runner->unload(identifier);
}
}
diff --git a/libs/plugin_python/src/runner/pythonrunner.cpp b/libs/plugin_python/src/runner/pythonrunner.cpp
index 05a0f30..b0bf48c 100644
--- a/libs/plugin_python/src/runner/pythonrunner.cpp
+++ b/libs/plugin_python/src/runner/pythonrunner.cpp
@@ -85,21 +85,6 @@ namespace mo2::python {
return true;
}
- std::optional<QByteArray> oldPythonHome;
- std::optional<QByteArray> oldPythonPath;
- auto restorePythonEnv = [&]() {
- if (oldPythonHome.has_value()) {
- setenv("PYTHONHOME", oldPythonHome->constData(), 1);
- } else {
- unsetenv("PYTHONHOME");
- }
- if (oldPythonPath.has_value()) {
- setenv("PYTHONPATH", oldPythonPath->constData(), 1);
- } else {
- unsetenv("PYTHONPATH");
- }
- };
-
try {
static const char* argv0 = "ModOrganizer.exe";
@@ -107,14 +92,49 @@ namespace mo2::python {
#ifdef MO2_PYTHON_SHARED_LIBRARY
// Ensure libpython symbols are globally visible for extension modules
// loaded later (_struct, PyQt6, etc.).
+ bool usedNoload = true;
void* pyHandle =
dlopen(MO2_PYTHON_SHARED_LIBRARY, RTLD_NOW | RTLD_GLOBAL | RTLD_NOLOAD);
if (pyHandle == nullptr) {
+ usedNoload = false;
pyHandle = dlopen(MO2_PYTHON_SHARED_LIBRARY, RTLD_NOW | RTLD_GLOBAL);
}
if (pyHandle == nullptr) {
- MOBase::log::warn("failed to dlopen python shared library '{}': {}",
+ MOBase::log::warn("python: failed to dlopen '{}': {}",
MO2_PYTHON_SHARED_LIBRARY, dlerror());
+ } else {
+ MOBase::log::debug("python: '{}' promoted to RTLD_GLOBAL ({})",
+ MO2_PYTHON_SHARED_LIBRARY,
+ usedNoload ? "RTLD_NOLOAD" : "fresh load");
+ }
+
+ // Diagnose which DSO the Python init symbols resolve to globally.
+ {
+ Dl_info di;
+ void* sym = dlsym(RTLD_DEFAULT, "Py_InitializeEx");
+ if (sym && dladdr(sym, &di)) {
+ fprintf(stderr, "[py-diag] Py_InitializeEx global from '%s'\n",
+ di.dli_fname);
+ MOBase::log::debug("python: Py_InitializeEx (global) from '{}'",
+ di.dli_fname);
+ } else {
+ fprintf(stderr, "[py-diag] Py_InitializeEx NOT in global scope "
+ "(sym=%p, err=%s)\n",
+ sym, dlerror());
+ MOBase::log::warn("python: Py_InitializeEx NOT in global scope "
+ "(sym={}, err={})",
+ sym, dlerror());
+ }
+ sym = dlsym(RTLD_DEFAULT, "Py_IsInitialized");
+ if (sym && dladdr(sym, &di)) {
+ fprintf(stderr, "[py-diag] Py_IsInitialized global from '%s'\n",
+ di.dli_fname);
+ MOBase::log::debug("python: Py_IsInitialized (global) from '{}'",
+ di.dli_fname);
+ } else {
+ fprintf(stderr, "[py-diag] Py_IsInitialized NOT in global scope\n");
+ MOBase::log::warn("python: Py_IsInitialized NOT in global scope");
+ }
}
#endif
#endif
@@ -130,6 +150,25 @@ namespace mo2::python {
} else {
pythonHome = QCoreApplication::applicationDirPath() + "/python";
}
+ if (!QDir(pythonHome).exists()) {
+ MOBase::log::warn("python: PYTHONHOME dir '{}' does not exist",
+ pythonHome);
+ }
+
+ std::optional<QByteArray> oldPythonHome;
+ std::optional<QByteArray> oldPythonPath;
+ auto restorePythonEnv = [&]() {
+ if (oldPythonHome.has_value()) {
+ setenv("PYTHONHOME", oldPythonHome->constData(), 1);
+ } else {
+ unsetenv("PYTHONHOME");
+ }
+ if (oldPythonPath.has_value()) {
+ setenv("PYTHONPATH", oldPythonPath->constData(), 1);
+ } else {
+ unsetenv("PYTHONPATH");
+ }
+ };
if (const char* v = std::getenv("PYTHONHOME"); v != nullptr) {
oldPythonHome = QByteArray(v);
}
@@ -137,48 +176,86 @@ namespace mo2::python {
oldPythonPath = QByteArray(v);
}
- if (QDir(pythonHome).exists()) {
- setenv("PYTHONHOME", pythonHome.toUtf8().constData(), 1);
-
- const QDir libDir(pythonHome + "/lib");
- const auto pyDirs =
- libDir.entryList({"python3.*"}, QDir::Dirs | QDir::NoDotAndDotDot);
- if (!pyDirs.isEmpty()) {
- const QString pyver = pyDirs.first();
- const QString pyPath = QString("%1/lib/%2:%1/lib/%2/site-packages:%1")
- .arg(pythonHome, pyver);
- setenv("PYTHONPATH", pyPath.toUtf8().constData(), 1);
- }
- }
-
// Paths we want to prepend/append for MO2 plugin loading.
auto paths = pythonPaths;
- PyConfig config;
- PyConfig_InitPythonConfig(&config);
+ // Configure bundled Python like running python/bin/python3 directly.
+ const QDir libDir(pythonHome + "/lib");
+ const auto pyDirs =
+ libDir.entryList({"python3.*"}, QDir::Dirs | QDir::NoDotAndDotDot);
+ const QString pyverDir = pyDirs.isEmpty() ? QStringLiteral("python3.13")
+ : pyDirs.first();
+ const QString stdlibDir = pythonHome + "/lib/" + pyverDir;
+ const QString stdlibZip = pythonHome + "/lib/python313.zip";
+ const QString rootDynloadDir = pythonHome + "/lib-dynload";
+ const QString rootPlatDir = pythonHome + "/plat-linux2";
+ const QString dynloadDir = stdlibDir + "/lib-dynload";
+ const QString siteDir = stdlibDir + "/site-packages";
- // from PyBind11
- config.parse_argv = 0;
- config.install_signal_handlers = 0;
+ QStringList corePaths = {stdlibDir, siteDir, dynloadDir};
+ if (QFile::exists(stdlibZip)) {
+ corePaths.prepend(stdlibZip);
+ }
+ if (QDir(rootDynloadDir).exists()) {
+ corePaths.append(rootDynloadDir);
+ }
+ if (QDir(rootPlatDir).exists()) {
+ corePaths.append(rootPlatDir);
+ }
- // from MO2
- config.site_import = 1;
- config.optimization_level = 2;
+ corePaths.append(pythonHome);
+ const QByteArray pyPath = corePaths.join(":").toUtf8();
+ setenv("PYTHONHOME", pythonHome.toUtf8().constData(), 1);
+ setenv("PYTHONPATH", pyPath.constData(), 1);
- py::initialize_interpreter(&config, 1, &argv0, true);
+ fprintf(stderr,
+ "[py-diag] calling Py_InitializeFromConfig, PYTHONHOME='%s', "
+ "Py_IsInitialized before=%d\n",
+ pythonHome.toUtf8().constData(), Py_IsInitialized());
+ MOBase::log::debug(
+ "python: calling Py_InitializeFromConfig, PYTHONHOME='{}', "
+ "Py_IsInitialized before={}",
+ pythonHome, Py_IsInitialized());
- // Restore process environment after interpreter startup so
- // subprocesses (umu/NaK/tools) are not forced onto MO2's Python.
- restorePythonEnv();
+ // Use Py_InitializeFromConfig (Python 3.8+) for explicit error reporting.
+ {
+ PyConfig config;
+ PyConfig_InitPythonConfig(&config);
+ // PYTHONHOME is already set via setenv; PyConfig reads it from env.
+ PyStatus status = Py_InitializeFromConfig(&config);
+ PyConfig_Clear(&config);
+ if (PyStatus_Exception(status)) {
+ fprintf(stderr,
+ "[py-diag] Py_InitializeFromConfig FAILED: '%s' [in '%s']\n",
+ status.err_msg ? status.err_msg : "(no message)",
+ status.func ? status.func : "(no func)");
+ MOBase::log::error(
+ "python: Py_InitializeFromConfig failed: '{}' [in '{}']",
+ status.err_msg ? status.err_msg : "(no message)",
+ status.func ? status.func : "(no func)");
+ restorePythonEnv();
+ return false;
+ }
+ }
+
+ fprintf(stderr, "[py-diag] Py_IsInitialized after=%d\n",
+ Py_IsInitialized());
+ MOBase::log::debug("python: Py_IsInitialized after={}",
+ Py_IsInitialized());
if (!Py_IsInitialized()) {
+ const char* ph = std::getenv("PYTHONHOME");
+ const char* pp = std::getenv("PYTHONPATH");
MOBase::log::error(
- "failed to init python: failed to initialize interpreter.");
-
- if (PyGILState_Check()) {
- PyEval_SaveThread();
- }
-
+ "failed to init python: Py_IsInitialized() returned false.");
+ MOBase::log::error(" PYTHONHOME='{}', PYTHONPATH='{}'",
+ ph ? ph : "(null)", pp ? pp : "(null)");
+ MOBase::log::error(" pythonHome='{}', exists={}", pythonHome,
+ QDir(pythonHome).exists() ? "yes" : "no");
+ const QString encPath = pythonHome + "/lib/python3.13/encodings";
+ MOBase::log::error(" encodings dir='{}', exists={}", encPath,
+ QDir(encPath).exists() ? "yes" : "no");
+ restorePythonEnv();
return false;
}
@@ -201,11 +278,11 @@ namespace mo2::python {
// when Python is initialized, the GIl is acquired, and if it is not
// release, trying to acquire it on a different thread will deadlock
PyEval_SaveThread();
+ restorePythonEnv();
return true;
}
catch (const py::error_already_set& ex) {
- restorePythonEnv();
MOBase::log::error("failed to init python: {}", ex.what());
return false;
}
@@ -353,7 +430,8 @@ namespace mo2::python {
return allInterfaceList;
}
catch (const py::error_already_set& ex) {
- MOBase::log::error("Failed to import plugin from {}.", identifier);
+ MOBase::log::error("Failed to import plugin from {}: {}", identifier,
+ ex.what());
throw pyexcept::PythonError(ex);
}
}
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;
}