aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xdocker/build-inner.sh185
-rw-r--r--libs/nak/src/game_finder/heroic.rs23
-rw-r--r--libs/nak/src/game_finder/known_games.rs159
-rw-r--r--libs/nak/src/game_finder/mod.rs31
-rw-r--r--libs/nak_ffi/include/nak_ffi.h1
-rw-r--r--libs/nak_ffi/src/lib.rs2
-rw-r--r--libs/plugin_python/src/runner/pythonrunner.cpp102
-rw-r--r--src/src/commandline.cpp3
-rw-r--r--src/src/mainwindow.cpp162
-rw-r--r--src/src/moapplication.cpp129
-rw-r--r--src/src/organizercore.cpp129
-rw-r--r--src/src/plugincontainer.cpp326
-rw-r--r--src/src/processrunner.cpp412
-rw-r--r--src/src/protonlauncher.cpp201
-rw-r--r--src/src/protonlauncher.h2
-rw-r--r--src/src/savestab.cpp655
-rw-r--r--src/src/shared/appconfig.cpp22
-rw-r--r--src/src/shared/appconfig.h10
-rw-r--r--src/src/spawn.cpp550
-rw-r--r--src/src/wineprefix.cpp36
20 files changed, 1804 insertions, 1336 deletions
diff --git a/docker/build-inner.sh b/docker/build-inner.sh
index dda9b14..847bfae 100755
--- a/docker/build-inner.sh
+++ b/docker/build-inner.sh
@@ -30,35 +30,7 @@ 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 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
-zf = zipfile.ZipFile('/src/${RUNDIR}/umu-run')
-zf.extractall('src')
-run_py = pathlib.Path('src/umu/umu_run.py')
-src = run_py.read_text()
-
-# Patch 1: preserve STEAM_COMPAT_CLIENT_INSTALL_PATH
-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)
-
-run_py.write_text(src)
-PATCHEOF
-)
- "${BUILD_PY}" -m zipapp "${UMU_PATCH_DIR}/src" -o "${OUT_DIR}/umu-run" -p '/usr/bin/env python3'
- chmod +x "${OUT_DIR}/umu-run"
- rm -rf "${UMU_PATCH_DIR}"
-fi
+[ -f "${RUNDIR}/umu-run" ] && cp -f "${RUNDIR}/umu-run" "${OUT_DIR}/umu-run"
[ -f "${RUNDIR}/README-PORTABLE.txt" ] && cp -f "${RUNDIR}/README-PORTABLE.txt" "${OUT_DIR}/"
[ -f "/src/src/fluorine-manager" ] && cp -f "/src/src/fluorine-manager" "${OUT_DIR}/"
@@ -156,6 +128,18 @@ find "${OUT_DIR}/plugins" -name "*.so" -exec sh -c 'ldd "$1" 2>/dev/null | grep
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}"
+# PyQt6 extension modules — these link against Qt6 libs that MO2 binaries don't
+# directly depend on (e.g. libQt6OpenGLWidgets, libQt6PrintSupport). Without
+# bundling these, PyQt6 falls back to host Qt which may be a different version.
+# Scan from the system dist-packages (portable Python isn't copied yet).
+for pyqt_search in /usr/lib/python3/dist-packages/PyQt6 \
+ "/usr/lib/python${PY_MM}/dist-packages/PyQt6" \
+ "/usr/local/lib/python${PY_MM}/dist-packages/PyQt6"; do
+ if [ -d "${pyqt_search}" ]; then
+ find "${pyqt_search}" -name "*.so" -exec sh -c 'ldd "$1" 2>/dev/null | grep "=>" | awk "{print \$3}" | grep "^/"' _ {} \; >> "${ALL_DEPS}"
+ break
+ fi
+done
sort -u "${ALL_DEPS}" | while read -r dep; do
dep_name="$(basename "${dep}")"
@@ -231,10 +215,29 @@ if [ -d "${PORTABLE_PY}" ]; then
find "${OUT_DIR}/python" -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
find "${OUT_DIR}/python" -name "*.pyc" -delete 2>/dev/null || true
- # Ensure versioned soname symlink exists (pybind11 links against libpython3.13.so.1.0).
- if [ -f "${OUT_DIR}/python/lib/libpython${PY_MM}.so" ] && \
- [ ! -f "${OUT_DIR}/python/lib/libpython${PY_MM}.so.1.0" ]; then
- ln -sf "libpython${PY_MM}.so" "${OUT_DIR}/python/lib/libpython${PY_MM}.so.1.0"
+ # The portable Python's libpython has SONAME "libpython3.13.so", but
+ # pybind11::embed links librunner.so against the build-venv's libpython
+ # which may have SONAME "libpython3.13.so.1.0". A SONAME mismatch causes
+ # two copies of libpython to be loaded at runtime, making
+ # Py_IsInitialized() return false after Py_InitializeFromConfig() succeeds.
+ #
+ # Fix: patch the portable Python's SONAME to include the .1.0 suffix,
+ # matching what the linker recorded in librunner.so's DT_NEEDED.
+ PP_LIBPY="${OUT_DIR}/python/lib/libpython${PY_MM}.so"
+ if [ -f "${PP_LIBPY}" ]; then
+ CURRENT_SONAME="$(readelf -d "${PP_LIBPY}" 2>/dev/null | grep SONAME | sed 's/.*\[//' | sed 's/\]//')"
+ echo "Portable Python SONAME: ${CURRENT_SONAME}"
+ if [ "${CURRENT_SONAME}" = "libpython${PY_MM}.so" ]; then
+ echo "Patching SONAME to libpython${PY_MM}.so.1.0 ..."
+ patchelf --set-soname "libpython${PY_MM}.so.1.0" "${PP_LIBPY}"
+ # Rename the file to match and create backwards symlink.
+ mv "${PP_LIBPY}" "${PP_LIBPY}.1.0"
+ ln -sf "libpython${PY_MM}.so.1.0" "${PP_LIBPY}"
+ fi
+ # Ensure the .1.0 name exists (either as the real file or a symlink).
+ if [ ! -e "${PP_LIBPY}.1.0" ]; then
+ ln -sf "libpython${PY_MM}.so" "${PP_LIBPY}.1.0"
+ fi
fi
else
echo "ERROR: Portable Python not found at ${PORTABLE_PY}"
@@ -274,9 +277,11 @@ done
# ── Fix RPATH so binaries find libs without LD_LIBRARY_PATH ──
echo "Patching RPATH..."
-patchelf --set-rpath '$ORIGIN/lib' "${OUT_DIR}/ModOrganizer-core"
+patchelf --set-rpath '$ORIGIN/lib:$ORIGIN/python/lib' "${OUT_DIR}/ModOrganizer-core"
[ -f "${OUT_DIR}/lootcli" ] && patchelf --set-rpath '$ORIGIN/lib' "${OUT_DIR}/lootcli"
-find "${OUT_DIR}/plugins" -name "*.so" -exec patchelf --set-rpath '$ORIGIN/../lib' {} \; 2>/dev/null || true
+find "${OUT_DIR}/plugins" -name "*.so" -exec patchelf --set-rpath '$ORIGIN/../lib:$ORIGIN/../python/lib' {} \; 2>/dev/null || true
+# Libraries in lib/ (e.g. librunner.so) need to find sibling libs and python/lib.
+find "${OUT_DIR}/lib" -name "*.so" -exec patchelf --set-rpath '$ORIGIN:$ORIGIN/../python/lib' {} \; 2>/dev/null || true
# ── Validate embedded Python runtime ──
if ! PYTHONHOME="${OUT_DIR}/python" \
@@ -295,15 +300,34 @@ set -euo pipefail
SELF="$(readlink -f "$0")"
HERE="$(cd "$(dirname "$SELF")" && pwd)"
export PATH="${HERE}:${PATH}"
-export LD_LIBRARY_PATH="${HERE}/lib:${HERE}/python/lib:${LD_LIBRARY_PATH:-}"
+
+# ── Sync portable Python to data dir (only when outdated) ──
+FLUORINE_DATA="${HOME}/.local/share/fluorine"
+PYTHON_DST="${FLUORINE_DATA}/python"
+PYTHON_SRC="${HERE}/python"
+
+if [ -d "${PYTHON_SRC}" ]; then
+ CURRENT_VER="$(stat -c '%i:%Y' "${PYTHON_SRC}/bin/python3" 2>/dev/null || echo "unknown")"
+ MARKER="${PYTHON_DST}/.version"
+
+ if [ ! -f "${MARKER}" ] || [ "$(cat "${MARKER}" 2>/dev/null)" != "${CURRENT_VER}" ]; then
+ echo "Syncing Python runtime to ${PYTHON_DST}..." >&2
+ rm -rf "${PYTHON_DST}"
+ mkdir -p "${PYTHON_DST}"
+ (cd "${PYTHON_SRC}" && tar --exclude-vcs -cf - .) | (cd "${PYTHON_DST}" && tar -xf -)
+ echo "${CURRENT_VER}" > "${MARKER}"
+ fi
+fi
+
+export LD_LIBRARY_PATH="${HERE}/lib:${PYTHON_DST}/lib:${LD_LIBRARY_PATH:-}"
export MO2_BASE_DIR="${HERE}"
export MO2_PLUGINS_DIR="${HERE}/plugins"
export MO2_DLLS_DIR="${HERE}/dlls"
-export MO2_PYTHON_DIR="${HERE}/python"
+export MO2_PYTHON_DIR="${PYTHON_DST}"
# PYTHONHOME is set only for the MO2 process (not exported to children like
# umu-run/Proton which have their own Python). MO2_PYTHON_DIR lets the
# binary reconstruct it internally.
-MO2_PYTHONHOME="${HERE}/python"
+MO2_PYTHONHOME="${PYTHON_DST}"
unset PYTHONPATH PYTHONNOUSERSITE PYTHONHOME
# Use bundled Qt6 plugins.
@@ -341,6 +365,19 @@ if [ -d "${APPDIR}/usr/bin/qt6plugins" ]; then
cp -a "${APPDIR}/usr/bin/qt6plugins"/. "${APPDIR}/usr/plugins/"
fi
+# Symlink the portable Python's libpython into usr/lib/ so that:
+# 1) linuxdeploy sees the dependency as already satisfied and doesn't bundle
+# the container's system libpython (which lacks static built-in modules)
+# 2) librunner.so (which lives in usr/lib/) can resolve libpython via $ORIGIN
+PP_APPDIR_LIB="${APPDIR}/usr/bin/python/lib"
+for pp_lib in "${PP_APPDIR_LIB}"/libpython*.so*; do
+ [ -e "${pp_lib}" ] || continue
+ pp_name="$(basename "${pp_lib}")"
+ # Remove any copy linuxdeploy might have placed, then symlink to portable.
+ rm -f "${APPDIR}/usr/lib/${pp_name}"
+ ln -sf ../bin/python/lib/"${pp_name}" "${APPDIR}/usr/lib/${pp_name}"
+done
+
# 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/"
@@ -358,10 +395,13 @@ if [ -f "/usr/share/icons/default/index.theme" ]; then
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"
+# Update RPATH for new lib location.
+# usr/lib/ contains librunner.so and other MO2 libs. The symlinks above make
+# libpython resolvable via $ORIGIN for libraries in usr/lib/.
+patchelf --set-rpath '$ORIGIN/../lib:$ORIGIN/python/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
+find "${APPDIR}/usr/lib" -name "*.so" -not -name "libpython*" -exec patchelf --set-rpath '$ORIGIN' {} \; 2>/dev/null || true
# Create AppRun wrapper
cat > "${APPDIR}/AppRun" <<'APPRUN'
@@ -372,35 +412,42 @@ 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 -)
-}
+# ── Sync portable Python to data dir (only when outdated) ──
+# Plugins and dlls are read-only and loaded directly from the squashfs mount.
+# Python needs a writable copy because pip/plugins may modify site-packages.
+FLUORINE_DATA="${HOME}/.local/share/fluorine"
+PYTHON_DST="${FLUORINE_DATA}/python"
+PYTHON_SRC="${BIN}/python"
-# Keep runtime payload outside the transient AppImage mount.
-sync_dir "plugins"
-sync_dir "dlls"
-sync_dir "python"
+if [ -d "${PYTHON_SRC}" ]; then
+ APPIMAGE_REAL="$(readlink -f "${APPIMAGE:-$0}")"
+ CURRENT_VER="$(stat -c '%i:%Y' "${APPIMAGE_REAL}" 2>/dev/null || echo "unknown")"
+ MARKER="${PYTHON_DST}/.version"
-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:-}"
+ if [ ! -f "${MARKER}" ] || [ "$(cat "${MARKER}" 2>/dev/null)" != "${CURRENT_VER}" ]; then
+ echo "Syncing Python runtime to ${PYTHON_DST}..."
+ rm -rf "${PYTHON_DST}"
+ mkdir -p "${PYTHON_DST}"
+ (cd "${PYTHON_SRC}" && tar --exclude-vcs -cf - .) | (cd "${PYTHON_DST}" && tar -xf -)
+ echo "${CURRENT_VER}" > "${MARKER}"
+ fi
fi
+# Save the original (pre-AppImage) environment so game launches can restore it.
+# Without this, AppImage's LD_LIBRARY_PATH/PATH leak into umu-run/Proton and
+# cause library conflicts that make games crash.
+export FLUORINE_ORIG_LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}"
+export FLUORINE_ORIG_PATH="${PATH}"
+export FLUORINE_ORIG_XDG_DATA_DIRS="${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
+export FLUORINE_ORIG_QT_PLUGIN_PATH="${QT_PLUGIN_PATH:-}"
+
+export PATH="${BIN}:${PATH}"
+export LD_LIBRARY_PATH="${HERE}/usr/lib:${PYTHON_DST}/lib:${LD_LIBRARY_PATH:-}"
+
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
+export MO2_PLUGINS_DIR="${BIN}/plugins"
+export MO2_DLLS_DIR="${BIN}/dlls"
+export MO2_PYTHON_DIR="${PYTHON_DST}"
unset PYTHONPATH PYTHONNOUSERSITE PYTHONHOME
@@ -434,12 +481,22 @@ 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.
+#
+# linuxdeploy scans binary deps and bundles them. We must prevent it from
+# bundling the container's system libpython (which lacks statically-compiled
+# extension modules like _ctypes, resource, _lzma). The portable Python
+# runtime in usr/bin/python/lib/ is the correct copy. Symlinks in usr/lib/
+# point to it, and --exclude-library prevents linuxdeploy from overwriting them.
export ARCH=x86_64
+# Tell linuxdeploy to skip libpython — we bundle the portable Python's copy
+# via symlinks in usr/lib/ and must not replace it with the container's version.
+export LINUXDEPLOY_EXCLUDE_MODULES="libpython"
"${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" \
+ --exclude-library "libpython*" \
2>&1 || {
echo "WARNING: linuxdeploy AppImage generation failed, falling back to staging dir output"
}
diff --git a/libs/nak/src/game_finder/heroic.rs b/libs/nak/src/game_finder/heroic.rs
index 7bdda09..e80c1e1 100644
--- a/libs/nak/src/game_finder/heroic.rs
+++ b/libs/nak/src/game_finder/heroic.rs
@@ -8,7 +8,7 @@ use std::path::{Path, PathBuf};
use serde::Deserialize;
-use super::known_games::find_by_gog_id;
+use super::known_games::{find_by_epic_id, find_by_gog_id, find_by_title};
use super::{Game, HeroicStore, Launcher};
use crate::logging::{log_info, log_warning};
@@ -202,11 +202,16 @@ fn detect_epic_games(heroic_path: &Path) -> Vec<Game> {
}
// Get title
- let name = game_obj
+ let title = game_obj
.get("title")
.and_then(|v| v.as_str())
- .unwrap_or(app_name)
- .to_string();
+ .unwrap_or(app_name);
+
+ // Look up known game info: try Epic AppName first, then title match
+ let known_game = find_by_epic_id(app_name)
+ .or_else(|| find_by_title(title));
+
+ let name = title.to_string();
// Get Wine prefix
let prefix_path = get_heroic_game_prefix(heroic_path, app_name);
@@ -219,11 +224,11 @@ fn detect_epic_games(heroic_path: &Path) -> Vec<Game> {
launcher: Launcher::Heroic {
store: HeroicStore::Epic,
},
- my_games_folder: None,
- appdata_local_folder: None,
- appdata_roaming_folder: None,
- registry_path: None,
- registry_value: None,
+ my_games_folder: known_game.and_then(|g| g.my_games_folder.map(String::from)),
+ appdata_local_folder: known_game.and_then(|g| g.appdata_local_folder.map(String::from)),
+ appdata_roaming_folder: known_game.and_then(|g| g.appdata_roaming_folder.map(String::from)),
+ registry_path: known_game.map(|g| g.registry_path.to_string()),
+ registry_value: known_game.map(|g| g.registry_value.to_string()),
});
}
}
diff --git a/libs/nak/src/game_finder/known_games.rs b/libs/nak/src/game_finder/known_games.rs
index 8a30c18..117b0c7 100644
--- a/libs/nak/src/game_finder/known_games.rs
+++ b/libs/nak/src/game_finder/known_games.rs
@@ -2,6 +2,8 @@
//!
//! Contains metadata for games that NaK supports, including:
//! - Steam App ID
+//! - GOG App ID
+//! - Epic Games Store App Name (Legendary/Heroic internal identifier)
//! - My Games folder name (Documents/My Games/*)
//! - AppData/Local folder name
//! - Registry path for game detection
@@ -13,8 +15,10 @@ pub struct KnownGame {
pub name: &'static str,
/// Steam App ID
pub steam_app_id: &'static str,
- /// GOG App ID (if available)
+ /// GOG App ID (if available on GOG)
pub gog_app_id: Option<&'static str>,
+ /// Epic Games Store AppName / Legendary internal ID (if available on Epic)
+ pub epic_app_id: Option<&'static str>,
/// Folder name in Documents/My Games (if applicable)
pub my_games_folder: Option<&'static str>,
/// Folder name in AppData/Local (if applicable)
@@ -35,7 +39,8 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
KnownGame {
name: "Enderal",
steam_app_id: "933480",
- gog_app_id: None,
+ gog_app_id: Some("1708684988"), // Enderal: Forgotten Stories on GOG
+ epic_app_id: None,
my_games_folder: Some("Enderal"),
appdata_local_folder: None,
appdata_roaming_folder: None,
@@ -46,7 +51,8 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
KnownGame {
name: "Enderal Special Edition",
steam_app_id: "976620",
- gog_app_id: None,
+ gog_app_id: None, // Not on GOG (only original Enderal)
+ epic_app_id: None,
my_games_folder: Some("Enderal Special Edition"),
appdata_local_folder: None,
appdata_roaming_folder: None,
@@ -58,6 +64,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Fallout 3",
steam_app_id: "22300",
gog_app_id: Some("1454315831"), // Fallout 3 GOTY
+ epic_app_id: Some("adeae8bbfc94427db57c7dfecce3f1d4"),
my_games_folder: Some("Fallout3"),
appdata_local_folder: Some("Fallout3"),
appdata_roaming_folder: None,
@@ -68,7 +75,8 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
KnownGame {
name: "Fallout 4",
steam_app_id: "377160",
- gog_app_id: None,
+ gog_app_id: Some("1998527297"), // Fallout 4 GOTY on GOG
+ epic_app_id: Some("61d52ce4d09d41e48800c22784d13ae8"),
my_games_folder: Some("Fallout4"),
appdata_local_folder: Some("Fallout4"),
appdata_roaming_folder: None,
@@ -80,6 +88,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Fallout 4 VR",
steam_app_id: "611660",
gog_app_id: None,
+ epic_app_id: None,
my_games_folder: Some("Fallout4VR"),
appdata_local_folder: None,
appdata_roaming_folder: None,
@@ -91,6 +100,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Fallout New Vegas",
steam_app_id: "22380",
gog_app_id: Some("1454587428"), // Fallout NV Ultimate
+ epic_app_id: Some("5daeb974a22a435988892319b3a4f476"),
my_games_folder: Some("FalloutNV"),
appdata_local_folder: Some("FalloutNV"),
appdata_roaming_folder: None,
@@ -102,6 +112,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Morrowind",
steam_app_id: "22320",
gog_app_id: Some("1440163901"), // Morrowind GOTY
+ epic_app_id: None, // On Epic but internal AppName not yet known
my_games_folder: Some("Morrowind"),
appdata_local_folder: None,
appdata_roaming_folder: None,
@@ -113,6 +124,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Oblivion",
steam_app_id: "22330",
gog_app_id: Some("1458058109"), // Oblivion GOTY Deluxe
+ epic_app_id: None, // On Epic but internal AppName not yet known
my_games_folder: Some("Oblivion"),
appdata_local_folder: Some("Oblivion"),
appdata_roaming_folder: None,
@@ -123,7 +135,8 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
KnownGame {
name: "Skyrim",
steam_app_id: "72850",
- gog_app_id: None, // Not on GOG
+ gog_app_id: None, // Not on GOG (only SE/AE)
+ epic_app_id: None, // Not on Epic (only SE/AE)
my_games_folder: Some("Skyrim"),
appdata_local_folder: Some("Skyrim"),
appdata_roaming_folder: None,
@@ -134,7 +147,8 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
KnownGame {
name: "Skyrim Special Edition",
steam_app_id: "489830",
- gog_app_id: Some("1711230643"), // Skyrim SE Anniversary Edition
+ gog_app_id: Some("1711230643"), // Skyrim SE on GOG
+ epic_app_id: Some("ac82db5035584c7f8a2c548d98c86b2c"),
my_games_folder: Some("Skyrim Special Edition"),
appdata_local_folder: Some("Skyrim Special Edition"),
appdata_roaming_folder: None,
@@ -146,6 +160,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Skyrim VR",
steam_app_id: "611670",
gog_app_id: None,
+ epic_app_id: None,
my_games_folder: Some("Skyrim VR"),
appdata_local_folder: None,
appdata_roaming_folder: None,
@@ -156,7 +171,8 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
KnownGame {
name: "Starfield",
steam_app_id: "1716740",
- gog_app_id: None,
+ gog_app_id: None, // Xbox/Steam exclusive
+ epic_app_id: None,
my_games_folder: Some("Starfield"),
appdata_local_folder: None,
appdata_roaming_folder: None,
@@ -169,6 +185,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "The Witcher 3",
steam_app_id: "292030",
gog_app_id: Some("1495134320"), // Witcher 3 GOTY
+ epic_app_id: None,
my_games_folder: Some("The Witcher 3"),
appdata_local_folder: None,
appdata_roaming_folder: None,
@@ -180,6 +197,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Cyberpunk 2077",
steam_app_id: "1091500",
gog_app_id: Some("1423049311"),
+ epic_app_id: None,
my_games_folder: None,
appdata_local_folder: Some("CD Projekt Red/Cyberpunk 2077"),
appdata_roaming_folder: None,
@@ -192,6 +210,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Baldur's Gate 3",
steam_app_id: "1086940",
gog_app_id: Some("1456460669"),
+ epic_app_id: None,
my_games_folder: None,
appdata_local_folder: Some("Larian Studios/Baldur's Gate 3"),
appdata_roaming_folder: None,
@@ -201,6 +220,15 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
},
];
+/// Alternative GOG App IDs that should map to the same game.
+/// Some games have multiple GOG product IDs (e.g. different editions).
+const GOG_ID_ALIASES: &[(&str, &str)] = &[
+ // Morrowind: Vortex uses 1435828767, NaK primary is 1440163901
+ ("1435828767", "1440163901"),
+ // Skyrim Anniversary Edition is a separate GOG product but same game as SE
+ ("1801825368", "1711230643"),
+];
+
/// Find a known game by Steam App ID
pub fn find_by_steam_id(app_id: &str) -> Option<&'static KnownGame> {
let normalized_id = normalize_steam_id(app_id);
@@ -209,9 +237,79 @@ pub fn find_by_steam_id(app_id: &str) -> Option<&'static KnownGame> {
/// Find a known game by GOG App ID
pub fn find_by_gog_id(app_id: &str) -> Option<&'static KnownGame> {
+ // Direct match first
+ if let Some(game) = KNOWN_GAMES.iter().find(|g| g.gog_app_id == Some(app_id)) {
+ return Some(game);
+ }
+ // Check aliases: resolve alternate GOG ID to primary, then look up
+ for &(alias, primary) in GOG_ID_ALIASES {
+ if app_id == alias {
+ return KNOWN_GAMES.iter().find(|g| g.gog_app_id == Some(primary));
+ }
+ }
+ None
+}
+
+/// Find a known game by Epic Games Store AppName (Legendary/Heroic internal ID)
+pub fn find_by_epic_id(app_id: &str) -> Option<&'static KnownGame> {
KNOWN_GAMES
.iter()
- .find(|g| g.gog_app_id == Some(app_id))
+ .find(|g| g.epic_app_id == Some(app_id))
+}
+
+/// Strip punctuation and collapse whitespace for fuzzy title matching.
+fn normalize_for_matching(s: &str) -> String {
+ s.chars()
+ .map(|c| if c.is_alphanumeric() || c == ' ' { c } else { ' ' })
+ .collect::<String>()
+ .split_whitespace()
+ .collect::<Vec<_>>()
+ .join(" ")
+}
+
+/// Find a known game by title, using fuzzy matching.
+/// Strips common suffixes like "Game of the Year Edition" for comparison.
+pub fn find_by_title(title: &str) -> Option<&'static KnownGame> {
+ let title_lower = title.to_lowercase();
+
+ // Exact match first
+ if let Some(game) = KNOWN_GAMES.iter().find(|g| g.name.to_lowercase() == title_lower) {
+ return Some(game);
+ }
+
+ // Check if the title starts with or contains a known game name
+ // Sort by name length descending so "Skyrim Special Edition" matches before "Skyrim"
+ let mut games_by_name_len: Vec<&KnownGame> = KNOWN_GAMES.iter().collect();
+ games_by_name_len.sort_by(|a, b| b.name.len().cmp(&a.name.len()));
+
+ // Normalize both title and game names: strip colons and extra whitespace for comparison
+ let title_normalized = normalize_for_matching(&title_lower);
+
+ for game in games_by_name_len {
+ let game_lower = game.name.to_lowercase();
+ let game_normalized = normalize_for_matching(&game_lower);
+
+ // Direct containment (e.g. "Fallout 3: Game of the Year Edition" contains "Fallout 3")
+ if title_lower.contains(&game_lower) {
+ return Some(game);
+ }
+
+ // Normalized containment (e.g. "Fallout: New Vegas" normalized to "fallout new vegas"
+ // matches game name "Fallout New Vegas" normalized to "fallout new vegas")
+ if title_normalized.contains(&game_normalized) {
+ return Some(game);
+ }
+
+ // After-colon match: "The Elder Scrolls III: Morrowind" -> check "Morrowind"
+ if let Some(after_colon) = title_lower.split(':').nth(1) {
+ let after_colon = after_colon.trim();
+ if after_colon.starts_with(&game_lower) {
+ return Some(game);
+ }
+ }
+ }
+
+ None
}
/// Find a known game by name (case-insensitive)
@@ -234,7 +332,7 @@ fn normalize_steam_id(app_id: &str) -> &str {
#[cfg(test)]
mod tests {
- use super::find_by_steam_id;
+ use super::*;
#[test]
fn fallout_3_goty_alias_maps_to_fallout_3() {
@@ -242,4 +340,47 @@ mod tests {
assert_eq!(game.name, "Fallout 3");
assert_eq!(game.steam_app_id, "22300");
}
+
+ #[test]
+ fn find_by_epic_id_works() {
+ let game = find_by_epic_id("ac82db5035584c7f8a2c548d98c86b2c")
+ .expect("Should find Skyrim SE by Epic ID");
+ assert_eq!(game.name, "Skyrim Special Edition");
+ }
+
+ #[test]
+ fn gog_alias_maps_correctly() {
+ // Skyrim Anniversary Edition GOG ID maps to Skyrim SE
+ let game = find_by_gog_id("1801825368")
+ .expect("Skyrim AE GOG ID should map to Skyrim SE");
+ assert_eq!(game.name, "Skyrim Special Edition");
+ }
+
+ #[test]
+ fn find_by_title_matches_full_titles() {
+ let game = find_by_title("The Elder Scrolls III: Morrowind Game of the Year Edition")
+ .expect("Should find Morrowind by full Epic title");
+ assert_eq!(game.name, "Morrowind");
+ }
+
+ #[test]
+ fn find_by_title_matches_colon_titles() {
+ let game = find_by_title("The Elder Scrolls IV: Oblivion Game of the Year Edition")
+ .expect("Should find Oblivion by full title");
+ assert_eq!(game.name, "Oblivion");
+ }
+
+ #[test]
+ fn find_by_title_prefers_longer_match() {
+ let game = find_by_title("Skyrim Special Edition")
+ .expect("Should find Skyrim SE, not Skyrim");
+ assert_eq!(game.name, "Skyrim Special Edition");
+ }
+
+ #[test]
+ fn find_by_title_fallout_nv() {
+ let game = find_by_title("Fallout: New Vegas Ultimate Edition")
+ .expect("Should find Fallout NV");
+ assert_eq!(game.name, "Fallout New Vegas");
+ }
}
diff --git a/libs/nak/src/game_finder/mod.rs b/libs/nak/src/game_finder/mod.rs
index c383c8a..600751a 100644
--- a/libs/nak/src/game_finder/mod.rs
+++ b/libs/nak/src/game_finder/mod.rs
@@ -20,7 +20,10 @@ use std::path::PathBuf;
pub use bottles::detect_bottles_games;
pub use heroic::detect_heroic_games;
-pub use known_games::{find_by_gog_id, find_by_name, find_by_steam_id, KnownGame, KNOWN_GAMES};
+pub use known_games::{
+ find_by_epic_id, find_by_gog_id, find_by_name, find_by_steam_id, find_by_title, KnownGame,
+ KNOWN_GAMES,
+};
pub use registry::{read_registry_value, wine_path_to_linux};
pub use steam::{detect_steam_games, find_game_install_path, find_game_prefix_path, get_known_game};
@@ -161,10 +164,16 @@ impl GameScanResult {
// Public API
// ============================================================================
-/// Detect all installed games from all supported launchers
+/// Detect all installed games from all supported launchers.
+///
+/// Games are detected in priority order: Steam -> Heroic (GOG -> Epic) -> Bottles.
+/// When the same game is found from multiple launchers, the higher-priority
+/// detection is kept and duplicates are skipped. This ensures registry entries
+/// (which are shared across storefronts) prefer Steam paths, then GOG, then Epic.
pub fn detect_all_games() -> GameScanResult {
let mut result = GameScanResult::default();
+ // Detect in priority order: Steam first, then GOG (via Heroic), then Epic, then Bottles.
let steam_games = detect_steam_games();
result.steam_count = steam_games.len();
result.games.extend(steam_games);
@@ -177,9 +186,27 @@ pub fn detect_all_games() -> GameScanResult {
result.bottles_count = bottles_games.len();
result.games.extend(bottles_games);
+ // Deduplicate: keep the first occurrence of each game (by registry_path),
+ // which respects the detection order (Steam > GOG > Epic > Bottles).
+ deduplicate_games(&mut result);
+
result
}
+/// Remove duplicate game detections, keeping the first (highest-priority) entry.
+/// Two games are considered duplicates if they have the same registry_path.
+fn deduplicate_games(result: &mut GameScanResult) {
+ let mut seen_registry_paths = std::collections::HashSet::new();
+ result.games.retain(|game| {
+ if let Some(ref reg_path) = game.registry_path {
+ seen_registry_paths.insert(reg_path.clone())
+ } else {
+ // Games without registry paths are always kept (no risk of conflict)
+ true
+ }
+ });
+}
+
/// Detect only Steam games
pub fn detect_steam_only() -> GameScanResult {
let steam_games = detect_steam_games();
diff --git a/libs/nak_ffi/include/nak_ffi.h b/libs/nak_ffi/include/nak_ffi.h
index 8e24041..cc66d9e 100644
--- a/libs/nak_ffi/include/nak_ffi.h
+++ b/libs/nak_ffi/include/nak_ffi.h
@@ -46,6 +46,7 @@ typedef struct {
const char *name;
const char *steam_app_id;
const char *gog_app_id; /* NULL if none */
+ const char *epic_app_id; /* NULL if none */
const char *my_games_folder; /* NULL if not applicable */
const char *appdata_local_folder; /* NULL if not applicable */
const char *appdata_roaming_folder; /* NULL if not applicable */
diff --git a/libs/nak_ffi/src/lib.rs b/libs/nak_ffi/src/lib.rs
index fe18cf3..65592fa 100644
--- a/libs/nak_ffi/src/lib.rs
+++ b/libs/nak_ffi/src/lib.rs
@@ -212,6 +212,7 @@ pub struct NakKnownGame {
pub name: *const c_char,
pub steam_app_id: *const c_char,
pub gog_app_id: *const c_char, // null if none
+ pub epic_app_id: *const c_char, // null if none
pub my_games_folder: *const c_char,
pub appdata_local_folder: *const c_char,
pub appdata_roaming_folder: *const c_char,
@@ -236,6 +237,7 @@ static KNOWN_GAMES_FFI: std::sync::LazyLock<KnownGamesVec> = std::sync::LazyLock
name: leak_str(kg.name),
steam_app_id: leak_str(kg.steam_app_id),
gog_app_id: leak_str_opt(kg.gog_app_id),
+ epic_app_id: leak_str_opt(kg.epic_app_id),
my_games_folder: leak_str_opt(kg.my_games_folder),
appdata_local_folder: leak_str_opt(kg.appdata_local_folder),
appdata_roaming_folder: leak_str_opt(kg.appdata_roaming_folder),
diff --git a/libs/plugin_python/src/runner/pythonrunner.cpp b/libs/plugin_python/src/runner/pythonrunner.cpp
index b0bf48c..3bc62a1 100644
--- a/libs/plugin_python/src/runner/pythonrunner.cpp
+++ b/libs/plugin_python/src/runner/pythonrunner.cpp
@@ -89,55 +89,63 @@ namespace mo2::python {
static const char* argv0 = "ModOrganizer.exe";
#ifndef _WIN32
-#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("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.
+ //
+ // We must promote the *already-loaded* libpython to RTLD_GLOBAL.
+ // Using the compile-time filename (e.g. "libpython3.13.so.1.0") with
+ // RTLD_NOLOAD can fail when the portable Python's SONAME differs
+ // (e.g. "libpython3.13.so"), causing a second copy to be loaded and
+ // making Py_IsInitialized() return false after Py_InitializeFromConfig().
+ //
+ // Instead, find the DSO that provides Py_IsInitialized via dladdr, then
+ // re-dlopen that exact path with RTLD_GLOBAL.
{
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",
+ void* sym = dlsym(RTLD_DEFAULT, "Py_IsInitialized");
+ if (sym && dladdr(sym, &di) && di.dli_fname) {
+ void* pyHandle =
+ dlopen(di.dli_fname, RTLD_NOW | RTLD_GLOBAL | RTLD_NOLOAD);
+ if (pyHandle) {
+ MOBase::log::debug(
+ "python: promoted '{}' to RTLD_GLOBAL via dladdr",
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 {
+ // Fallback: load by full path (not NOLOAD).
+ pyHandle = dlopen(di.dli_fname, RTLD_NOW | RTLD_GLOBAL);
+ if (pyHandle) {
+ MOBase::log::debug(
+ "python: loaded '{}' with RTLD_GLOBAL (fresh)",
+ di.dli_fname);
+ } else {
+ MOBase::log::warn(
+ "python: failed to promote '{}' to RTLD_GLOBAL: {}",
+ di.dli_fname, dlerror());
+ }
+ }
} else {
- fprintf(stderr, "[py-diag] Py_IsInitialized NOT in global scope\n");
- MOBase::log::warn("python: Py_IsInitialized NOT in global scope");
+ // Py_IsInitialized not yet in scope — libpython may not be loaded
+ // as a dependency yet. Try the compile-time name.
+#ifdef MO2_PYTHON_SHARED_LIBRARY
+ void* pyHandle =
+ dlopen(MO2_PYTHON_SHARED_LIBRARY, RTLD_NOW | RTLD_GLOBAL);
+ if (pyHandle) {
+ MOBase::log::debug(
+ "python: loaded '{}' with RTLD_GLOBAL (compile-time name)",
+ MO2_PYTHON_SHARED_LIBRARY);
+ } else {
+ MOBase::log::warn(
+ "python: failed to dlopen '{}': {}",
+ MO2_PYTHON_SHARED_LIBRARY, dlerror());
+ }
+#else
+ MOBase::log::warn(
+ "python: Py_IsInitialized not found in global scope and "
+ "no compile-time library name available");
+#endif
}
}
#endif
-#endif
// For portable/AppImage builds, set PYTHONHOME so the interpreter
// finds the bundled stdlib instead of looking at system paths.
@@ -221,8 +229,18 @@ namespace mo2::python {
{
PyConfig config;
PyConfig_InitPythonConfig(&config);
- // PYTHONHOME is already set via setenv; PyConfig reads it from env.
- PyStatus status = Py_InitializeFromConfig(&config);
+ // Set config.home directly (more reliable than env for embedded use).
+ std::wstring wHome = pythonHome.toStdWString();
+ PyStatus status = PyConfig_SetString(&config, &config.home, wHome.c_str());
+ if (PyStatus_Exception(status)) {
+ MOBase::log::error(
+ "python: PyConfig_SetString(home) failed: '{}'",
+ status.err_msg ? status.err_msg : "(no message)");
+ PyConfig_Clear(&config);
+ restorePythonEnv();
+ return false;
+ }
+ status = Py_InitializeFromConfig(&config);
PyConfig_Clear(&config);
if (PyStatus_Exception(status)) {
fprintf(stderr,
diff --git a/src/src/commandline.cpp b/src/src/commandline.cpp
index 4613350..121fb58 100644
--- a/src/src/commandline.cpp
+++ b/src/src/commandline.cpp
@@ -853,8 +853,7 @@ std::optional<int> ReloadPluginCommand::runPostOrganizer(OrganizerCore& core)
const QString name = QString::fromStdString(vm()["PLUGIN"].as<std::string>());
QString filepath =
- QDir(AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath()))
- .absoluteFilePath(name);
+ QDir(AppConfig::pluginsPath()).absoluteFilePath(name);
log::debug("reloading plugin from {}", filepath);
core.pluginContainer().reloadPlugin(filepath);
diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp
index 7aa6964..1eb5e36 100644
--- a/src/src/mainwindow.cpp
+++ b/src/src/mainwindow.cpp
@@ -544,12 +544,12 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore,
QApplication::instance()->installEventFilter(this);
- scheduleCheckForProblems();
- refreshExecutablesList();
- updatePinnedExecutables();
- resetActionIcons();
- resetButtonIcons();
- processUpdates();
+ scheduleCheckForProblems();
+ refreshExecutablesList();
+ updatePinnedExecutables();
+ resetActionIcons();
+ resetButtonIcons();
+ processUpdates();
ui->modList->updateModCount();
ui->espList->updatePluginCount();
@@ -573,8 +573,8 @@ void MainWindow::setupModList()
});
}
-void MainWindow::resetActionIcons()
-{
+void MainWindow::resetActionIcons()
+{
// this is a bit of a hack
//
// the .qss files have historically set qproperty-icon by id and these ids
@@ -636,38 +636,38 @@ void MainWindow::resetActionIcons()
}
}
- // update the button for the potentially new icon
- updateProblemsButton();
-}
-
-void MainWindow::resetButtonIcons()
-{
- // Some icon-only QPushButtons can lose their icons after stylesheet repolish on
- // Linux/AppImage. Re-apply explicit resource icons to keep these controls visible.
- ui->listOptionsBtn->setIcon(QIcon::fromTheme(
- "preferences-system", QIcon(":/MO/gui/settings")));
- ui->openFolderMenu->setIcon(
- QIcon::fromTheme("folder-open", QIcon(":/MO/gui/open_folder")));
- ui->restoreModsButton->setIcon(
- QIcon::fromTheme("edit-undo", QIcon(":/MO/gui/restore")));
- ui->saveModsButton->setIcon(
- QIcon::fromTheme("document-save", QIcon(":/MO/gui/backup")));
- ui->sortButton->setIcon(QIcon::fromTheme("view-sort-ascending", QIcon(":/MO/gui/sort")));
- ui->restoreButton->setIcon(
- QIcon::fromTheme("edit-undo", QIcon(":/MO/gui/restore")));
- ui->saveButton->setIcon(QIcon::fromTheme("document-save", QIcon(":/MO/gui/backup")));
-
- ui->listOptionsBtn->setIconSize(QSize(16, 16));
- ui->openFolderMenu->setIconSize(QSize(16, 16));
- ui->restoreModsButton->setIconSize(QSize(16, 16));
- ui->saveModsButton->setIconSize(QSize(16, 16));
- ui->sortButton->setIconSize(QSize(16, 16));
- ui->restoreButton->setIconSize(QSize(16, 16));
- ui->saveButton->setIconSize(QSize(16, 16));
-}
-
-MainWindow::~MainWindow()
-{
+ // update the button for the potentially new icon
+ updateProblemsButton();
+}
+
+void MainWindow::resetButtonIcons()
+{
+ // Some icon-only QPushButtons can lose their icons after stylesheet repolish on
+ // Linux/AppImage. Re-apply explicit resource icons to keep these controls visible.
+ ui->listOptionsBtn->setIcon(QIcon::fromTheme(
+ "preferences-system", QIcon(":/MO/gui/settings")));
+ ui->openFolderMenu->setIcon(
+ QIcon::fromTheme("folder-open", QIcon(":/MO/gui/open_folder")));
+ ui->restoreModsButton->setIcon(
+ QIcon::fromTheme("edit-undo", QIcon(":/MO/gui/restore")));
+ ui->saveModsButton->setIcon(
+ QIcon::fromTheme("document-save", QIcon(":/MO/gui/backup")));
+ ui->sortButton->setIcon(QIcon::fromTheme("view-sort-ascending", QIcon(":/MO/gui/sort")));
+ ui->restoreButton->setIcon(
+ QIcon::fromTheme("edit-undo", QIcon(":/MO/gui/restore")));
+ ui->saveButton->setIcon(QIcon::fromTheme("document-save", QIcon(":/MO/gui/backup")));
+
+ ui->listOptionsBtn->setIconSize(QSize(16, 16));
+ ui->openFolderMenu->setIconSize(QSize(16, 16));
+ ui->restoreModsButton->setIconSize(QSize(16, 16));
+ ui->saveModsButton->setIconSize(QSize(16, 16));
+ ui->sortButton->setIconSize(QSize(16, 16));
+ ui->restoreButton->setIconSize(QSize(16, 16));
+ ui->saveButton->setIconSize(QSize(16, 16));
+}
+
+MainWindow::~MainWindow()
+{
try {
cleanup();
@@ -739,11 +739,11 @@ void MainWindow::allowListResize()
ui->espList->header()->setStretchLastSection(true);
}
-void MainWindow::updateStyle(const QString&)
-{
- resetActionIcons();
- resetButtonIcons();
-}
+void MainWindow::updateStyle(const QString&)
+{
+ resetActionIcons();
+ resetButtonIcons();
+}
void MainWindow::resizeEvent(QResizeEvent* event)
{
@@ -1913,10 +1913,10 @@ bool MainWindow::refreshProfiles(bool selectProfile, QString newProfile)
return profileBox->count() > 1;
}
-void MainWindow::refreshExecutablesList()
-{
- QAbstractItemModel* model = ui->executablesListBox->model();
-
+void MainWindow::refreshExecutablesList()
+{
+ QAbstractItemModel* model = ui->executablesListBox->model();
+
auto add = [&](const QString& title, const QFileInfo& binary) {
QIcon icon;
if (!binary.fileName().isEmpty()) {
@@ -1932,36 +1932,36 @@ void MainWindow::refreshExecutablesList()
Qt::SizeHintRole);
};
- ui->executablesListBox->setEnabled(false);
- ui->executablesListBox->clear();
-
- add(tr("<Edit...>"), {});
-
- auto shouldSkipInLaunchDropdown = [](const Executable& exe) {
- const QString title = exe.title().trimmed();
- const QString lower = title.toLower();
- if (lower == QStringLiteral("proton") || lower == QStringLiteral("prefix") ||
- lower.startsWith(QStringLiteral("proton ")) ||
- lower.startsWith(QStringLiteral("prefix "))) {
- return true;
- }
-
- return false;
- };
-
- for (const auto& exe : *m_OrganizerCore.executablesList()) {
- if (exe.hide()) {
- continue;
- }
-
- if (shouldSkipInLaunchDropdown(exe)) {
- log::debug("Skipping internal executable entry '{}' from launch dropdown",
- exe.title());
- continue;
- }
-
- add(exe.title(), exe.binaryInfo());
- }
+ ui->executablesListBox->setEnabled(false);
+ ui->executablesListBox->clear();
+
+ add(tr("<Edit...>"), {});
+
+ auto shouldSkipInLaunchDropdown = [](const Executable& exe) {
+ const QString title = exe.title().trimmed();
+ const QString lower = title.toLower();
+ if (lower == QStringLiteral("proton") || lower == QStringLiteral("prefix") ||
+ lower.startsWith(QStringLiteral("proton ")) ||
+ lower.startsWith(QStringLiteral("prefix "))) {
+ return true;
+ }
+
+ return false;
+ };
+
+ for (const auto& exe : *m_OrganizerCore.executablesList()) {
+ if (exe.hide()) {
+ continue;
+ }
+
+ if (shouldSkipInLaunchDropdown(exe)) {
+ log::debug("Skipping internal executable entry '{}' from launch dropdown",
+ exe.title());
+ continue;
+ }
+
+ add(exe.title(), exe.binaryInfo());
+ }
if (ui->executablesListBox->count() == 1) {
// all executables are hidden, add an empty one to at least be able to
@@ -2692,9 +2692,7 @@ void MainWindow::openInstallFolder()
void MainWindow::openPluginsFolder()
{
- QString pluginsPath =
- AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath());
- shell::Explore(pluginsPath);
+ shell::Explore(AppConfig::pluginsPath());
}
void MainWindow::openStylesheetsFolder()
diff --git a/src/src/moapplication.cpp b/src/src/moapplication.cpp
index fd60745..3009a45 100644
--- a/src/src/moapplication.cpp
+++ b/src/src/moapplication.cpp
@@ -41,9 +41,9 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "shared/util.h"
#include "thread_utils.h"
#include "tutorialmanager.h"
-#include <QDebug>
-#include <QFile>
-#include <QPainter>
+#include <QDebug>
+#include <QFile>
+#include <QPainter>
#include <QProxyStyle>
#include <QSslSocket>
#include <QStringList>
@@ -51,10 +51,10 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QStyleOption>
#include <iplugingame.h>
#include <log.h>
-#include <report.h>
-#include <scopeguard.h>
-#include <utility.h>
-#include <cstdio>
+#include <report.h>
+#include <scopeguard.h>
+#include <utility.h>
+#include <cstdio>
#ifdef _WIN32
// see addDllsToPath() below
@@ -270,43 +270,43 @@ void MOApplication::firstTimeSetup(MOMultiProcess& multiProcess)
Qt::QueuedConnection);
}
-int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect)
-{
- TimeThis tt("MOApplication setup()");
- std::fprintf(stderr, "[setup-diag] setup() entered\n");
- std::fflush(stderr);
+int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect)
+{
+ TimeThis tt("MOApplication setup()");
+ std::fprintf(stderr, "[setup-diag] setup() entered\n");
+ std::fflush(stderr);
// makes plugin data path available to plugins, see
// IOrganizer::getPluginDataPath()
MOBase::details::setPluginDataPath(OrganizerCore::pluginDataPath());
// figuring out the current instance
- m_instance = getCurrentInstance(forceSelect);
- if (!m_instance) {
- std::fprintf(stderr, "[setup-diag] getCurrentInstance() returned null\n");
- std::fflush(stderr);
- return 1;
- }
- std::fprintf(stderr, "[setup-diag] instance dir='%s' ini='%s'\n",
- qUtf8Printable(m_instance->directory()),
- qUtf8Printable(m_instance->iniPath()));
- std::fflush(stderr);
+ m_instance = getCurrentInstance(forceSelect);
+ if (!m_instance) {
+ std::fprintf(stderr, "[setup-diag] getCurrentInstance() returned null\n");
+ std::fflush(stderr);
+ return 1;
+ }
+ std::fprintf(stderr, "[setup-diag] instance dir='%s' ini='%s'\n",
+ qUtf8Printable(m_instance->directory()),
+ qUtf8Printable(m_instance->iniPath()));
+ std::fflush(stderr);
// first time the data path is available, set the global property and log
// directory, then log a bunch of debug stuff
const QString dataPath = m_instance->directory();
setProperty("dataPath", dataPath);
- if (!setLogDirectory(dataPath)) {
- std::fprintf(stderr, "[setup-diag] setLogDirectory() failed for '%s'\n",
- qUtf8Printable(dataPath));
- std::fflush(stderr);
- reportError(tr("Failed to create log folder."));
- InstanceManager::singleton().clearCurrentInstance();
- return 1;
- }
- std::fprintf(stderr, "[setup-diag] setLogDirectory() ok\n");
- std::fflush(stderr);
+ if (!setLogDirectory(dataPath)) {
+ std::fprintf(stderr, "[setup-diag] setLogDirectory() failed for '%s'\n",
+ qUtf8Printable(dataPath));
+ std::fflush(stderr);
+ reportError(tr("Failed to create log folder."));
+ InstanceManager::singleton().clearCurrentInstance();
+ return 1;
+ }
+ std::fprintf(stderr, "[setup-diag] setLogDirectory() ok\n");
+ std::fflush(stderr);
#ifdef _WIN32
log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW()));
@@ -336,11 +336,11 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect)
}
// loading settings
- m_settings.reset(new Settings(m_instance->iniPath(), true));
- std::fprintf(stderr, "[setup-diag] settings loaded from '%s'\n",
- qUtf8Printable(m_instance->iniPath()));
- std::fflush(stderr);
- log::getDefault().setLevel(m_settings->diagnostics().logLevel());
+ m_settings.reset(new Settings(m_instance->iniPath(), true));
+ std::fprintf(stderr, "[setup-diag] settings loaded from '%s'\n",
+ qUtf8Printable(m_instance->iniPath()));
+ std::fflush(stderr);
+ log::getDefault().setLevel(m_settings->diagnostics().logLevel());
log::debug("using ini at '{}'", m_settings->filename());
OrganizerCore::setGlobalCoreDumpType(m_settings->diagnostics().coreDumpType());
@@ -375,30 +375,30 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect)
tt.start("MOApplication::doOneRun() OrganizerCore");
log::debug("initializing core");
- m_core.reset(new OrganizerCore(*m_settings));
- std::fprintf(stderr, "[setup-diag] organizer core constructed\n");
- std::fflush(stderr);
- if (!m_core->bootstrap()) {
- std::fprintf(stderr, "[setup-diag] organizer core bootstrap failed\n");
- std::fflush(stderr);
- reportError(tr("Failed to set up data paths."));
- InstanceManager::singleton().clearCurrentInstance();
- return 1;
- }
- std::fprintf(stderr, "[setup-diag] organizer core bootstrap ok\n");
- std::fflush(stderr);
+ m_core.reset(new OrganizerCore(*m_settings));
+ std::fprintf(stderr, "[setup-diag] organizer core constructed\n");
+ std::fflush(stderr);
+ if (!m_core->bootstrap()) {
+ std::fprintf(stderr, "[setup-diag] organizer core bootstrap failed\n");
+ std::fflush(stderr);
+ reportError(tr("Failed to set up data paths."));
+ InstanceManager::singleton().clearCurrentInstance();
+ return 1;
+ }
+ std::fprintf(stderr, "[setup-diag] organizer core bootstrap ok\n");
+ std::fflush(stderr);
// plugins
tt.start("MOApplication::doOneRun() plugins");
log::debug("initializing plugins");
- m_plugins = std::make_unique<PluginContainer>(m_core.get());
- std::fprintf(stderr, "[setup-diag] plugin container constructed, loading plugins...\n");
- std::fflush(stderr);
- m_plugins->loadPlugins();
- std::fprintf(stderr, "[setup-diag] plugin loading finished\n");
- std::fflush(stderr);
- log::debug("all plugins loaded");
+ m_plugins = std::make_unique<PluginContainer>(m_core.get());
+ std::fprintf(stderr, "[setup-diag] plugin container constructed, loading plugins...\n");
+ std::fflush(stderr);
+ m_plugins->loadPlugins();
+ std::fprintf(stderr, "[setup-diag] plugin loading finished\n");
+ std::fflush(stderr);
+ log::debug("all plugins loaded");
// instance
log::debug("entering setupInstanceLoop...");
@@ -456,14 +456,15 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect)
m_core->createDefaultProfile();
m_core->createOverwriteDirectories();
- log::info("using game plugin '{}' ('{}', variant {}, steam id '{}') at {}",
- m_instance->gamePlugin()->gameName(),
- m_instance->gamePlugin()->gameShortName(),
- (m_settings->game().edition().value_or("").isEmpty()
- ? "(none)"
- : *m_settings->game().edition()),
- m_instance->gamePlugin()->steamAPPId(),
- m_instance->gamePlugin()->gameDirectory().absolutePath());
+ {
+ const auto edition = m_settings->game().edition().value_or("");
+ const auto variant = edition.isEmpty() ? QString("Steam") : edition;
+ log::info("using game plugin '{}' ('{}', variant {}) at {}",
+ m_instance->gamePlugin()->gameName(),
+ m_instance->gamePlugin()->gameShortName(),
+ variant,
+ m_instance->gamePlugin()->gameDirectory().absolutePath());
+ }
CategoryFactory::instance().loadCategories();
m_core->updateExecutablesList();
diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp
index 630f23b..79ea90b 100644
--- a/src/src/organizercore.cpp
+++ b/src/src/organizercore.cpp
@@ -904,8 +904,7 @@ QString OrganizerCore::pluginDataPath()
// separate writable directory so mkdir() never hits a read-only FS.
return AppConfig::basePath() + "/plugin_data";
#else
- return AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath()) +
- "/data";
+ return AppConfig::pluginsPath() + "/data";
#endif
}
@@ -2193,23 +2192,23 @@ bool OrganizerCore::beforeRun(
WinePrefix prefix(prefixPathStr);
if (prefix.isValid()) {
const QString dataDirName = resolveWineDataDirName(managedGame());
- const QString appDataLocal = prefix.appdataLocal();
- const QString pluginsTargetDir =
- QDir(appDataLocal).filePath(dataDirName);
- const QString documentsDir =
- resolvePrefixGameDocumentsDir(prefix, dataDirName);
+ const QString appDataLocal = prefix.appdataLocal();
+ const QString pluginsTargetDir =
+ QDir(appDataLocal).filePath(dataDirName);
+ const QString documentsDir =
+ resolvePrefixGameDocumentsDir(prefix, dataDirName);
log::info("Wine prefix paths: AppData/Local plugins dir='{}', "
"Documents/My Games INI dir='{}'",
pluginsTargetDir, documentsDir);
- const auto localSavesFeature = gameFeatures().gameFeature<LocalSavegames>();
- const QString absoluteSaveDir =
- resolveAbsoluteSaveDir(prefix, managedGame(),
- localSavesFeature.get(), m_CurrentProfile);
- log::info("Wine prefix save target: '{}'", absoluteSaveDir);
-
- // Read plugin lines from profile's plugins.txt
- QFile pluginsFile(m_CurrentProfile->getPluginsFileName());
- if (pluginsFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ const auto localSavesFeature = gameFeatures().gameFeature<LocalSavegames>();
+ const QString absoluteSaveDir =
+ resolveAbsoluteSaveDir(prefix, managedGame(),
+ localSavesFeature.get(), m_CurrentProfile);
+ log::info("Wine prefix save target: '{}'", absoluteSaveDir);
+
+ // Read plugin lines from profile's plugins.txt
+ QFile pluginsFile(m_CurrentProfile->getPluginsFileName());
+ if (pluginsFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QStringList plugins;
QTextStream stream(&pluginsFile);
while (!stream.atEnd()) {
@@ -2219,17 +2218,17 @@ bool OrganizerCore::beforeRun(
}
}
pluginsFile.close();
-
- if (!plugins.isEmpty()) {
- log::info(
- "Deploying plugin list to '{}' and '{}' (load order to '{}')",
- QDir(pluginsTargetDir).filePath("Plugins.txt"),
- QDir(pluginsTargetDir).filePath("plugins.txt"),
- QDir(pluginsTargetDir).filePath("loadorder.txt"));
- if (prefix.deployPlugins(plugins, dataDirName)) {
- log::debug("Deployed {} plugins to prefix '{}' (dataDirName='{}')",
- plugins.size(), prefixPathStr, dataDirName);
- } else {
+
+ if (!plugins.isEmpty()) {
+ log::info(
+ "Deploying plugin list to '{}' and '{}' (load order to '{}')",
+ QDir(pluginsTargetDir).filePath("Plugins.txt"),
+ QDir(pluginsTargetDir).filePath("plugins.txt"),
+ QDir(pluginsTargetDir).filePath("loadorder.txt"));
+ if (prefix.deployPlugins(plugins, dataDirName)) {
+ log::debug("Deployed {} plugins to prefix '{}' (dataDirName='{}')",
+ plugins.size(), prefixPathStr, dataDirName);
+ } else {
log::error("Failed to deploy {} plugins to prefix '{}' "
"(dataDirName='{}')",
plugins.size(), prefixPathStr, dataDirName);
@@ -2244,18 +2243,18 @@ bool OrganizerCore::beforeRun(
const QString targetIniBase =
resolvePrefixGameDocumentsDir(prefix, dataDirName);
int deployedIniCount = 0;
- for (const QString& iniFile : managedGame()->iniFiles()) {
- const QString sourceIni =
- m_CurrentProfile->absoluteIniFilePath(iniFile);
- const QString targetIni =
- QDir(targetIniBase).filePath(QFileInfo(iniFile).fileName());
- log::info("INI deploy target: '{}' -> '{}' (exists={})", sourceIni,
- targetIni, QFileInfo::exists(sourceIni));
- if (QFileInfo::exists(sourceIni) &&
- prefix.deployProfileIni(sourceIni, targetIni)) {
- ++deployedIniCount;
- log::debug("Deployed profile INI '{}' -> '{}'", sourceIni, targetIni);
- }
+ for (const QString& iniFile : managedGame()->iniFiles()) {
+ const QString sourceIni =
+ m_CurrentProfile->absoluteIniFilePath(iniFile);
+ const QString targetIni =
+ QDir(targetIniBase).filePath(QFileInfo(iniFile).fileName());
+ log::info("INI deploy target: '{}' -> '{}' (exists={})", sourceIni,
+ targetIni, QFileInfo::exists(sourceIni));
+ if (QFileInfo::exists(sourceIni) &&
+ prefix.deployProfileIni(sourceIni, targetIni)) {
+ ++deployedIniCount;
+ log::debug("Deployed profile INI '{}' -> '{}'", sourceIni, targetIni);
+ }
}
if (deployedIniCount > 0) {
log::debug("Deployed {} profile INI files to prefix '{}'", deployedIniCount,
@@ -2267,15 +2266,15 @@ bool OrganizerCore::beforeRun(
managedGame()->documentsDirectory().absolutePath());
}
- if (m_CurrentProfile->localSavesEnabled()) {
- const QString profileSavesDir =
- QDir(m_CurrentProfile->absolutePath()).filePath("saves");
- log::info("Save deploy target: '{}' -> '{}'", profileSavesDir,
- absoluteSaveDir);
- if (!prefix.deployProfileSaves(profileSavesDir, absoluteSaveDir, true)) {
- log::warn("Failed to deploy profile saves from '{}' to prefix '{}'",
- profileSavesDir, prefixPathStr);
- } else {
+ if (m_CurrentProfile->localSavesEnabled()) {
+ const QString profileSavesDir =
+ QDir(m_CurrentProfile->absolutePath()).filePath("saves");
+ log::info("Save deploy target: '{}' -> '{}'", profileSavesDir,
+ absoluteSaveDir);
+ if (!prefix.deployProfileSaves(profileSavesDir, absoluteSaveDir, true)) {
+ log::warn("Failed to deploy profile saves from '{}' to prefix '{}'",
+ profileSavesDir, prefixPathStr);
+ } else {
log::debug("Deployed profile saves '{}' -> '{}' in prefix '{}'",
profileSavesDir, absoluteSaveDir, prefixPathStr);
}
@@ -2321,29 +2320,29 @@ void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode)
resolveAbsoluteSaveDir(prefix, managedGame(),
localSavesFeature.get(), m_CurrentProfile);
- if (m_CurrentProfile->localSavesEnabled()) {
- const QString profileSavesDir =
- QDir(m_CurrentProfile->absolutePath()).filePath("saves");
- log::info("Save sync target: '{}' <- '{}'", profileSavesDir,
- absoluteSaveDir);
- if (!prefix.syncSavesBack(profileSavesDir, absoluteSaveDir)) {
- log::warn("Failed to sync saves back from prefix '{}' to '{}'",
- prefixPathStr, profileSavesDir);
- }
- }
+ if (m_CurrentProfile->localSavesEnabled()) {
+ const QString profileSavesDir =
+ QDir(m_CurrentProfile->absolutePath()).filePath("saves");
+ log::info("Save sync target: '{}' <- '{}'", profileSavesDir,
+ absoluteSaveDir);
+ if (!prefix.syncSavesBack(profileSavesDir, absoluteSaveDir)) {
+ log::warn("Failed to sync saves back from prefix '{}' to '{}'",
+ prefixPathStr, profileSavesDir);
+ }
+ }
if (m_CurrentProfile->localSettingsEnabled()) {
const QString targetIniBase =
resolvePrefixGameDocumentsDir(prefix, dataDirName);
QList<QPair<QString, QString>> iniMappings;
for (const QString& iniFile : managedGame()->iniFiles()) {
- const QString profileIni =
- m_CurrentProfile->absoluteIniFilePath(iniFile);
- const QString targetIni =
- QDir(targetIniBase).filePath(QFileInfo(iniFile).fileName());
- iniMappings.append({profileIni, targetIni});
- log::info("INI sync target: '{}' <- '{}'", profileIni, targetIni);
- }
+ const QString profileIni =
+ m_CurrentProfile->absoluteIniFilePath(iniFile);
+ const QString targetIni =
+ QDir(targetIniBase).filePath(QFileInfo(iniFile).fileName());
+ iniMappings.append({profileIni, targetIni});
+ log::info("INI sync target: '{}' <- '{}'", profileIni, targetIni);
+ }
if (!iniMappings.isEmpty() &&
!prefix.syncProfileInisBack(iniMappings)) {
diff --git a/src/src/plugincontainer.cpp b/src/src/plugincontainer.cpp
index dad0117..e215c0a 100644
--- a/src/src/plugincontainer.cpp
+++ b/src/src/plugincontainer.cpp
@@ -4,12 +4,12 @@
#include "organizerproxy.h"
#include "report.h"
#include "shared/appconfig.h"
-#include <QAction>
-#include <QCoreApplication>
-#include <QDirIterator>
-#include <QMessageBox>
-#include <QToolButton>
-#include <cstdio>
+#include <QAction>
+#include <QCoreApplication>
+#include <QDirIterator>
+#include <QMessageBox>
+#include <QToolButton>
+#include <cstdio>
#include <boost/fusion/algorithm/iteration/for_each.hpp>
#include <boost/fusion/include/at_key.hpp>
#include <boost/fusion/include/for_each.hpp>
@@ -22,13 +22,13 @@ using namespace MOShared;
namespace bf = boost::fusion;
-#ifndef _WIN32
+#ifndef _WIN32
// Ensure the instance plugin directory contains symlinks to all bundled plugins.
// Real user files (not matching any bundled plugin name) are left untouched.
// Stale copies of bundled plugins are replaced with symlinks so that RPATH
// resolution works correctly (critical for Flatpak where libs live in /app/).
-static void ensureBundledPluginsLinked(const QString& bundledDir,
- const QString& instanceDir)
+static void ensureBundledPluginsLinked(const QString& bundledDir,
+ const QString& instanceDir)
{
QDir().mkpath(instanceDir);
QDirIterator it(bundledDir, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
@@ -54,16 +54,19 @@ static void ensureBundledPluginsLinked(const QString& bundledDir,
}
}
- QFile::link(it.filePath(), target);
+ if (!QFile::link(it.filePath(), target)) {
+ log::warn("ensureBundledPluginsLinked: failed to symlink '{}' -> '{}'",
+ it.filePath(), target);
+ }
}
-}
-#endif
-
-static void printPluginDiagToStderr(const QString& message)
-{
- std::fprintf(stderr, "[plugin-diag] %s\n", qUtf8Printable(message));
- std::fflush(stderr);
-}
+}
+#endif
+
+static void printPluginDiagToStderr(const QString& message)
+{
+ std::fprintf(stderr, "[plugin-diag] %s\n", qUtf8Printable(message));
+ std::fflush(stderr);
+}
// Welcome to the wonderful world of MO2 plugin management!
//
@@ -446,9 +449,7 @@ QStringList PluginContainer::pluginFileNames() const
std::vector<IPluginProxy*> proxyList = bf::at_key<IPluginProxy>(m_Plugins);
for (IPluginProxy* proxy : proxyList) {
QStringList proxiedPlugins = proxy->pluginList(
- m_PluginPath.isEmpty()
- ? (AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath()))
- : m_PluginPath);
+ m_PluginPath.isEmpty() ? AppConfig::pluginsPath() : m_PluginPath);
result.append(proxiedPlugins);
}
return result;
@@ -643,33 +644,31 @@ IPlugin* PluginContainer::registerPlugin(QObject* plugin, const QString& filepat
return preview;
}
}
- { // proxy plugins
- IPluginProxy* proxy = qobject_cast<IPluginProxy*>(plugin);
- if (initPlugin(proxy, pluginProxy, skipInit)) {
- bf::at_key<IPluginProxy>(m_Plugins).push_back(proxy);
- emit pluginRegistered(proxy);
-
- const QString pluginRoot =
- m_PluginPath.isEmpty()
- ? (AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath()))
- : m_PluginPath;
- QStringList filepaths = proxy->pluginList(pluginRoot);
- log::warn("proxy '{}' discovered {} proxied plugin candidate(s) in '{}'",
- proxy->name(), filepaths.size(),
- QDir::toNativeSeparators(pluginRoot));
- printPluginDiagToStderr(
- QString("proxy '%1' discovered %2 proxied plugin candidate(s) in '%3'")
- .arg(proxy->name())
- .arg(filepaths.size())
- .arg(QDir::toNativeSeparators(pluginRoot)));
- for (const QString& filepath : filepaths) {
- log::debug("proxy '{}' candidate: '{}'", proxy->name(),
- QDir::toNativeSeparators(filepath));
- loadProxied(filepath, proxy);
- }
- return proxy;
- }
- }
+ { // proxy plugins
+ IPluginProxy* proxy = qobject_cast<IPluginProxy*>(plugin);
+ if (initPlugin(proxy, pluginProxy, skipInit)) {
+ bf::at_key<IPluginProxy>(m_Plugins).push_back(proxy);
+ emit pluginRegistered(proxy);
+
+ const QString pluginRoot =
+ m_PluginPath.isEmpty() ? AppConfig::pluginsPath() : m_PluginPath;
+ QStringList filepaths = proxy->pluginList(pluginRoot);
+ log::warn("proxy '{}' discovered {} proxied plugin candidate(s) in '{}'",
+ proxy->name(), filepaths.size(),
+ QDir::toNativeSeparators(pluginRoot));
+ printPluginDiagToStderr(
+ QString("proxy '%1' discovered %2 proxied plugin candidate(s) in '%3'")
+ .arg(proxy->name())
+ .arg(filepaths.size())
+ .arg(QDir::toNativeSeparators(pluginRoot)));
+ for (const QString& filepath : filepaths) {
+ log::debug("proxy '{}' candidate: '{}'", proxy->name(),
+ QDir::toNativeSeparators(filepath));
+ loadProxied(filepath, proxy);
+ }
+ return proxy;
+ }
+ }
{ // dummy plugins
// only initialize these, no processing otherwise
@@ -853,70 +852,70 @@ void PluginContainer::startPluginsImpl(const std::vector<QObject*>& plugins) con
}
}
-std::vector<QObject*> PluginContainer::loadProxied(const QString& filepath,
- IPluginProxy* proxy)
-{
- std::vector<QObject*> proxiedPlugins;
-
- try {
- log::warn("loading proxied plugin candidate '{}' via proxy '{}'",
- QDir::toNativeSeparators(filepath),
- (proxy ? proxy->name() : QStringLiteral("<null>")));
- printPluginDiagToStderr(
- QString("loading proxied plugin candidate '%1' via proxy '%2'")
- .arg(QDir::toNativeSeparators(filepath))
- .arg(proxy ? proxy->name() : QStringLiteral("<null>")));
-
- // We get a list of matching plugins as proxies can return multiple plugins
- // per file and do not have a good way of supporting multiple inheritance.
- QList<QObject*> matchingPlugins = proxy->load(filepath);
- if (matchingPlugins.isEmpty()) {
- log::warn("no plugins were returned for proxied candidate '{}' via proxy '{}'",
- QDir::toNativeSeparators(filepath), proxy->name());
- printPluginDiagToStderr(
- QString("no plugins were returned for proxied candidate '%1' via proxy '%2'")
- .arg(QDir::toNativeSeparators(filepath))
- .arg(proxy->name()));
- }
-
- // We are going to group plugin by names and "fix" them later:
- std::map<QString, std::vector<IPlugin*>> proxiedByNames;
-
- for (QObject* proxiedPlugin : matchingPlugins) {
- if (proxiedPlugin == nullptr) {
- log::warn("proxy '{}' returned a null QObject for '{}'", proxy->name(),
- QDir::toNativeSeparators(filepath));
- printPluginDiagToStderr(
- QString("proxy '%1' returned a null QObject for '%2'")
- .arg(proxy->name())
- .arg(QDir::toNativeSeparators(filepath)));
- continue;
- }
-
- if (IPlugin* proxied = registerPlugin(proxiedPlugin, filepath, proxy); proxied) {
- log::debug("loaded plugin '{}@{}' from '{}' - [{}]", proxied->name(),
- proxied->version().canonicalString(),
- QFileInfo(filepath).fileName(),
- implementedInterfaces(proxied).join(", "));
-
- // Store the plugin for later:
- proxiedPlugins.push_back(proxiedPlugin);
- proxiedByNames[proxied->name()].push_back(proxied);
- } else {
- log::warn(
- "proxied candidate '{}' from proxy '{}' failed to register as an MO2 plugin",
- QDir::toNativeSeparators(filepath), proxy->name());
- printPluginDiagToStderr(
- QString("proxied candidate '%1' from proxy '%2' failed to register as an "
- "MO2 plugin")
- .arg(QDir::toNativeSeparators(filepath))
- .arg(proxy->name()));
- }
- }
+std::vector<QObject*> PluginContainer::loadProxied(const QString& filepath,
+ IPluginProxy* proxy)
+{
+ std::vector<QObject*> proxiedPlugins;
+
+ try {
+ log::warn("loading proxied plugin candidate '{}' via proxy '{}'",
+ QDir::toNativeSeparators(filepath),
+ (proxy ? proxy->name() : QStringLiteral("<null>")));
+ printPluginDiagToStderr(
+ QString("loading proxied plugin candidate '%1' via proxy '%2'")
+ .arg(QDir::toNativeSeparators(filepath))
+ .arg(proxy ? proxy->name() : QStringLiteral("<null>")));
+
+ // We get a list of matching plugins as proxies can return multiple plugins
+ // per file and do not have a good way of supporting multiple inheritance.
+ QList<QObject*> matchingPlugins = proxy->load(filepath);
+ if (matchingPlugins.isEmpty()) {
+ log::warn("no plugins were returned for proxied candidate '{}' via proxy '{}'",
+ QDir::toNativeSeparators(filepath), proxy->name());
+ printPluginDiagToStderr(
+ QString("no plugins were returned for proxied candidate '%1' via proxy '%2'")
+ .arg(QDir::toNativeSeparators(filepath))
+ .arg(proxy->name()));
+ }
+
+ // We are going to group plugin by names and "fix" them later:
+ std::map<QString, std::vector<IPlugin*>> proxiedByNames;
+
+ for (QObject* proxiedPlugin : matchingPlugins) {
+ if (proxiedPlugin == nullptr) {
+ log::warn("proxy '{}' returned a null QObject for '{}'", proxy->name(),
+ QDir::toNativeSeparators(filepath));
+ printPluginDiagToStderr(
+ QString("proxy '%1' returned a null QObject for '%2'")
+ .arg(proxy->name())
+ .arg(QDir::toNativeSeparators(filepath)));
+ continue;
+ }
+
+ if (IPlugin* proxied = registerPlugin(proxiedPlugin, filepath, proxy); proxied) {
+ log::debug("loaded plugin '{}@{}' from '{}' - [{}]", proxied->name(),
+ proxied->version().canonicalString(),
+ QFileInfo(filepath).fileName(),
+ implementedInterfaces(proxied).join(", "));
+
+ // Store the plugin for later:
+ proxiedPlugins.push_back(proxiedPlugin);
+ proxiedByNames[proxied->name()].push_back(proxied);
+ } else {
+ log::warn(
+ "proxied candidate '{}' from proxy '{}' failed to register as an MO2 plugin",
+ QDir::toNativeSeparators(filepath), proxy->name());
+ printPluginDiagToStderr(
+ QString("proxied candidate '%1' from proxy '%2' failed to register as an "
+ "MO2 plugin")
+ .arg(QDir::toNativeSeparators(filepath))
+ .arg(proxy->name()));
+ }
+ }
// Fake masters:
- for (auto& [name, proxiedPlugins] : proxiedByNames) {
- if (proxiedPlugins.size() > 1) {
+ for (auto& [name, proxiedPlugins] : proxiedByNames) {
+ if (proxiedPlugins.size() > 1) {
auto it = std::min_element(std::begin(proxiedPlugins), std::end(proxiedPlugins),
[&](auto const& lhs, auto const& rhs) {
return isBetterInterface(as_qobject(lhs),
@@ -928,43 +927,43 @@ std::vector<QObject*> PluginContainer::loadProxied(const QString& filepath,
m_Requirements.at(proxiedPlugin).setMaster(*it);
}
}
- }
- }
- log::warn("finished proxied candidate '{}' via proxy '{}': {} plugin(s) loaded",
- QDir::toNativeSeparators(filepath), proxy->name(),
- proxiedPlugins.size());
- printPluginDiagToStderr(
- QString("finished proxied candidate '%1' via proxy '%2': %3 plugin(s) loaded")
- .arg(QDir::toNativeSeparators(filepath))
- .arg(proxy->name())
- .arg(proxiedPlugins.size()));
- } catch (const std::exception& e) {
- log::error("failed to initialize proxied candidate '{}' via proxy '{}': {}",
- QDir::toNativeSeparators(filepath),
- (proxy ? proxy->name() : QStringLiteral("<null>")), e.what());
- printPluginDiagToStderr(
- QString("failed to initialize proxied candidate '%1' via proxy '%2': %3")
- .arg(QDir::toNativeSeparators(filepath))
- .arg(proxy ? proxy->name() : QStringLiteral("<null>"))
- .arg(e.what()));
- reportError(
- QObject::tr("failed to initialize plugin %1: %2").arg(filepath).arg(e.what()));
- } catch (...) {
- log::error("failed to initialize proxied candidate '{}' via proxy '{}': "
- "unknown exception",
- QDir::toNativeSeparators(filepath),
- (proxy ? proxy->name() : QStringLiteral("<null>")));
- printPluginDiagToStderr(
- QString("failed to initialize proxied candidate '%1' via proxy '%2': unknown "
- "exception")
- .arg(QDir::toNativeSeparators(filepath))
- .arg(proxy ? proxy->name() : QStringLiteral("<null>")));
- reportError(QObject::tr("failed to initialize plugin %1: unknown exception")
- .arg(filepath));
- }
-
- return proxiedPlugins;
-}
+ }
+ }
+ log::warn("finished proxied candidate '{}' via proxy '{}': {} plugin(s) loaded",
+ QDir::toNativeSeparators(filepath), proxy->name(),
+ proxiedPlugins.size());
+ printPluginDiagToStderr(
+ QString("finished proxied candidate '%1' via proxy '%2': %3 plugin(s) loaded")
+ .arg(QDir::toNativeSeparators(filepath))
+ .arg(proxy->name())
+ .arg(proxiedPlugins.size()));
+ } catch (const std::exception& e) {
+ log::error("failed to initialize proxied candidate '{}' via proxy '{}': {}",
+ QDir::toNativeSeparators(filepath),
+ (proxy ? proxy->name() : QStringLiteral("<null>")), e.what());
+ printPluginDiagToStderr(
+ QString("failed to initialize proxied candidate '%1' via proxy '%2': %3")
+ .arg(QDir::toNativeSeparators(filepath))
+ .arg(proxy ? proxy->name() : QStringLiteral("<null>"))
+ .arg(e.what()));
+ reportError(
+ QObject::tr("failed to initialize plugin %1: %2").arg(filepath).arg(e.what()));
+ } catch (...) {
+ log::error("failed to initialize proxied candidate '{}' via proxy '{}': "
+ "unknown exception",
+ QDir::toNativeSeparators(filepath),
+ (proxy ? proxy->name() : QStringLiteral("<null>")));
+ printPluginDiagToStderr(
+ QString("failed to initialize proxied candidate '%1' via proxy '%2': unknown "
+ "exception")
+ .arg(QDir::toNativeSeparators(filepath))
+ .arg(proxy ? proxy->name() : QStringLiteral("<null>")));
+ reportError(QObject::tr("failed to initialize plugin %1: unknown exception")
+ .arg(filepath));
+ }
+
+ return proxiedPlugins;
+}
QObject* PluginContainer::loadQtPlugin(const QString& filepath)
{
@@ -1033,9 +1032,7 @@ void PluginContainer::loadPlugin(QString const& filepath)
// We need to check if this can be handled by a proxy.
for (auto* proxy : this->plugins<IPluginProxy>()) {
auto filepaths = proxy->pluginList(
- m_PluginPath.isEmpty()
- ? (AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath()))
- : m_PluginPath);
+ m_PluginPath.isEmpty() ? AppConfig::pluginsPath() : m_PluginPath);
if (filepaths.contains(filepath)) {
plugins = loadProxied(filepath, proxy);
break;
@@ -1247,14 +1244,13 @@ void PluginContainer::loadPlugins()
loadCheck.close();
}
- if (!loadCheck.open(QIODevice::WriteOnly)) {
- log::warn("failed to open loadcheck file for writing '{}'",
- QDir::toNativeSeparators(loadCheck.fileName()));
- }
+ if (!loadCheck.open(QIODevice::WriteOnly)) {
+ log::warn("failed to open loadcheck file for writing '{}'",
+ QDir::toNativeSeparators(loadCheck.fileName()));
+ }
}
- QString pluginPath =
- AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath());
+ QString pluginPath = AppConfig::pluginsPath();
#ifndef _WIN32
// Per-instance plugin directory: symlink bundled plugins, then load from there.
@@ -1334,15 +1330,15 @@ void PluginContainer::loadPlugins()
}
log::warn("user skipped plugin '{}', remembering in loadcheck", skipPlugin);
- if (loadCheck.open(QIODevice::WriteOnly)) {
- loadCheck.write(skipPlugin.toUtf8());
- loadCheck.write("\n");
- loadCheck.flush();
- } else {
- log::warn("failed to persist skipped plugin to '{}'",
- QDir::toNativeSeparators(loadCheck.fileName()));
- }
- }
+ if (loadCheck.open(QIODevice::WriteOnly)) {
+ loadCheck.write(skipPlugin.toUtf8());
+ loadCheck.write("\n");
+ loadCheck.flush();
+ } else {
+ log::warn("failed to persist skipped plugin to '{}'",
+ QDir::toNativeSeparators(loadCheck.fileName()));
+ }
+ }
bf::at_key<IPluginDiagnose>(m_Plugins).push_back(this);
diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp
index e7354f3..892fd79 100644
--- a/src/src/processrunner.cpp
+++ b/src/src/processrunner.cpp
@@ -22,43 +22,45 @@
#include <dirent.h>
#include <fstream>
#include <signal.h>
+#include <sys/wait.h>
#include <unordered_map>
#include <unordered_set>
-#include <sys/wait.h>
+
#endif
using namespace MOBase;
-void adjustForVirtualized(const IPluginGame* game, spawn::SpawnParameters& sp,
- const Settings& settings)
-{
+void adjustForVirtualized(const IPluginGame *game, spawn::SpawnParameters &sp,
+ const Settings &settings) {
const QString modsPath = settings.paths().mods();
// Check if this a request with either an executable or a working directory
// under our mods folder then will start the process in a virtualized
// "environment" with the appropriate paths fixed:
// (i.e. mods\FNIS\path\exe => game\data\path\exe)
- QString cwdPath = sp.currentDirectory.absolutePath();
+ QString cwdPath = sp.currentDirectory.absolutePath();
QString trailedModsPath = modsPath;
if (!trailedModsPath.endsWith('/')) {
trailedModsPath = trailedModsPath + '/';
}
- bool virtualizedCwd = cwdPath.startsWith(trailedModsPath, Qt::CaseInsensitive);
- QString binPath = sp.binary.absoluteFilePath();
- bool virtualizedBin = binPath.startsWith(trailedModsPath, Qt::CaseInsensitive);
+ bool virtualizedCwd =
+ cwdPath.startsWith(trailedModsPath, Qt::CaseInsensitive);
+ QString binPath = sp.binary.absoluteFilePath();
+ bool virtualizedBin =
+ binPath.startsWith(trailedModsPath, Qt::CaseInsensitive);
if (virtualizedCwd || virtualizedBin) {
if (virtualizedCwd) {
- int cwdOffset = cwdPath.indexOf('/', trailedModsPath.length());
+ int cwdOffset = cwdPath.indexOf('/', trailedModsPath.length());
QString adjustedCwd = cwdPath.mid(cwdOffset, -1);
- cwdPath = game->dataDirectory().absolutePath();
+ cwdPath = game->dataDirectory().absolutePath();
if (cwdOffset >= 0)
cwdPath += adjustedCwd;
}
if (virtualizedBin) {
- int binOffset = binPath.indexOf('/', trailedModsPath.length());
+ int binOffset = binPath.indexOf('/', trailedModsPath.length());
QString adjustedBin = binPath.mid(binOffset, -1);
- binPath = game->dataDirectory().absolutePath();
+ binPath = game->dataDirectory().absolutePath();
if (binOffset >= 0)
binPath += adjustedBin;
}
@@ -69,7 +71,7 @@ void adjustForVirtualized(const IPluginGame* game, spawn::SpawnParameters& sp,
.arg(QDir::toNativeSeparators(cwdPath),
QDir::toNativeSeparators(binPath), sp.arguments);
- sp.binary = QFileInfo(QCoreApplication::applicationFilePath());
+ sp.binary = QFileInfo(QCoreApplication::applicationFilePath());
sp.arguments = cmdline;
sp.currentDirectory.setPath(QCoreApplication::applicationDirPath());
#else
@@ -97,8 +99,7 @@ void adjustForVirtualized(const IPluginGame* game, spawn::SpawnParameters& sp,
}
#ifdef _WIN32
-std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid)
-{
+std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid) {
if (handle == INVALID_HANDLE_VALUE) {
return ProcessRunner::Error;
}
@@ -116,7 +117,7 @@ std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid)
return {};
}
- case WAIT_FAILED: // fall-through
+ case WAIT_FAILED: // fall-through
default: {
// error
const auto e = ::GetLastError();
@@ -126,23 +127,16 @@ std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid)
}
}
#else
-std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid)
-{
+std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid) {
Q_UNUSED(handle);
Q_UNUSED(pid);
return ProcessRunner::Completed;
}
#endif
-enum class Interest
-{
- None = 0,
- Weak,
- Strong
-};
+enum class Interest { None = 0, Weak, Strong };
-QString toString(Interest i)
-{
+QString toString(Interest i) {
switch (i) {
case Interest::Weak:
return "weak";
@@ -150,22 +144,20 @@ QString toString(Interest i)
case Interest::Strong:
return "strong";
- case Interest::None: // fall-through
+ case Interest::None: // fall-through
default:
return "no";
}
}
-struct InterestingProcess
-{
+struct InterestingProcess {
env::Process p;
Interest interest = Interest::None;
env::HandlePtr handle;
};
-InterestingProcess findRandomProcess(const env::Process& root)
-{
- for (auto&& c : root.children()) {
+InterestingProcess findRandomProcess(const env::Process &root) {
+ for (auto &&c : root.children()) {
env::HandlePtr h = c.openHandleForWait();
if (h) {
return {c, Interest::Weak, std::move(h)};
@@ -183,18 +175,18 @@ InterestingProcess findRandomProcess(const env::Process& root)
// returns a process that's in the hidden list, or the top-level process if
// they're all hidden; returns an invalid process if the list is empty
//
-InterestingProcess findInterestingProcessInTrees(const env::Process& root)
-{
+InterestingProcess findInterestingProcessInTrees(const env::Process &root) {
// Certain process names we wish to "hide" for aesthetic reason:
static const std::vector<QString> hiddenList = {
- QFileInfo(QCoreApplication::applicationFilePath()).fileName(), "conhost.exe"};
+ QFileInfo(QCoreApplication::applicationFilePath()).fileName(),
+ "conhost.exe"};
if (root.children().empty()) {
return {};
}
- auto isHidden = [&](auto&& p) {
- for (auto& h : hiddenList) {
+ auto isHidden = [&](auto &&p) {
+ for (auto &h : hiddenList) {
if (p.name().contains(h, Qt::CaseInsensitive)) {
return true;
}
@@ -203,7 +195,7 @@ InterestingProcess findInterestingProcessInTrees(const env::Process& root)
return false;
};
- for (auto&& p : root.children()) {
+ for (auto &&p : root.children()) {
if (!isHidden(p)) {
env::HandlePtr h = p.openHandleForWait();
if (h) {
@@ -221,21 +213,19 @@ InterestingProcess findInterestingProcessInTrees(const env::Process& root)
return findRandomProcess(root);
}
-void dump(const env::Process& p, int indent)
-{
- log::debug("{}{}, pid={}, ppid={}", std::string(indent * 4, ' '), p.name(), p.pid(),
- p.ppid());
+void dump(const env::Process &p, int indent) {
+ log::debug("{}{}, pid={}, ppid={}", std::string(indent * 4, ' '), p.name(),
+ p.pid(), p.ppid());
- for (auto&& c : p.children()) {
+ for (auto &&c : p.children()) {
dump(c, indent + 1);
}
}
-void dump(const env::Process& root)
-{
+void dump(const env::Process &root) {
log::debug("process tree:");
- for (auto&& p : root.children()) {
+ for (auto &&p : root.children()) {
dump(p, 1);
}
}
@@ -243,8 +233,7 @@ void dump(const env::Process& root)
#ifdef _WIN32
// gets the most interesting process in the list
//
-InterestingProcess getInterestingProcess(HANDLE job)
-{
+InterestingProcess getInterestingProcess(HANDLE job) {
env::Process root = env::getProcessTree(job);
if (root.children().empty()) {
log::debug("nothing to wait for");
@@ -270,10 +259,9 @@ const std::chrono::milliseconds Infinite(-1);
// waits for completion, times out after `wait` if not Infinite
//
std::optional<ProcessRunner::Results> timedWait(HANDLE handle, DWORD pid,
- UILocker::Session* ls,
+ UILocker::Session *ls,
std::chrono::milliseconds wait,
- std::atomic<bool>& interrupt)
-{
+ std::atomic<bool> &interrupt) {
using namespace std::chrono;
high_resolution_clock::time_point start;
@@ -310,7 +298,7 @@ std::optional<ProcessRunner::Results> timedWait(HANDLE handle, DWORD pid,
return ProcessRunner::Cancelled;
}
- case UILocker::NoResult: // fall-through
+ case UILocker::NoResult: // fall-through
default: {
// shouldn't happen
log::debug("unexpected result {} while waiting for {}",
@@ -335,9 +323,9 @@ std::optional<ProcessRunner::Results> timedWait(HANDLE handle, DWORD pid,
return ProcessRunner::ForceUnlocked;
}
-ProcessRunner::Results waitForProcessesThreadImpl(HANDLE job, UILocker::Session* ls,
- std::atomic<bool>& interrupt)
-{
+ProcessRunner::Results
+waitForProcessesThreadImpl(HANDLE job, UILocker::Session *ls,
+ std::atomic<bool> &interrupt) {
using namespace std::chrono;
DWORD currentPID = 0;
@@ -395,9 +383,9 @@ ProcessRunner::Results waitForProcessesThreadImpl(HANDLE job, UILocker::Session*
return ProcessRunner::ForceUnlocked;
}
-void waitForProcessesThread(ProcessRunner::Results& result, HANDLE job,
- UILocker::Session* ls, std::atomic<bool>& interrupt)
-{
+void waitForProcessesThread(ProcessRunner::Results &result, HANDLE job,
+ UILocker::Session *ls,
+ std::atomic<bool> &interrupt) {
result = waitForProcessesThreadImpl(job, ls, interrupt);
// the session can be null when running shortcuts with locking disabled
@@ -406,9 +394,9 @@ void waitForProcessesThread(ProcessRunner::Results& result, HANDLE job,
}
}
-ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProcesses,
- UILocker::Session* ls)
-{
+ProcessRunner::Results
+waitForProcesses(const std::vector<HANDLE> &initialProcesses,
+ UILocker::Session *ls) {
if (initialProcesses.empty()) {
// nothing to wait for
return ProcessRunner::Completed;
@@ -428,7 +416,7 @@ ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProces
bool oneWorked = false;
- for (auto&& h : initialProcesses) {
+ for (auto &&h : initialProcesses) {
if (::AssignProcessToJobObject(job.get(), h)) {
oneWorked = true;
} else {
@@ -457,13 +445,11 @@ ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProces
auto results = ProcessRunner::Running;
std::atomic<bool> interrupt(false);
- auto* t = QThread::create(waitForProcessesThread, std::ref(results), monitor, ls,
- std::ref(interrupt));
+ auto *t = QThread::create(waitForProcessesThread, std::ref(results), monitor,
+ ls, std::ref(interrupt));
QEventLoop events;
- QObject::connect(t, &QThread::finished, [&] {
- events.quit();
- });
+ QObject::connect(t, &QThread::finished, [&] { events.quit(); });
t->start();
events.exec();
@@ -479,8 +465,7 @@ ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProces
}
ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode,
- UILocker::Session* ls)
-{
+ UILocker::Session *ls) {
std::vector<HANDLE> processes = {initialProcess};
const auto r = waitForProcesses(processes, ls);
@@ -489,7 +474,8 @@ ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode,
if (exitCode && r != ProcessRunner::Running) {
if (!::GetExitCodeProcess(initialProcess, exitCode)) {
const auto e = ::GetLastError();
- log::warn("failed to get exit code of process, {}", formatSystemMessage(e));
+ log::warn("failed to get exit code of process, {}",
+ formatSystemMessage(e));
}
}
@@ -498,13 +484,11 @@ ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode,
#else // !_WIN32
-pid_t handleToPid(HANDLE h)
-{
+pid_t handleToPid(HANDLE h) {
return static_cast<pid_t>(reinterpret_cast<intptr_t>(h));
}
-QString readProcComm(pid_t pid)
-{
+QString readProcComm(pid_t pid) {
QFile f(QString("/proc/%1/comm").arg(pid));
if (!f.open(QIODevice::ReadOnly)) {
return {};
@@ -513,21 +497,20 @@ QString readProcComm(pid_t pid)
return QString::fromUtf8(f.readAll()).trimmed();
}
-std::unordered_map<pid_t, std::vector<pid_t>> buildProcChildrenMap()
-{
+std::unordered_map<pid_t, std::vector<pid_t>> buildProcChildrenMap() {
std::unordered_map<pid_t, std::vector<pid_t>> children;
- DIR* proc = opendir("/proc");
+ DIR *proc = opendir("/proc");
if (!proc) {
return children;
}
- struct dirent* entry = nullptr;
+ struct dirent *entry = nullptr;
while ((entry = readdir(proc)) != nullptr) {
if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) {
continue;
}
- const char* name = entry->d_name;
+ const char *name = entry->d_name;
if (*name == '\0' || !std::isdigit(static_cast<unsigned char>(*name))) {
continue;
}
@@ -556,9 +539,8 @@ std::unordered_map<pid_t, std::vector<pid_t>> buildProcChildrenMap()
return children;
}
-std::unordered_set<pid_t>
-collectDescendants(pid_t root, const std::unordered_map<pid_t, std::vector<pid_t>>& children)
-{
+std::unordered_set<pid_t> collectDescendants(
+ pid_t root, const std::unordered_map<pid_t, std::vector<pid_t>> &children) {
std::unordered_set<pid_t> out;
std::deque<pid_t> q;
q.push_back(root);
@@ -582,8 +564,8 @@ collectDescendants(pid_t root, const std::unordered_map<pid_t, std::vector<pid_t
return out;
}
-QStringList buildExpectedExecutables(const QFileInfo& binary, const QString& arguments)
-{
+QStringList buildExpectedExecutables(const QFileInfo &binary,
+ const QString &arguments) {
QStringList expected;
auto addName = [&](QString name) {
name = name.trimmed().toLower();
@@ -595,7 +577,7 @@ QStringList buildExpectedExecutables(const QFileInfo& binary, const QString& arg
addName(binary.fileName());
const auto args = QProcess::splitCommand(arguments);
- for (const QString& arg : args) {
+ for (const QString &arg : args) {
const QFileInfo fi(arg);
const QString base = fi.fileName();
if (base.endsWith(".exe", Qt::CaseInsensitive)) {
@@ -603,17 +585,18 @@ QStringList buildExpectedExecutables(const QFileInfo& binary, const QString& arg
}
}
+ log::debug("buildExpectedExecutables: returning [{}]",
+ expected.join(", ").toStdString());
return expected;
}
-pid_t findTrackedProcess(pid_t rootPid, const QStringList& expected,
- QString* trackedNameOut)
-{
+pid_t findTrackedProcess(pid_t rootPid, const QStringList &expected,
+ QString *trackedNameOut) {
if (expected.isEmpty()) {
return 0;
}
- const auto children = buildProcChildrenMap();
+ const auto children = buildProcChildrenMap();
const auto descendants = collectDescendants(rootPid, children);
if (descendants.empty()) {
return 0;
@@ -636,12 +619,16 @@ pid_t findTrackedProcess(pid_t rootPid, const QStringList& expected,
if (best > 0 && trackedNameOut) {
*trackedNameOut = bestName;
+ log::debug("findTrackedProcess: found best={}, name={}", best,
+ bestName.toStdString());
+ } else {
+ log::debug("findTrackedProcess: no tracked process found for rootPid={}",
+ rootPid);
}
return best;
}
-DWORD exitCodeFromWaitStatus(int status)
-{
+DWORD exitCodeFromWaitStatus(int status) {
if (WIFEXITED(status)) {
return static_cast<DWORD>(WEXITSTATUS(status));
}
@@ -653,9 +640,9 @@ DWORD exitCodeFromWaitStatus(int status)
return 0;
}
-ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session* ls,
- const QStringList& expected)
-{
+ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode,
+ UILocker::Session *ls,
+ const QStringList &expected) {
if (pid <= 0) {
return ProcessRunner::Error;
}
@@ -665,10 +652,9 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session
// which works for any process owned by the same user.
bool useKillPoll = false;
{
- int status = 0;
+ int status = 0;
const pid_t probe = ::waitpid(pid, &status, WNOHANG);
if (probe == pid) {
- // Already exited before we even started waiting
if (exitCode != nullptr) {
*exitCode = exitCodeFromWaitStatus(status);
}
@@ -686,13 +672,13 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session
while (true) {
QString trackedName;
- pid_t displayPid = pid;
- QString displayName = readProcComm(pid);
- const pid_t tracked = findTrackedProcess(pid, expected, &trackedName);
+ pid_t displayPid = pid;
+ QString displayName = readProcComm(pid);
+ const pid_t tracked = findTrackedProcess(pid, expected, &trackedName);
if (tracked > 0) {
seenTrackedProcess = true;
- displayPid = tracked;
- displayName = trackedName;
+ displayPid = tracked;
+ displayName = trackedName;
} else if (seenTrackedProcess) {
if (exitCode != nullptr) {
*exitCode = 0;
@@ -702,7 +688,8 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session
}
if (ls != nullptr) {
- ls->setInfo(static_cast<DWORD>(std::max<pid_t>(0, displayPid)), displayName);
+ ls->setInfo(static_cast<DWORD>(std::max<pid_t>(0, displayPid)),
+ displayName);
}
if (useKillPoll) {
@@ -716,13 +703,13 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session
return ProcessRunner::Completed;
}
// EPERM means the process exists but we can't signal it; keep waiting
- if (errno != EPERM) {
+ else if (errno != EPERM) {
log::error("failed checking process {}, errno={}", pid, errno);
return ProcessRunner::Error;
}
}
} else {
- int status = 0;
+ int status = 0;
const pid_t waitResult = ::waitpid(pid, &status, WNOHANG);
if (waitResult == pid) {
@@ -776,23 +763,21 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session
}
ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode,
- UILocker::Session* ls,
- const QStringList& expected)
-{
+ UILocker::Session *ls,
+ const QStringList &expected) {
return waitForPid(handleToPid(initialProcess), exitCode, ls, expected);
}
-ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProcesses,
- UILocker::Session* ls,
- const QStringList& expected)
-{
+ProcessRunner::Results
+waitForProcesses(const std::vector<HANDLE> &initialProcesses,
+ UILocker::Session *ls, const QStringList &expected) {
if (initialProcesses.empty()) {
return ProcessRunner::Completed;
}
for (HANDLE h : initialProcesses) {
DWORD ignored = 0;
- const auto r = waitForPid(handleToPid(h), &ignored, ls, expected);
+ const auto r = waitForPid(handleToPid(h), &ignored, ls, expected);
if (r != ProcessRunner::Completed) {
return r;
}
@@ -803,16 +788,14 @@ ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProces
#endif // _WIN32
-ProcessRunner::ProcessRunner(OrganizerCore& core, IUserInterface* ui)
- : m_core(core), m_ui(ui), m_lockReason(UILocker::NoReason), m_waitFlags(NoFlags),
- m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1)
-{
+ProcessRunner::ProcessRunner(OrganizerCore &core, IUserInterface *ui)
+ : m_core(core), m_ui(ui), m_lockReason(UILocker::NoReason),
+ m_waitFlags(NoFlags), m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1) {
// all processes started in ProcessRunner are hooked by default
setHooked(true);
}
-ProcessRunner& ProcessRunner::setBinary(const QFileInfo& binary)
-{
+ProcessRunner &ProcessRunner::setBinary(const QFileInfo &binary) {
#ifndef _WIN32
m_sp.binary = QFileInfo(MOBase::normalizePathForHost(binary.filePath()));
#else
@@ -821,14 +804,12 @@ ProcessRunner& ProcessRunner::setBinary(const QFileInfo& binary)
return *this;
}
-ProcessRunner& ProcessRunner::setArguments(const QString& arguments)
-{
+ProcessRunner &ProcessRunner::setArguments(const QString &arguments) {
m_sp.arguments = arguments;
return *this;
}
-ProcessRunner& ProcessRunner::setCurrentDirectory(const QDir& directory)
-{
+ProcessRunner &ProcessRunner::setCurrentDirectory(const QDir &directory) {
#ifndef _WIN32
m_sp.currentDirectory.setPath(MOBase::normalizePathForHost(directory.path()));
#else
@@ -837,37 +818,35 @@ ProcessRunner& ProcessRunner::setCurrentDirectory(const QDir& directory)
return *this;
}
-ProcessRunner& ProcessRunner::setSteamID(const QString& steamID)
-{
+ProcessRunner &ProcessRunner::setSteamID(const QString &steamID) {
m_sp.steamAppID = steamID;
return *this;
}
-ProcessRunner& ProcessRunner::setCustomOverwrite(const QString& customOverwrite)
-{
+ProcessRunner &
+ProcessRunner::setCustomOverwrite(const QString &customOverwrite) {
m_customOverwrite = customOverwrite;
return *this;
}
-ProcessRunner& ProcessRunner::setForcedLibraries(const ForcedLibraries& forcedLibraries)
-{
+ProcessRunner &
+ProcessRunner::setForcedLibraries(const ForcedLibraries &forcedLibraries) {
m_forcedLibraries = forcedLibraries;
return *this;
}
-ProcessRunner& ProcessRunner::setProfileName(const QString& profileName)
-{
+ProcessRunner &ProcessRunner::setProfileName(const QString &profileName) {
m_profileName = profileName;
return *this;
}
-ProcessRunner& ProcessRunner::setWaitForCompletion(WaitFlags flags,
- UILocker::Reasons reason)
-{
- m_waitFlags = flags;
+ProcessRunner &ProcessRunner::setWaitForCompletion(WaitFlags flags,
+ UILocker::Reasons reason) {
+ m_waitFlags = flags;
m_lockReason = reason;
- if (m_waitFlags.testFlag(WaitForRefresh) && !m_waitFlags.testFlag(TriggerRefresh)) {
+ if (m_waitFlags.testFlag(WaitForRefresh) &&
+ !m_waitFlags.testFlag(TriggerRefresh)) {
log::warn("process runner: WaitForRefresh without TriggerRefresh "
"makes no sense, will be ignored");
}
@@ -875,14 +854,13 @@ ProcessRunner& ProcessRunner::setWaitForCompletion(WaitFlags flags,
return *this;
}
-ProcessRunner& ProcessRunner::setHooked(bool b)
-{
+ProcessRunner &ProcessRunner::setHooked(bool b) {
m_sp.hooked = b;
return *this;
}
-ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targetInfo)
-{
+ProcessRunner &ProcessRunner::setFromFile(QWidget *parent,
+ const QFileInfo &targetInfo) {
if (!parent && m_ui) {
parent = m_ui->mainWindow();
}
@@ -900,7 +878,7 @@ ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targ
break;
}
- case spawn::FileExecutionTypes::Other: // fall-through
+ case spawn::FileExecutionTypes::Other: // fall-through
default: {
m_shellOpen = targetInfo;
setHooked(false);
@@ -911,8 +889,7 @@ ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targ
return *this;
}
-ProcessRunner& ProcessRunner::setFromExecutable(const Executable& exe)
-{
+ProcessRunner &ProcessRunner::setFromExecutable(const Executable &exe) {
const auto profile = m_core.currentProfile();
if (!profile) {
throw MyException(QObject::tr("No profile set"));
@@ -941,33 +918,33 @@ ProcessRunner& ProcessRunner::setFromExecutable(const Executable& exe)
return *this;
}
-ProcessRunner& ProcessRunner::setFromShortcut(const MOShortcut& shortcut)
-{
+ProcessRunner &ProcessRunner::setFromShortcut(const MOShortcut &shortcut) {
const auto currentInstance = InstanceManager::singleton().currentInstance();
if (currentInstance) {
if (shortcut.hasInstance() && !shortcut.isForInstance(*currentInstance)) {
- MOBase::reportError(
- QObject::tr(
- "This shortcut is for instance '%1' but Mod Organizer is currently "
- "running for '%2'. Exit Mod Organizer before running the shortcut or "
- "change the active instance.")
- .arg(shortcut.instanceDisplayName())
- .arg(currentInstance->displayName()));
+ MOBase::reportError(QObject::tr("This shortcut is for instance '%1' but "
+ "Mod Organizer is currently "
+ "running for '%2'. Exit Mod Organizer "
+ "before running the shortcut or "
+ "change the active instance.")
+ .arg(shortcut.instanceDisplayName())
+ .arg(currentInstance->displayName()));
throw std::exception();
}
}
- const auto* exes = m_core.executablesList();
- const auto exe = exes->find(shortcut.executableName());
+ const auto *exes = m_core.executablesList();
+ const auto exe = exes->find(shortcut.executableName());
if (exe != exes->end()) {
setFromExecutable(*exe);
} else {
- MOBase::reportError(QObject::tr("Executable '%1' does not exist in instance '%2'.")
- .arg(shortcut.executableName())
- .arg(currentInstance->displayName()));
+ MOBase::reportError(
+ QObject::tr("Executable '%1' does not exist in instance '%2'.")
+ .arg(shortcut.executableName())
+ .arg(currentInstance->displayName()));
throw std::exception();
}
@@ -975,11 +952,10 @@ ProcessRunner& ProcessRunner::setFromShortcut(const MOShortcut& shortcut)
return *this;
}
-ProcessRunner& ProcessRunner::setFromFileOrExecutable(
- const QString& executable, const QStringList& args, const QString& cwd,
- const QString& profileOverride, const QString& forcedCustomOverwrite,
- bool ignoreCustomOverwrite)
-{
+ProcessRunner &ProcessRunner::setFromFileOrExecutable(
+ const QString &executable, const QStringList &args, const QString &cwd,
+ const QString &profileOverride, const QString &forcedCustomOverwrite,
+ bool ignoreCustomOverwrite) {
const auto profile = m_core.currentProfile();
if (!profile) {
throw MyException(QObject::tr("No profile set"));
@@ -1004,24 +980,27 @@ ProcessRunner& ProcessRunner::setFromFileOrExecutable(
}
try {
- const Executable& exe = m_core.executablesList()->getByBinary(m_sp.binary);
+ const Executable &exe =
+ m_core.executablesList()->getByBinary(m_sp.binary);
setSteamID(exe.steamAppID());
- setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString());
+ setCustomOverwrite(
+ profile->setting("custom_overwrites", exe.title()).toString());
if (profile->forcedLibrariesEnabled(exe.title())) {
setForcedLibraries(profile->determineForcedLibraries(exe.title()));
}
- } catch (const std::runtime_error&) {
+ } catch (const std::runtime_error &) {
// nop
}
} else {
// only a file name, search executables list
try {
- const Executable& exe = m_core.executablesList()->get(executable);
+ const Executable &exe = m_core.executablesList()->get(executable);
setSteamID(exe.steamAppID());
- setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString());
+ setCustomOverwrite(
+ profile->setting("custom_overwrites", exe.title()).toString());
if (profile->forcedLibrariesEnabled(exe.title())) {
setForcedLibraries(profile->determineForcedLibraries(exe.title()));
@@ -1036,7 +1015,7 @@ ProcessRunner& ProcessRunner::setFromFileOrExecutable(
if (cwd == "") {
setCurrentDirectory(exe.workingDirectory());
}
- } catch (const std::runtime_error&) {
+ } catch (const std::runtime_error &) {
log::warn("\"{}\" not set up as executable", executable);
}
}
@@ -1050,13 +1029,11 @@ ProcessRunner& ProcessRunner::setFromFileOrExecutable(
return *this;
}
-bool ProcessRunner::shouldRunShell() const
-{
+bool ProcessRunner::shouldRunShell() const {
return !m_shellOpen.filePath().isEmpty();
}
-ProcessRunner::Results ProcessRunner::run()
-{
+ProcessRunner::Results ProcessRunner::run() {
// check if setHooked() was called after setFromFile(); this needs to
// modify the settings to run the associated executable instead of using
// shell::Open()
@@ -1097,9 +1074,9 @@ ProcessRunner::Results ProcessRunner::run()
return postRun();
}
-std::optional<ProcessRunner::Results> ProcessRunner::runShell()
-{
- const auto file = MOBase::normalizePathForHost(m_shellOpen.absoluteFilePath());
+std::optional<ProcessRunner::Results> ProcessRunner::runShell() {
+ const auto file =
+ MOBase::normalizePathForHost(m_shellOpen.absoluteFilePath());
log::debug("executing from shell: '{}'", file);
@@ -1122,8 +1099,7 @@ std::optional<ProcessRunner::Results> ProcessRunner::runShell()
return {};
}
-std::optional<ProcessRunner::Results> ProcessRunner::runBinary()
-{
+std::optional<ProcessRunner::Results> ProcessRunner::runBinary() {
if (m_profileName.isEmpty()) {
// get the current profile name if it wasn't overridden
const auto profile = m_core.currentProfile();
@@ -1143,10 +1119,10 @@ std::optional<ProcessRunner::Results> ProcessRunner::runBinary()
}
// parent widget used for any dialog popped up while checking for things
- QWidget* parent = (m_ui ? m_ui->mainWindow() : nullptr);
+ QWidget *parent = (m_ui ? m_ui->mainWindow() : nullptr);
- const auto* game = m_core.managedGame();
- auto& settings = m_core.settings();
+ const auto *game = m_core.managedGame();
+ auto &settings = m_core.settings();
if (m_sp.steamAppID.trimmed().isEmpty()) {
const QString gameSteamId = game->steamAPPId().trimmed();
@@ -1158,7 +1134,8 @@ std::optional<ProcessRunner::Results> ProcessRunner::runBinary()
}
// start steam if needed
- if (!checkSteam(parent, m_sp, game->gameDirectory(), m_sp.steamAppID, settings)) {
+ if (!checkSteam(parent, m_sp, game->gameDirectory(), m_sp.steamAppID,
+ settings)) {
return Error;
}
@@ -1175,7 +1152,8 @@ std::optional<ProcessRunner::Results> ProcessRunner::runBinary()
#ifdef _WIN32
m_handle.reset(startBinary(parent, m_sp));
#else
- m_handle.reset(reinterpret_cast<HANDLE>(static_cast<intptr_t>(startBinary(parent, m_sp))));
+ m_handle.reset(reinterpret_cast<HANDLE>(
+ static_cast<intptr_t>(startBinary(parent, m_sp))));
#endif
if (m_handle.get() == INVALID_HANDLE_VALUE) {
return Error;
@@ -1184,8 +1162,7 @@ std::optional<ProcessRunner::Results> ProcessRunner::runBinary()
return {};
}
-bool ProcessRunner::shouldRefresh(Results r) const
-{
+bool ProcessRunner::shouldRefresh(Results r) const {
// afterRun() is only called with the Refresh flag; it refreshes the
// directory structure and notifies plugins
//
@@ -1211,11 +1188,12 @@ bool ProcessRunner::shouldRefresh(Results r) const
case ForceUnlocked: {
// The process may still be running when the user force-unlocks.
// Refreshing in that state can race with file updates.
- log::debug("process runner: not refreshing because the ui was force unlocked");
+ log::debug(
+ "process runner: not refreshing because the ui was force unlocked");
return false;
}
- case Error: // fall-through
+ case Error: // fall-through
case Cancelled:
case Running:
default: {
@@ -1224,8 +1202,7 @@ bool ProcessRunner::shouldRefresh(Results r) const
}
}
-ProcessRunner::Results ProcessRunner::postRun()
-{
+ProcessRunner::Results ProcessRunner::postRun() {
const bool mustWait = (m_waitFlags & ForceWait);
if (!m_sp.hooked && !mustWait) {
@@ -1248,8 +1225,9 @@ ProcessRunner::Results ProcessRunner::postRun()
if (mustWait) {
if (!lockEnabled) {
// at least tell the user what's going on
- log::debug("locking is disabled, but the output of the application is required; "
- "overriding this setting and locking the ui");
+ log::debug(
+ "locking is disabled, but the output of the application is required; "
+ "overriding this setting and locking the ui");
}
} else {
// no force wait
@@ -1257,10 +1235,12 @@ ProcessRunner::Results ProcessRunner::postRun()
if (m_lockReason == UILocker::NoReason) {
// no locking requested
#ifndef _WIN32
- // Main window launches typically use TriggerRefresh without waiting/locking.
- // In that mode we still need post-run refresh/sync once the process exits.
+ // Main window launches typically use TriggerRefresh without
+ // waiting/locking. In that mode we still need post-run refresh/sync once
+ // the process exits.
if (m_waitFlags.testFlag(TriggerRefresh)) {
- const pid_t pid = static_cast<pid_t>(reinterpret_cast<intptr_t>(m_handle.get()));
+ const pid_t pid =
+ static_cast<pid_t>(reinterpret_cast<intptr_t>(m_handle.get()));
const QFileInfo binary = m_sp.binary;
QPointer<OrganizerCore> core = &m_core;
@@ -1283,11 +1263,11 @@ ProcessRunner::Results ProcessRunner::postRun()
} else if (errno == ECHILD) {
// Detached process — poll with kill(0) until gone.
while (::kill(pid, 0) == 0 || errno == EPERM) {
- usleep(200000); // 200ms
+ usleep(200000); // 200ms
}
} else {
- MOBase::log::warn("process runner: waitpid failed for pid {}: {}", pid,
- errno);
+ MOBase::log::warn("process runner: waitpid failed for pid {}: {}",
+ pid, errno);
}
if (!core) {
@@ -1295,7 +1275,8 @@ ProcessRunner::Results ProcessRunner::postRun()
}
QMetaObject::invokeMethod(
- core, [core, binary, exitCode]() {
+ core,
+ [core, binary, exitCode]() {
if (core) {
core->afterRun(binary, exitCode);
}
@@ -1303,7 +1284,8 @@ ProcessRunner::Results ProcessRunner::postRun()
Qt::QueuedConnection);
}).detach();
- log::debug("process runner: scheduled async post-run refresh for pid {}", pid);
+ log::debug(
+ "process runner: scheduled async post-run refresh for pid {}", pid);
}
#endif
return Running;
@@ -1331,14 +1313,14 @@ ProcessRunner::Results ProcessRunner::postRun()
//
// MO will be running in the background with no visual feedback, but that's
// how it is
- r = waitForProcess(m_handle.get(), &m_exitCode, nullptr, expectedExecutables);
+ r = waitForProcess(m_handle.get(), &m_exitCode, nullptr,
+ expectedExecutables);
} else {
- withLock([&](auto& ls) {
+ withLock([&](auto &ls) {
r = waitForProcess(m_handle.get(), &m_exitCode, &ls, expectedExecutables);
});
}
-
if (shouldRefresh(r)) {
QEventLoop loop;
const bool wait = m_waitFlags.testFlag(WaitForRefresh);
@@ -1361,38 +1343,28 @@ ProcessRunner::Results ProcessRunner::postRun()
}
#ifdef _WIN32
-ProcessRunner::Results ProcessRunner::attachToProcess(HANDLE h)
-{
+ProcessRunner::Results ProcessRunner::attachToProcess(HANDLE h) {
m_handle.reset(h);
return postRun();
}
#else
-ProcessRunner::Results ProcessRunner::attachToProcess(pid_t pid)
-{
+ProcessRunner::Results ProcessRunner::attachToProcess(pid_t pid) {
m_handle.reset(reinterpret_cast<HANDLE>(static_cast<intptr_t>(pid)));
return postRun();
}
#endif
-DWORD ProcessRunner::exitCode() const
-{
- return m_exitCode;
-}
+DWORD ProcessRunner::exitCode() const { return m_exitCode; }
#ifdef _WIN32
-HANDLE ProcessRunner::getProcessHandle() const
-{
- return m_handle.get();
-}
+HANDLE ProcessRunner::getProcessHandle() const { return m_handle.get(); }
#else
-pid_t ProcessRunner::getProcessHandle() const
-{
+pid_t ProcessRunner::getProcessHandle() const {
return static_cast<pid_t>(reinterpret_cast<intptr_t>(m_handle.get()));
}
#endif
-env::HandlePtr ProcessRunner::stealProcessHandle()
-{
+env::HandlePtr ProcessRunner::stealProcessHandle() {
auto h = m_handle.release();
m_handle.reset(INVALID_HANDLE_VALUE);
return env::HandlePtr(h);
@@ -1400,8 +1372,7 @@ env::HandlePtr ProcessRunner::stealProcessHandle()
#ifdef _WIN32
ProcessRunner::Results
-ProcessRunner::waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason)
-{
+ProcessRunner::waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason) {
m_lockReason = reason;
if (!m_core.settings().interface().lockGUI()) {
@@ -1412,7 +1383,7 @@ ProcessRunner::waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason)
auto r = Error;
for (;;) {
- withLock([&](auto& ls) {
+ withLock([&](auto &ls) {
const auto processes = getRunningUSVFSProcesses();
if (processes.empty()) {
r = Completed;
@@ -1439,8 +1410,7 @@ ProcessRunner::waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason)
}
#endif
-void ProcessRunner::withLock(std::function<void(UILocker::Session&)> f)
-{
+void ProcessRunner::withLock(std::function<void(UILocker::Session &)> f) {
auto ls = UILocker::instance().lock(m_lockReason);
f(*ls);
}
diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp
index f2bd7a0..599ef62 100644
--- a/src/src/protonlauncher.cpp
+++ b/src/src/protonlauncher.cpp
@@ -3,14 +3,98 @@
#include <nak_ffi.h>
#include <QCoreApplication>
#include <QDir>
+#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QProcessEnvironment>
#include <QStandardPaths>
#include <log.h>
+
namespace
{
+// Restore the pre-AppImage environment for child processes (umu-run, Proton,
+// pressure-vessel). The AppRun script saves FLUORINE_ORIG_* vars before
+// modifying PATH, LD_LIBRARY_PATH, etc. We restore from those saved values
+// so game processes get a clean host environment without AppImage library paths.
+void cleanAppImageEnv(QProcessEnvironment& env)
+{
+ // Remove Fluorine/AppImage-specific vars that should never leak to game processes.
+ env.remove("QT_QPA_PLATFORM_PLUGIN_PATH");
+ env.remove("MO2_PLUGINS_DIR");
+ env.remove("MO2_DLLS_DIR");
+ env.remove("MO2_PYTHON_DIR");
+ env.remove("MO2_BASE_DIR");
+
+ // AppImage runtime injects these — they can confuse pressure-vessel/umu-run.
+ env.remove("APPIMAGE");
+ env.remove("APPDIR");
+ env.remove("OWD");
+ env.remove("ARGV0");
+ env.remove("APPIMAGE_ORIGINAL_EXEC");
+
+ env.remove("DESKTOPINTEGRATION");
+
+ // Restore saved pre-AppImage values. AppRun sets FLUORINE_ORIG_* before
+ // modifying PATH, LD_LIBRARY_PATH, etc. If those vars exist, use them to
+ // restore the original host environment. If not (standalone/non-AppImage),
+ // fall back to stripping known AppImage patterns.
+ auto restoreOrStrip = [](const QString& var, const QString& origVar,
+ QProcessEnvironment& e) {
+ if (e.contains(origVar)) {
+ const QString orig = e.value(origVar);
+ if (orig.isEmpty()) {
+ e.remove(var);
+ } else {
+ e.insert(var, orig);
+ }
+ e.remove(origVar);
+ } else {
+ // Fallback: strip AppImage mount paths by pattern.
+ const QString value = e.value(var);
+ if (value.isEmpty()) return;
+ QStringList kept;
+ for (const QString& p : value.split(':')) {
+ if (p.contains(".mount_Fluori") || p.contains("/fluorine/python")) {
+ continue;
+ }
+ kept.append(p);
+ }
+ if (kept.isEmpty()) {
+ e.remove(var);
+ } else {
+ e.insert(var, kept.join(':'));
+ }
+ }
+ };
+
+ const bool hasOrigVars = env.contains("FLUORINE_ORIG_PATH");
+ restoreOrStrip("LD_LIBRARY_PATH", "FLUORINE_ORIG_LD_LIBRARY_PATH", env);
+ restoreOrStrip("PATH", "FLUORINE_ORIG_PATH", env);
+ restoreOrStrip("XDG_DATA_DIRS", "FLUORINE_ORIG_XDG_DATA_DIRS", env);
+ restoreOrStrip("QT_PLUGIN_PATH", "FLUORINE_ORIG_QT_PLUGIN_PATH", env);
+
+ MOBase::log::debug("cleanAppImageEnv: {} (LD_LIBRARY_PATH='{}')",
+ hasOrigVars ? "restored from FLUORINE_ORIG_*" : "pattern-strip fallback",
+ env.value("LD_LIBRARY_PATH", "<unset>"));
+}
+
+// GE-Proton crashes in update_builtin_libs() if tracked_files doesn't exist
+// (e.g. prefix was created by plain Wine, not Proton). Create an empty one.
+void ensureTrackedFilesExist(const QString& compatDataPath)
+{
+ if (compatDataPath.isEmpty()) return;
+ const QString tracked = QDir(compatDataPath).filePath("tracked_files");
+ if (!QFileInfo::exists(tracked)) {
+ QDir().mkpath(compatDataPath);
+ QFile f(tracked);
+ if (f.open(QIODevice::WriteOnly)) {
+ f.close();
+ MOBase::log::debug("created empty tracked_files at '{}'", tracked);
+ }
+ }
+}
+
QString compatDataPathFromPrefix(const QString& prefixPath)
{
if (prefixPath.isEmpty()) {
@@ -226,6 +310,12 @@ ProtonLauncher& ProtonLauncher::setSteamDrm(bool useSteamDrm)
return *this;
}
+ProtonLauncher& ProtonLauncher::setStoreVariant(const QString& variant)
+{
+ m_storeVariant = variant.trimmed();
+ return *this;
+}
+
ProtonLauncher& ProtonLauncher::addEnvVar(const QString& key, const QString& value)
{
if (!key.isEmpty()) {
@@ -277,6 +367,7 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.remove("PYTHONHOME");
+ cleanAppImageEnv(env);
if (!m_prefixPath.isEmpty()) {
env.insert("WINEPREFIX", m_prefixPath);
@@ -284,6 +375,7 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const
const QString compatDataPath = compatDataPathFromPrefix(m_prefixPath);
if (!compatDataPath.isEmpty()) {
+ ensureTrackedFilesExist(compatDataPath);
env.insert("STEAM_COMPAT_DATA_PATH", compatDataPath);
}
@@ -320,6 +412,10 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const
MOBase::log::info("Proton launch: '{}' run '{}'", protonScript, m_binary);
+ if (!m_workingDir.isEmpty()) {
+ env.insert("PWD", m_workingDir);
+ }
+
return startDetachedWithEnv(program, arguments, m_workingDir, env, pid);
}
@@ -335,9 +431,11 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const
ensureSteamRunning();
}
- // Resolve umu-run according to user preference (bundled vs system).
+ // Resolve umu-run: prefer the copy deployed to the Fluorine data dir
+ // (~/.local/share/fluorine/umu-run), then system PATH, then AppImage bundled.
const QString appDir = QCoreApplication::applicationDirPath();
const QString bundled = appDir + QStringLiteral("/umu-run");
+ const QString dataDir = QDir::home().filePath(".local/share/fluorine/umu-run");
// Search PATH for a system umu-run, excluding our own app directory
// (the launcher prepends it to PATH, so findExecutable would find the
@@ -356,25 +454,20 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const
}
}
+ // Priority: data dir > system (if preferred) > bundled > system (fallback)
QString umuRun;
- if (m_preferSystemUmu) {
- if (!system.isEmpty()) {
- umuRun = system;
- } else if (QFileInfo::exists(bundled)) {
- umuRun = bundled;
- MOBase::log::warn(
- "System umu-run preferred but not found in PATH, falling back to bundled");
- }
- } else {
- if (QFileInfo::exists(bundled)) {
- umuRun = bundled;
- } else if (!system.isEmpty()) {
- umuRun = system;
- }
+ if (QFileInfo::exists(dataDir)) {
+ umuRun = dataDir;
+ } else if (m_preferSystemUmu && !system.isEmpty()) {
+ umuRun = system;
+ } else if (QFileInfo::exists(bundled)) {
+ umuRun = bundled;
+ } else if (!system.isEmpty()) {
+ umuRun = system;
}
- MOBase::log::info("umu-run: preferSystem={}, bundled='{}' (exists={}), system='{}', selected='{}'",
- m_preferSystemUmu, bundled, QFileInfo::exists(bundled), system, umuRun);
+ MOBase::log::info("umu-run: dataDir='{}' (exists={}), bundled='{}' (exists={}), system='{}', selected='{}'",
+ dataDir, QFileInfo::exists(dataDir), bundled, QFileInfo::exists(bundled), system, umuRun);
if (umuRun.isEmpty()) {
MOBase::log::warn("umu-run not found (bundled or in PATH)");
@@ -390,6 +483,7 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.remove("PYTHONHOME");
+ cleanAppImageEnv(env);
if (!m_prefixPath.isEmpty()) {
env.insert("WINEPREFIX", m_prefixPath);
@@ -400,12 +494,8 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const
}
// umu-run sets STEAM_COMPAT_DATA_PATH internally from WINEPREFIX, so we
- // do NOT set it here. However, the game's Steamworks DRM still needs
- // STEAM_COMPAT_CLIENT_INSTALL_PATH to locate the Steam client libraries.
- const QString steamPath = detectSteamPath();
- if (!steamPath.isEmpty()) {
- env.insert("STEAM_COMPAT_CLIENT_INSTALL_PATH", steamPath);
- }
+ // do NOT set it here. However, ensure tracked_files exists for Proton.
+ ensureTrackedFilesExist(compatDataPathFromPrefix(m_prefixPath));
uint32_t effectiveSteamAppId = m_steamAppId;
if (effectiveSteamAppId == 0) {
@@ -418,17 +508,57 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const
}
}
- if (m_useSteamDrm && effectiveSteamAppId != 0) {
- // umu-run expects GAMEID in "umu-<AppID>" format to extract SteamAppId.
+ // Always pass the game's Steam App ID as GAMEID for protonfixes lookup.
+ // GAMEID identifies the game (for compatibility fixes), separate from DRM.
+ if (effectiveSteamAppId != 0) {
env.insert("GAMEID", QStringLiteral("umu-") + QString::number(effectiveSteamAppId));
- env.insert("SteamAppId", QString::number(effectiveSteamAppId));
- env.insert("SteamGameId", QString::number(effectiveSteamAppId));
- env.insert("STORE", QStringLiteral("steam"));
} else {
- // No Steam DRM — use a generic game ID so umu-run doesn't try Steam integration.
env.insert("GAMEID", QStringLiteral("umu-0"));
}
+ if (m_useSteamDrm) {
+ // Steam DRM games need the Steam client path and Steam identity env vars.
+ env.insert("STORE", QStringLiteral("steam"));
+ const QString steamPath = detectSteamPath();
+ if (!steamPath.isEmpty()) {
+ env.insert("STEAM_COMPAT_CLIENT_INSTALL_PATH", steamPath);
+ }
+ if (effectiveSteamAppId != 0) {
+ env.insert("SteamAppId", QString::number(effectiveSteamAppId));
+ env.insert("SteamGameId", QString::number(effectiveSteamAppId));
+ }
+ } else {
+ // Non-Steam games: do NOT set SteamAppId/SteamGameId — those can trigger
+ // Steamworks initialization in Wine which crashes GOG/Epic games.
+ env.remove("SteamAPPId");
+ env.remove("SteamAppId");
+ env.remove("SteamGameId");
+
+ // Set STORE so umu-run knows this is a non-Steam game. Without STORE,
+ // umu-run may attempt Steam-specific behavior that breaks GOG/Epic titles.
+ // umu-run expects lowercase values (gog, egs, etc.).
+ if (!m_storeVariant.isEmpty()) {
+ env.insert("STORE", m_storeVariant.toLower());
+ } else {
+ env.insert("STORE", QStringLiteral("gog"));
+ }
+ }
+
+ // Remove FUSE VFS file descriptors — these are Fluorine-internal and must
+ // not leak into game processes (especially pressure-vessel containers).
+ env.remove("_FUSE_COMMFD");
+ env.remove("_FUSE_COMMFD2");
+
+ // Remove Qt/KDE theme vars that can confuse pressure-vessel/Wine.
+ env.remove("QML_DISABLE_DISK_CACHE");
+ env.remove("QT_ICON_THEME_NAME");
+ env.remove("QT_STYLE_OVERRIDE");
+ env.remove("OLDPWD");
+
+ if (!m_workingDir.isEmpty()) {
+ env.insert("PWD", m_workingDir);
+ }
+
for (auto it = m_wrapperEnvVars.cbegin(); it != m_wrapperEnvVars.cend(); ++it) {
env.insert(it.key(), it.value());
}
@@ -446,13 +576,11 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const
}
}
- MOBase::log::info("UMU launch: '{}' '{}' (game id: {}, steam: '{}')", umuRun,
- m_binary,
- (effectiveSteamAppId == 0
- ? QStringLiteral("<unset>")
- : QStringLiteral("umu-") +
- QString::number(effectiveSteamAppId)),
- steamPath);
+ MOBase::log::info("UMU launch: '{}' '{}' (GAMEID={}, STORE={}, steam_drm={})",
+ umuRun, m_binary,
+ env.value("GAMEID"),
+ env.value("STORE", "<unset>"),
+ m_useSteamDrm);
return startDetachedWithEnv(program, arguments, m_workingDir, env, pid);
}
@@ -470,6 +598,7 @@ bool ProtonLauncher::launchDirect(qint64& pid) const
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.remove("PYTHONHOME");
+ cleanAppImageEnv(env);
for (auto it = m_wrapperEnvVars.cbegin(); it != m_wrapperEnvVars.cend(); ++it) {
env.insert(it.key(), it.value());
}
diff --git a/src/src/protonlauncher.h b/src/src/protonlauncher.h
index 5480c69..7ef6e76 100644
--- a/src/src/protonlauncher.h
+++ b/src/src/protonlauncher.h
@@ -24,6 +24,7 @@ public:
ProtonLauncher& setPreferSystemUmu(bool preferSystemUmu);
ProtonLauncher& setUseSteamRun(bool useSteamRun);
ProtonLauncher& setSteamDrm(bool useSteamDrm);
+ ProtonLauncher& setStoreVariant(const QString& variant);
ProtonLauncher& addEnvVar(const QString& key, const QString& value);
// Launch dispatch: UMU -> Proton -> Direct
@@ -46,6 +47,7 @@ private:
bool m_preferSystemUmu;
bool m_useSteamRun;
bool m_useSteamDrm;
+ QString m_storeVariant; // "GOG", "Epic", or empty for Steam
QMap<QString, QString> m_envVars;
QMap<QString, QString> m_wrapperEnvVars;
};
diff --git a/src/src/savestab.cpp b/src/src/savestab.cpp
index 2f921e5..29411fa 100644
--- a/src/src/savestab.cpp
+++ b/src/src/savestab.cpp
@@ -1,6 +1,6 @@
-#include "savestab.h"
-#include "activatemodsdialog.h"
-#include "organizercore.h"
+#include "savestab.h"
+#include "activatemodsdialog.h"
+#include "organizercore.h"
#include "ui_mainwindow.h"
#include <iplugingame.h>
#include <isavegameinfowidget.h>
@@ -8,12 +8,55 @@
#include <new>
#include <report.h>
+#include <QFile>
+#include <QTextStream>
+
using namespace MOBase;
namespace
{
bool g_disableSaveTooltipsAfterOOM = false;
+// Read a value from a Bethesda-style INI file without QSettings.
+// QSettings::IniFormat interprets backslashes as line continuations,
+// which corrupts values like "sLocalSavePath=__MO_Saves\".
+QString readIniValueDirect(const QString& iniFile, const QString& section,
+ const QString& key)
+{
+ QFile file(iniFile);
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ return {};
+ }
+
+ const QString sectionHeader = "[" + section + "]";
+ QTextStream in(&file);
+ bool inSection = false;
+
+ while (!in.atEnd()) {
+ QString line = in.readLine().trimmed();
+ if (line.startsWith('[') && line.endsWith(']')) {
+ if (inSection)
+ break;
+ if (line.compare(sectionHeader, Qt::CaseInsensitive) == 0)
+ inSection = true;
+ continue;
+ }
+
+ if (inSection && !line.isEmpty() && !line.startsWith(';') &&
+ !line.startsWith('#')) {
+ int eqPos = line.indexOf('=');
+ if (eqPos > 0) {
+ QString existingKey = line.left(eqPos).trimmed();
+ if (existingKey.compare(key, Qt::CaseInsensitive) == 0) {
+ return line.mid(eqPos + 1).trimmed();
+ }
+ }
+ }
+ }
+
+ return {};
+}
+
QString sanitizeText(QString in, int maxLen = 200)
{
for (int i = 0; i < in.size(); ++i) {
@@ -51,48 +94,48 @@ bool isLikelyCorruptSaveText(QString const& in)
} // namespace
SavesTab::SavesTab(QWidget* window, OrganizerCore& core, Ui::MainWindow* mwui)
- : m_window(window), m_core(core), m_CurrentSaveView(nullptr),
- ui{mwui->tabWidget, mwui->savesTab, mwui->savegameList}
-{
- m_SavesWatcherTimer.setSingleShot(true);
- m_SavesWatcherTimer.setInterval(500);
-
- ui.list->installEventFilter(this);
- ui.list->setMouseTracking(true);
-
- connect(&m_SavesWatcher, &QFileSystemWatcher::directoryChanged, [&] {
- m_SavesWatcherTimer.start();
- });
-
- connect(&m_SavesWatcherTimer, &QTimer::timeout, [&] {
- refreshSavesIfOpen();
- });
-
- connect(ui.list, &QWidget::customContextMenuRequested, [&](auto pos) {
- onContextMenu(pos);
- });
-
- connect(ui.list, &QTreeWidget::itemEntered, [&](auto* item) {
- saveSelectionChanged(item);
- });
-}
-
-bool SavesTab::eventFilter(QObject* object, QEvent* e)
-{
- if (object == ui.list) {
- if (e->type() == QEvent::Leave || e->type() == QEvent::WindowDeactivate) {
- hideSaveGameInfo();
- } else if (e->type() == QEvent::KeyPress) {
- QKeyEvent* keyEvent = static_cast<QKeyEvent*>(e);
- if (keyEvent->key() == Qt::Key_Delete) {
- deleteSavegame();
- }
- }
- }
-
- return false;
-}
-
+ : m_window(window), m_core(core), m_CurrentSaveView(nullptr),
+ ui{mwui->tabWidget, mwui->savesTab, mwui->savegameList}
+{
+ m_SavesWatcherTimer.setSingleShot(true);
+ m_SavesWatcherTimer.setInterval(500);
+
+ ui.list->installEventFilter(this);
+ ui.list->setMouseTracking(true);
+
+ connect(&m_SavesWatcher, &QFileSystemWatcher::directoryChanged, [&] {
+ m_SavesWatcherTimer.start();
+ });
+
+ connect(&m_SavesWatcherTimer, &QTimer::timeout, [&] {
+ refreshSavesIfOpen();
+ });
+
+ connect(ui.list, &QWidget::customContextMenuRequested, [&](auto pos) {
+ onContextMenu(pos);
+ });
+
+ connect(ui.list, &QTreeWidget::itemEntered, [&](auto* item) {
+ saveSelectionChanged(item);
+ });
+}
+
+bool SavesTab::eventFilter(QObject* object, QEvent* e)
+{
+ if (object == ui.list) {
+ if (e->type() == QEvent::Leave || e->type() == QEvent::WindowDeactivate) {
+ hideSaveGameInfo();
+ } else if (e->type() == QEvent::KeyPress) {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(e);
+ if (keyEvent->key() == Qt::Key_Delete) {
+ deleteSavegame();
+ }
+ }
+ }
+
+ return false;
+}
+
void SavesTab::displaySaveGameInfo(QTreeWidgetItem* newItem)
{
if (g_disableSaveTooltipsAfterOOM) {
@@ -100,21 +143,21 @@ void SavesTab::displaySaveGameInfo(QTreeWidgetItem* newItem)
}
// don't display the widget if the main window doesn't have focus
- //
- // this goes against the standard behaviour for tooltips, which are displayed
- // on hover regardless of focus, but this widget is so large and busy that
- // it's probably better this way
- if (!m_window->isActiveWindow()) {
- return;
- }
-
+ //
+ // this goes against the standard behaviour for tooltips, which are displayed
+ // on hover regardless of focus, but this widget is so large and busy that
+ // it's probably better this way
+ if (!m_window->isActiveWindow()) {
+ return;
+ }
+
if (m_CurrentSaveView == nullptr) {
auto info = m_core.gameFeatures().gameFeature<SaveGameInfo>();
-
- if (info != nullptr) {
- m_CurrentSaveView = info->getSaveGameWidget(m_window);
- }
-
+
+ if (info != nullptr) {
+ m_CurrentSaveView = info->getSaveGameWidget(m_window);
+ }
+
if (m_CurrentSaveView == nullptr) {
return;
}
@@ -135,129 +178,135 @@ void SavesTab::displaySaveGameInfo(QTreeWidgetItem* newItem)
hideSaveGameInfo();
return;
}
-
- QWindow* window = m_CurrentSaveView->window()->windowHandle();
- QRect screenRect;
- if (window == nullptr)
- screenRect = QGuiApplication::primaryScreen()->geometry();
- else
- screenRect = window->screen()->geometry();
-
- QPoint pos = QCursor::pos();
- if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) {
- pos.rx() -= (m_CurrentSaveView->width() + 2);
- } else {
- pos.rx() += 5;
- }
-
- if (pos.y() + m_CurrentSaveView->height() > screenRect.bottom()) {
- pos.ry() -= (m_CurrentSaveView->height() + 10);
- } else {
- pos.ry() += 20;
- }
- m_CurrentSaveView->move(pos);
-
- m_CurrentSaveView->show();
- m_CurrentSaveView->setProperty("displayItem",
- QVariant::fromValue(static_cast<void*>(newItem)));
-}
-
-void SavesTab::saveSelectionChanged(QTreeWidgetItem* newItem)
-{
- if (newItem == nullptr) {
- hideSaveGameInfo();
- } else if (m_CurrentSaveView == nullptr ||
- newItem != m_CurrentSaveView->property("displayItem").value<void*>()) {
- displaySaveGameInfo(newItem);
- }
-}
-
-void SavesTab::hideSaveGameInfo()
-{
- if (m_CurrentSaveView != nullptr) {
- m_CurrentSaveView->deleteLater();
- m_CurrentSaveView = nullptr;
- }
-}
-
-void SavesTab::refreshSavesIfOpen()
-{
- if (ui.mainTabs->currentWidget() == ui.tab) {
- refreshSaveList();
- }
-}
-
-QDir SavesTab::currentSavesDir() const
-{
- // TODO: This code should probably be handled by the game plugins
- QDir savesDir;
- if (m_core.currentProfile()->localSavesEnabled()) {
- savesDir.setPath(m_core.currentProfile()->savePath());
- } else {
- auto iniFiles = m_core.managedGame()->iniFiles();
-
- if (iniFiles.isEmpty() ||
- m_core.gameFeatures().gameFeature<LocalSavegames>() == nullptr) {
- return m_core.managedGame()->savesDirectory();
- }
-
- QString iniPath = m_core.currentProfile()->absoluteIniFilePath(iniFiles[0]);
-
-#ifdef _WIN32
- wchar_t path[MAX_PATH];
- if (::GetPrivateProfileStringW(L"General", L"SLocalSavePath", L"", path, MAX_PATH,
- iniPath.toStdWString().c_str())) {
- savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath(
- QString::fromWCharArray(path)));
- } else {
- savesDir = m_core.managedGame()->savesDirectory();
- }
-#else
- // On Linux, use QSettings to read the INI file
- QSettings ini(iniPath, QSettings::IniFormat);
- QString savePath = ini.value("General/SLocalSavePath").toString();
- if (!savePath.isEmpty()) {
- savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath(savePath));
- } else {
- savesDir = m_core.managedGame()->savesDirectory();
- }
-#endif
- }
-
- return savesDir;
-}
-
-void SavesTab::startMonitorSaves()
-{
- stopMonitorSaves();
-
- QDir savesDir = currentSavesDir();
-
- m_SavesWatcher.addPath(savesDir.absolutePath());
-}
-
-void SavesTab::stopMonitorSaves()
-{
- if (m_SavesWatcher.directories().length() > 0) {
- m_SavesWatcher.removePaths(m_SavesWatcher.directories());
- }
-}
-
-void SavesTab::refreshSaveList()
-{
- TimeThis tt("MainWindow::refreshSaveList()");
-
- startMonitorSaves(); // re-starts monitoring
-
- try {
- QDir savesDir = currentSavesDir();
- MOBase::log::debug("reading save games from {}", savesDir.absolutePath());
- m_SaveGames = m_core.managedGame()->listSaves(savesDir);
- std::sort(m_SaveGames.begin(), m_SaveGames.end(),
- [](auto const& lhs, auto const& rhs) {
- return lhs->getCreationTime() > rhs->getCreationTime();
- });
-
+
+ QWindow* window = m_CurrentSaveView->window()->windowHandle();
+ QRect screenRect;
+ if (window == nullptr)
+ screenRect = QGuiApplication::primaryScreen()->geometry();
+ else
+ screenRect = window->screen()->geometry();
+
+ QPoint pos = QCursor::pos();
+ if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) {
+ pos.rx() -= (m_CurrentSaveView->width() + 2);
+ } else {
+ pos.rx() += 5;
+ }
+
+ if (pos.y() + m_CurrentSaveView->height() > screenRect.bottom()) {
+ pos.ry() -= (m_CurrentSaveView->height() + 10);
+ } else {
+ pos.ry() += 20;
+ }
+ m_CurrentSaveView->move(pos);
+
+ m_CurrentSaveView->show();
+ m_CurrentSaveView->setProperty("displayItem",
+ QVariant::fromValue(static_cast<void*>(newItem)));
+}
+
+void SavesTab::saveSelectionChanged(QTreeWidgetItem* newItem)
+{
+ if (newItem == nullptr) {
+ hideSaveGameInfo();
+ } else if (m_CurrentSaveView == nullptr ||
+ newItem != m_CurrentSaveView->property("displayItem").value<void*>()) {
+ displaySaveGameInfo(newItem);
+ }
+}
+
+void SavesTab::hideSaveGameInfo()
+{
+ if (m_CurrentSaveView != nullptr) {
+ m_CurrentSaveView->deleteLater();
+ m_CurrentSaveView = nullptr;
+ }
+}
+
+void SavesTab::refreshSavesIfOpen()
+{
+ if (ui.mainTabs->currentWidget() == ui.tab) {
+ refreshSaveList();
+ }
+}
+
+QDir SavesTab::currentSavesDir() const
+{
+ // TODO: This code should probably be handled by the game plugins
+ QDir savesDir;
+ if (m_core.currentProfile()->localSavesEnabled()) {
+ savesDir.setPath(m_core.currentProfile()->savePath());
+ } else {
+ auto iniFiles = m_core.managedGame()->iniFiles();
+
+ if (iniFiles.isEmpty() ||
+ m_core.gameFeatures().gameFeature<LocalSavegames>() == nullptr) {
+ return m_core.managedGame()->savesDirectory();
+ }
+
+ QString iniPath = m_core.currentProfile()->absoluteIniFilePath(iniFiles[0]);
+
+#ifdef _WIN32
+ wchar_t path[MAX_PATH];
+ if (::GetPrivateProfileStringW(L"General", L"SLocalSavePath", L"", path, MAX_PATH,
+ iniPath.toStdWString().c_str())) {
+ savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath(
+ QString::fromWCharArray(path)));
+ } else {
+ savesDir = m_core.managedGame()->savesDirectory();
+ }
+#else
+ // Read directly without QSettings — QSettings::IniFormat interprets
+ // trailing backslashes as line continuations, corrupting values like
+ // "sLocalSavePath=__MO_Saves\".
+ QString savePath = readIniValueDirect(iniPath, "General", "sLocalSavePath");
+ // Strip trailing path separators (Bethesda INIs use "Saves\" with a
+ // trailing backslash that is part of the value, not a continuation).
+ while (savePath.endsWith('\\') || savePath.endsWith('/')) {
+ savePath.chop(1);
+ }
+ if (!savePath.isEmpty()) {
+ savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath(savePath));
+ } else {
+ savesDir = m_core.managedGame()->savesDirectory();
+ }
+#endif
+ }
+
+ return savesDir;
+}
+
+void SavesTab::startMonitorSaves()
+{
+ stopMonitorSaves();
+
+ QDir savesDir = currentSavesDir();
+
+ m_SavesWatcher.addPath(savesDir.absolutePath());
+}
+
+void SavesTab::stopMonitorSaves()
+{
+ if (m_SavesWatcher.directories().length() > 0) {
+ m_SavesWatcher.removePaths(m_SavesWatcher.directories());
+ }
+}
+
+void SavesTab::refreshSaveList()
+{
+ TimeThis tt("MainWindow::refreshSaveList()");
+
+ startMonitorSaves(); // re-starts monitoring
+
+ try {
+ QDir savesDir = currentSavesDir();
+ MOBase::log::debug("reading save games from {}", savesDir.absolutePath());
+ m_SaveGames = m_core.managedGame()->listSaves(savesDir);
+ std::sort(m_SaveGames.begin(), m_SaveGames.end(),
+ [](auto const& lhs, auto const& rhs) {
+ return lhs->getCreationTime() > rhs->getCreationTime();
+ });
+
ui.list->clear();
for (auto& save : m_SaveGames) {
auto relpath = savesDir.relativeFilePath(save->getFilepath());
@@ -269,125 +318,125 @@ void SavesTab::refreshSaveList()
ui.list->addTopLevelItem(new QTreeWidgetItem(ui.list, {display, relpath}));
}
} catch (std::exception& e) {
- // listSaves() can throw
- log::error("{}", e.what());
- }
-}
-
-void SavesTab::deleteSavegame()
-{
- auto info = m_core.gameFeatures().gameFeature<SaveGameInfo>();
-
- QString savesMsgLabel;
- QStringList deleteFiles;
-
- int count = 0;
-
- for (const QModelIndex& idx : ui.list->selectionModel()->selectedRows()) {
-
- auto& saveGame = m_SaveGames[idx.row()];
-
- if (count < 10) {
- savesMsgLabel +=
- "<li>" + QFileInfo(saveGame->getFilepath()).completeBaseName() + "</li>";
- }
- ++count;
-
- deleteFiles += saveGame->allFiles();
- }
-
- if (count > 10) {
- savesMsgLabel += "<li><i>... " + tr("%1 more").arg(count - 10) + "</i></li>";
- }
-
- if (QMessageBox::question(
- m_window, tr("Confirm"),
- tr("Are you sure you want to remove the following %n save(s)?<br>"
- "<ul>%1</ul><br>"
- "Removed saves will be sent to the Recycle Bin.",
- "", count)
- .arg(savesMsgLabel),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- shellDelete(deleteFiles, true); // recycle bin delete.
- refreshSaveList();
- }
-}
-
-void SavesTab::onContextMenu(const QPoint& pos)
-{
- QItemSelectionModel* selection = ui.list->selectionModel();
-
- if (!selection->hasSelection()) {
- return;
- }
-
- QMenu menu;
-
- auto info = m_core.gameFeatures().gameFeature<SaveGameInfo>();
- if (info != nullptr) {
- QAction* action = menu.addAction(tr("Fix enabled mods..."));
- action->setEnabled(false);
- if (selection->selectedRows().count() == 1) {
- auto& save = m_SaveGames[selection->selectedRows()[0].row()];
- SaveGameInfo::MissingAssets missing = info->getMissingAssets(*save);
- if (missing.size() != 0) {
- connect(action, &QAction::triggered, this, [this, missing] {
- fixMods(missing);
- });
- action->setEnabled(true);
- }
- }
- }
-
- QString deleteMenuLabel =
- tr("Delete %n save(s)", "", selection->selectedRows().count());
- menu.addAction(deleteMenuLabel, [&] {
- deleteSavegame();
- });
-
- menu.addAction(tr("Open in Explorer..."), [&] {
- openInExplorer();
- });
-
- menu.exec(ui.list->viewport()->mapToGlobal(pos));
-}
-
-void SavesTab::fixMods(SaveGameInfo::MissingAssets const& missingAssets)
-{
- ActivateModsDialog dialog(missingAssets, m_window);
- if (dialog.exec() == QDialog::Accepted) {
- // activate the required mods, then enable all esps
- std::set<QString> modsToActivate = dialog.getModsToActivate();
- for (std::set<QString>::iterator iter = modsToActivate.begin();
- iter != modsToActivate.end(); ++iter) {
- if ((*iter != "<data>") && (*iter != "<overwrite>")) {
- unsigned int modIndex = ModInfo::getIndex(*iter);
- m_core.currentProfile()->setModEnabled(modIndex, true);
- }
- }
-
- m_core.currentProfile()->writeModlist();
- m_core.refreshLists();
-
- std::set<QString> espsToActivate = dialog.getESPsToActivate();
- for (std::set<QString>::iterator iter = espsToActivate.begin();
- iter != espsToActivate.end(); ++iter) {
- m_core.pluginList()->enableESP(*iter);
- }
-
- m_core.saveCurrentLists();
- }
-}
-
-void SavesTab::openInExplorer()
-{
- auto info = m_core.gameFeatures().gameFeature<SaveGameInfo>();
-
- const auto sel = ui.list->selectionModel()->selectedRows();
- if (sel.empty()) {
- return;
- }
-
- auto& saveGame = m_SaveGames[sel[0].row()];
- shell::Explore(saveGame->getFilepath());
-}
+ // listSaves() can throw
+ log::error("{}", e.what());
+ }
+}
+
+void SavesTab::deleteSavegame()
+{
+ auto info = m_core.gameFeatures().gameFeature<SaveGameInfo>();
+
+ QString savesMsgLabel;
+ QStringList deleteFiles;
+
+ int count = 0;
+
+ for (const QModelIndex& idx : ui.list->selectionModel()->selectedRows()) {
+
+ auto& saveGame = m_SaveGames[idx.row()];
+
+ if (count < 10) {
+ savesMsgLabel +=
+ "<li>" + QFileInfo(saveGame->getFilepath()).completeBaseName() + "</li>";
+ }
+ ++count;
+
+ deleteFiles += saveGame->allFiles();
+ }
+
+ if (count > 10) {
+ savesMsgLabel += "<li><i>... " + tr("%1 more").arg(count - 10) + "</i></li>";
+ }
+
+ if (QMessageBox::question(
+ m_window, tr("Confirm"),
+ tr("Are you sure you want to remove the following %n save(s)?<br>"
+ "<ul>%1</ul><br>"
+ "Removed saves will be sent to the Recycle Bin.",
+ "", count)
+ .arg(savesMsgLabel),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ shellDelete(deleteFiles, true); // recycle bin delete.
+ refreshSaveList();
+ }
+}
+
+void SavesTab::onContextMenu(const QPoint& pos)
+{
+ QItemSelectionModel* selection = ui.list->selectionModel();
+
+ if (!selection->hasSelection()) {
+ return;
+ }
+
+ QMenu menu;
+
+ auto info = m_core.gameFeatures().gameFeature<SaveGameInfo>();
+ if (info != nullptr) {
+ QAction* action = menu.addAction(tr("Fix enabled mods..."));
+ action->setEnabled(false);
+ if (selection->selectedRows().count() == 1) {
+ auto& save = m_SaveGames[selection->selectedRows()[0].row()];
+ SaveGameInfo::MissingAssets missing = info->getMissingAssets(*save);
+ if (missing.size() != 0) {
+ connect(action, &QAction::triggered, this, [this, missing] {
+ fixMods(missing);
+ });
+ action->setEnabled(true);
+ }
+ }
+ }
+
+ QString deleteMenuLabel =
+ tr("Delete %n save(s)", "", selection->selectedRows().count());
+ menu.addAction(deleteMenuLabel, [&] {
+ deleteSavegame();
+ });
+
+ menu.addAction(tr("Open in Explorer..."), [&] {
+ openInExplorer();
+ });
+
+ menu.exec(ui.list->viewport()->mapToGlobal(pos));
+}
+
+void SavesTab::fixMods(SaveGameInfo::MissingAssets const& missingAssets)
+{
+ ActivateModsDialog dialog(missingAssets, m_window);
+ if (dialog.exec() == QDialog::Accepted) {
+ // activate the required mods, then enable all esps
+ std::set<QString> modsToActivate = dialog.getModsToActivate();
+ for (std::set<QString>::iterator iter = modsToActivate.begin();
+ iter != modsToActivate.end(); ++iter) {
+ if ((*iter != "<data>") && (*iter != "<overwrite>")) {
+ unsigned int modIndex = ModInfo::getIndex(*iter);
+ m_core.currentProfile()->setModEnabled(modIndex, true);
+ }
+ }
+
+ m_core.currentProfile()->writeModlist();
+ m_core.refreshLists();
+
+ std::set<QString> espsToActivate = dialog.getESPsToActivate();
+ for (std::set<QString>::iterator iter = espsToActivate.begin();
+ iter != espsToActivate.end(); ++iter) {
+ m_core.pluginList()->enableESP(*iter);
+ }
+
+ m_core.saveCurrentLists();
+ }
+}
+
+void SavesTab::openInExplorer()
+{
+ auto info = m_core.gameFeatures().gameFeature<SaveGameInfo>();
+
+ const auto sel = ui.list->selectionModel()->selectedRows();
+ if (sel.empty()) {
+ return;
+ }
+
+ auto& saveGame = m_SaveGames[sel[0].row()];
+ shell::Explore(saveGame->getFilepath());
+}
diff --git a/src/src/shared/appconfig.cpp b/src/src/shared/appconfig.cpp
index 90bbffb..579318f 100644
--- a/src/src/shared/appconfig.cpp
+++ b/src/src/shared/appconfig.cpp
@@ -46,6 +46,28 @@ QString basePath()
return QCoreApplication::applicationDirPath();
}
+QString pluginsPath()
+{
+#ifndef _WIN32
+ const char* envDir = std::getenv("MO2_PLUGINS_DIR");
+ if (envDir && envDir[0] != '\0') {
+ return QString::fromUtf8(envDir);
+ }
+#endif
+ return basePath() + "/" + QString::fromStdWString(pluginPath());
+}
+
+QString dllsPath()
+{
+#ifndef _WIN32
+ const char* envDir = std::getenv("MO2_DLLS_DIR");
+ if (envDir && envDir[0] != '\0') {
+ return QString::fromUtf8(envDir);
+ }
+#endif
+ return basePath() + "/dlls";
+}
+
namespace MOShared
{
#undef PARWSTRING
diff --git a/src/src/shared/appconfig.h b/src/src/shared/appconfig.h
index 03f3300..0c0f7b4 100644
--- a/src/src/shared/appconfig.h
+++ b/src/src/shared/appconfig.h
@@ -36,6 +36,16 @@ namespace AppConfig
// returned; otherwise falls back to QCoreApplication::applicationDirPath().
QString basePath();
+// Returns the directory containing MO2 plugins. On Linux, if the
+// MO2_PLUGINS_DIR environment variable is set (e.g. when plugins live
+// inside a read-only AppImage squashfs) that value is used; otherwise
+// falls back to basePath() + "/plugins".
+QString pluginsPath();
+
+// Returns the directory containing bundled DLLs (7z.so, etc.).
+// Respects MO2_DLLS_DIR on Linux, otherwise basePath() + "/dlls".
+QString dllsPath();
+
namespace MOShared
{
#undef PARWSTRING
diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp
index 05e07bd..850d5a2 100644
--- a/src/src/spawn.cpp
+++ b/src/src/spawn.cpp
@@ -53,12 +53,10 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
using namespace MOBase;
using namespace MOShared;
-namespace spawn::dialogs
-{
+namespace spawn::dialogs {
#ifdef _WIN32
-std::wstring makeRightsDetails(const env::FileSecurity& fs)
-{
+std::wstring makeRightsDetails(const env::FileSecurity &fs) {
if (fs.rights.normalRights) {
return L"(normal rights)";
}
@@ -75,22 +73,22 @@ std::wstring makeRightsDetails(const env::FileSecurity& fs)
return s;
}
-QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more = {})
-{
+QString makeDetails(const SpawnParameters &sp, DWORD code,
+ const QString &more = {}) {
std::wstring owner, rights;
if (sp.binary.isFile()) {
const auto fs = env::getFileSecurity(sp.binary.absoluteFilePath());
if (fs.error.isEmpty()) {
- owner = fs.owner.toStdWString();
+ owner = fs.owner.toStdWString();
rights = makeRightsDetails(fs);
} else {
- owner = fs.error.toStdWString();
+ owner = fs.error.toStdWString();
rights = fs.error.toStdWString();
}
} else {
- owner = L"(file not found)";
+ owner = L"(file not found)";
rights = L"(file not found)";
}
@@ -98,7 +96,7 @@ QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more =
(sp.currentDirectory.isEmpty() ? true : sp.currentDirectory.exists());
const auto appDir = QCoreApplication::applicationDirPath();
- const auto sep = QDir::separator();
+ const auto sep = QDir::separator();
const std::wstring usvfs_x86_dll =
QFileInfo(appDir + sep + "usvfs_x86.dll").isFile() ? L"ok" : L"not found";
@@ -107,10 +105,12 @@ QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more =
QFileInfo(appDir + sep + "usvfs_x64.dll").isFile() ? L"ok" : L"not found";
const std::wstring usvfs_x86_proxy =
- QFileInfo(appDir + sep + "usvfs_proxy_x86.exe").isFile() ? L"ok" : L"not found";
+ QFileInfo(appDir + sep + "usvfs_proxy_x86.exe").isFile() ? L"ok"
+ : L"not found";
const std::wstring usvfs_x64_proxy =
- QFileInfo(appDir + sep + "usvfs_proxy_x64.exe").isFile() ? L"ok" : L"not found";
+ QFileInfo(appDir + sep + "usvfs_proxy_x64.exe").isFile() ? L"ok"
+ : L"not found";
std::wstring elevated;
if (auto b = env::Environment().windowsInfo().isElevated()) {
@@ -119,42 +119,44 @@ QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more =
elevated = L"(not available)";
}
- auto s = std::format(L"Error {} {}{}: {}\n"
- L" . binary: '{}'\n"
- L" . owner: {}\n"
- L" . rights: {}\n"
- L" . arguments: '{}'\n"
- L" . cwd: '{}'{}\n"
- L" . stdout: {}, stderr: {}, hooked: {}\n"
- L" . MO elevated: {}",
- code, errorCodeName(code), (more.isEmpty() ? more : ", " + more),
- formatSystemMessage(code),
- QDir::toNativeSeparators(sp.binary.absoluteFilePath()), owner,
- rights, sp.arguments,
- QDir::toNativeSeparators(sp.currentDirectory.absolutePath()),
- (cwdExists ? L"" : L" (not found)"),
- (sp.stdOut == INVALID_HANDLE_VALUE ? L"no" : L"yes"),
- (sp.stdErr == INVALID_HANDLE_VALUE ? L"no" : L"yes"),
- (sp.hooked ? L"yes" : L"no"), elevated);
+ auto s = std::format(
+ L"Error {} {}{}: {}\n"
+ L" . binary: '{}'\n"
+ L" . owner: {}\n"
+ L" . rights: {}\n"
+ L" . arguments: '{}'\n"
+ L" . cwd: '{}'{}\n"
+ L" . stdout: {}, stderr: {}, hooked: {}\n"
+ L" . MO elevated: {}",
+ code, errorCodeName(code), (more.isEmpty() ? more : ", " + more),
+ formatSystemMessage(code),
+ QDir::toNativeSeparators(sp.binary.absoluteFilePath()), owner, rights,
+ sp.arguments,
+ QDir::toNativeSeparators(sp.currentDirectory.absolutePath()),
+ (cwdExists ? L"" : L" (not found)"),
+ (sp.stdOut == INVALID_HANDLE_VALUE ? L"no" : L"yes"),
+ (sp.stdErr == INVALID_HANDLE_VALUE ? L"no" : L"yes"),
+ (sp.hooked ? L"yes" : L"no"), elevated);
if (sp.hooked) {
s += std::format(L"\n . usvfs x86:{} x64:{} proxy_x86:{} proxy_x64:{}",
- usvfs_x86_dll, usvfs_x64_dll, usvfs_x86_proxy, usvfs_x64_proxy);
+ usvfs_x86_dll, usvfs_x64_dll, usvfs_x86_proxy,
+ usvfs_x64_proxy);
}
return QString::fromStdWString(s);
}
#else
-QString makeDetails(const SpawnParameters& sp, int code, const QString& more = {})
-{
+QString makeDetails(const SpawnParameters &sp, int code,
+ const QString &more = {}) {
const bool cwdExists =
(sp.currentDirectory.isEmpty() ? true : sp.currentDirectory.exists());
QString s = QString("Error %1%2: %3\n"
- " . binary: '%4'\n"
- " . arguments: '%5'\n"
- " . cwd: '%6'%7\n"
- " . stdout: %8, stderr: %9, hooked: %10")
+ " . binary: '%4'\n"
+ " . arguments: '%5'\n"
+ " . cwd: '%6'%7\n"
+ " . stdout: %8, stderr: %9, hooked: %10")
.arg(code)
.arg(more.isEmpty() ? more : ", " + more)
.arg(QString::fromUtf8(strerror(code)))
@@ -171,19 +173,21 @@ QString makeDetails(const SpawnParameters& sp, int code, const QString& more = {
#endif
#ifdef _WIN32
-QString makeContent(const SpawnParameters& sp, DWORD code)
-{
+QString makeContent(const SpawnParameters &sp, DWORD code) {
if (code == ERROR_INVALID_PARAMETER) {
return QObject::tr(
- "This error typically happens because an antivirus has deleted critical "
+ "This error typically happens because an antivirus has deleted "
+ "critical "
"files from Mod Organizer's installation folder or has made them "
"generally inaccessible. Add an exclusion for Mod Organizer's "
- "installation folder in your antivirus, reinstall Mod Organizer and try "
+ "installation folder in your antivirus, reinstall Mod Organizer and "
+ "try "
"again.");
} else if (code == ERROR_ACCESS_DENIED) {
return QObject::tr(
"This error typically happens because an antivirus is preventing Mod "
- "Organizer from starting programs. Add an exclusion for Mod Organizer's "
+ "Organizer from starting programs. Add an exclusion for Mod "
+ "Organizer's "
"installation folder in your antivirus and try again.");
} else if (code == ERROR_FILE_NOT_FOUND) {
return QObject::tr("The file '%1' does not exist.")
@@ -198,15 +202,13 @@ QString makeContent(const SpawnParameters& sp, DWORD code)
return QString::fromStdWString(formatSystemMessage(code));
}
#else
-QString makeContent(const SpawnParameters& sp, int code)
-{
+QString makeContent(const SpawnParameters &sp, int code) {
if (code == ENOENT) {
return QObject::tr("The file '%1' does not exist.")
.arg(sp.binary.absoluteFilePath());
} else if (code == EACCES) {
- return QObject::tr(
- "Permission denied when trying to start '%1'. "
- "Check that the file is executable.")
+ return QObject::tr("Permission denied when trying to start '%1'. "
+ "Check that the file is executable.")
.arg(sp.binary.absoluteFilePath());
}
@@ -220,38 +222,39 @@ QString makeContent(const SpawnParameters& sp, int code)
#endif
#ifdef _WIN32
-QMessageBox::StandardButton badSteamReg(QWidget* parent, const QString& keyName,
- const QString& valueName)
-{
+QMessageBox::StandardButton badSteamReg(QWidget *parent, const QString &keyName,
+ const QString &valueName) {
const auto details =
- QString("can't start steam, registry value at '%1' is empty or doesn't exist")
+ QString(
+ "can't start steam, registry value at '%1' is empty or doesn't exist")
.arg(keyName + "\\" + valueName);
log::error("{}", details);
return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam"))
.main(QObject::tr("Cannot start Steam"))
- .content(
- QObject::tr("The path to the Steam executable cannot be found. You might try "
- "reinstalling Steam."))
+ .content(QObject::tr(
+ "The path to the Steam executable cannot be found. You might try "
+ "reinstalling Steam."))
.details(details)
.icon(QMessageBox::Critical)
.button({QObject::tr("Continue without starting Steam"),
- QObject::tr("The program may fail to launch."), QMessageBox::Yes})
+ QObject::tr("The program may fail to launch."),
+ QMessageBox::Yes})
.button({QObject::tr("Cancel"), QMessageBox::Cancel})
.exec();
}
#endif
#ifdef _WIN32
-QMessageBox::StandardButton startSteamFailed(QWidget* parent, const QString& keyName,
- const QString& valueName,
- const QString& exe,
- const SpawnParameters& sp, DWORD e)
-{
- auto details = QString("a steam install was found in the registry at '%1': '%2'\n\n")
- .arg(keyName + "\\" + valueName)
- .arg(exe);
+QMessageBox::StandardButton
+startSteamFailed(QWidget *parent, const QString &keyName,
+ const QString &valueName, const QString &exe,
+ const SpawnParameters &sp, DWORD e) {
+ auto details =
+ QString("a steam install was found in the registry at '%1': '%2'\n\n")
+ .arg(keyName + "\\" + valueName)
+ .arg(exe);
details += makeDetails(sp, e);
@@ -263,26 +266,27 @@ QMessageBox::StandardButton startSteamFailed(QWidget* parent, const QString& key
.details(details)
.icon(QMessageBox::Critical)
.button({QObject::tr("Continue without starting Steam"),
- QObject::tr("The program may fail to launch."), QMessageBox::Yes})
+ QObject::tr("The program may fail to launch."),
+ QMessageBox::Yes})
.button({QObject::tr("Cancel"), QMessageBox::Cancel})
.exec();
}
#endif
-void spawnFailed(QWidget* parent, const SpawnParameters& sp,
+void spawnFailed(QWidget *parent, const SpawnParameters &sp,
#ifdef _WIN32
DWORD code
#else
int code
#endif
-)
-{
+) {
const auto details = makeDetails(sp, code);
log::error("{}", details);
const auto title = QObject::tr("Cannot launch program");
- const auto mainText = QObject::tr("Cannot start %1").arg(sp.binary.fileName());
+ const auto mainText =
+ QObject::tr("Cannot start %1").arg(sp.binary.fileName());
MOBase::TaskDialog(parent, title)
.main(mainText)
@@ -293,10 +297,9 @@ void spawnFailed(QWidget* parent, const SpawnParameters& sp,
}
#ifdef _WIN32
-void helperFailed(QWidget* parent, DWORD code, const QString& why,
- const std::wstring& binary, const std::wstring& cwd,
- const std::wstring& args)
-{
+void helperFailed(QWidget *parent, DWORD code, const QString &why,
+ const std::wstring &binary, const std::wstring &cwd,
+ const std::wstring &args) {
SpawnParameters sp;
sp.binary = QFileInfo(QString::fromStdWString(binary));
sp.currentDirectory.setPath(QString::fromStdWString(cwd));
@@ -307,7 +310,8 @@ void helperFailed(QWidget* parent, DWORD code, const QString& why,
const auto title = QObject::tr("Cannot launch helper");
- const auto mainText = QObject::tr("Cannot start %1").arg(sp.binary.fileName());
+ const auto mainText =
+ QObject::tr("Cannot start %1").arg(sp.binary.fileName());
MOBase::TaskDialog(parent, title)
.main(mainText)
@@ -317,20 +321,22 @@ void helperFailed(QWidget* parent, DWORD code, const QString& why,
.exec();
}
-bool confirmRestartAsAdmin(QWidget* parent, const SpawnParameters& sp)
-{
+bool confirmRestartAsAdmin(QWidget *parent, const SpawnParameters &sp) {
const auto details = makeDetails(sp, ERROR_ELEVATION_REQUIRED);
log::error("{}", details);
const auto title = QObject::tr("Elevation required");
- const auto mainText = QObject::tr("Cannot start %1").arg(sp.binary.fileName());
+ const auto mainText =
+ QObject::tr("Cannot start %1").arg(sp.binary.fileName());
const auto content = QObject::tr(
"This program is requesting to run as administrator but Mod Organizer "
- "itself is not running as administrator. Running programs as administrator "
- "is typically unnecessary as long as the game and Mod Organizer have been "
+ "itself is not running as administrator. Running programs as "
+ "administrator "
+ "is typically unnecessary as long as the game and Mod Organizer have "
+ "been "
"installed outside \"Program Files\".\r\n\r\n"
"You can restart Mod Organizer as administrator and try launching the "
"program again.");
@@ -344,24 +350,24 @@ bool confirmRestartAsAdmin(QWidget* parent, const SpawnParameters& sp)
.details(details)
.icon(QMessageBox::Question)
.button({QObject::tr("Restart Mod Organizer as administrator"),
- QObject::tr(
- "You must allow \"helper.exe\" to make changes to the system."),
+ QObject::tr("You must allow \"helper.exe\" to make changes "
+ "to the system."),
QMessageBox::Yes})
.button({QObject::tr("Cancel"), QMessageBox::Cancel})
.exec();
return (r == QMessageBox::Yes);
}
-#endif // _WIN32
+#endif // _WIN32
-QMessageBox::StandardButton
-confirmStartSteam(QWidget* parent, const SpawnParameters& sp, const QString& details)
-{
- const auto title = QObject::tr("Launch Steam");
+QMessageBox::StandardButton confirmStartSteam(QWidget *parent,
+ const SpawnParameters &sp,
+ const QString &details) {
+ const auto title = QObject::tr("Launch Steam");
const auto mainText = QObject::tr("This program requires Steam");
- const auto content = QObject::tr(
- "Mod Organizer has detected that this program likely requires Steam to be "
- "running to function properly.");
+ const auto content = QObject::tr("Mod Organizer has detected that this "
+ "program likely requires Steam to be "
+ "running to function properly.");
return MOBase::TaskDialog(parent, title)
.main(mainText)
@@ -377,17 +383,16 @@ confirmStartSteam(QWidget* parent, const SpawnParameters& sp, const QString& det
}
#ifdef _WIN32
-QMessageBox::StandardButton confirmRestartAsAdminForSteam(QWidget* parent,
- const SpawnParameters& sp)
-{
- const auto title = QObject::tr("Elevation required");
+QMessageBox::StandardButton
+confirmRestartAsAdminForSteam(QWidget *parent, const SpawnParameters &sp) {
+ const auto title = QObject::tr("Elevation required");
const auto mainText = QObject::tr("Steam is running as administrator");
- const auto content = QObject::tr(
+ const auto content = QObject::tr(
"Running Steam as administrator is typically unnecessary and can cause "
- "problems when Mod Organizer itself is not running as administrator."
- "\r\n\r\n"
- "You can restart Mod Organizer as administrator and try launching the "
- "program again.");
+ "problems when Mod Organizer itself is not running as administrator."
+ "\r\n\r\n"
+ "You can restart Mod Organizer as administrator and try launching the "
+ "program again.");
return MOBase::TaskDialog(parent, title)
.main(mainText)
@@ -395,24 +400,25 @@ QMessageBox::StandardButton confirmRestartAsAdminForSteam(QWidget* parent,
.icon(QMessageBox::Question)
.button(
{QObject::tr("Restart Mod Organizer as administrator"),
- QObject::tr("You must allow \"helper.exe\" to make changes to the system."),
+ QObject::tr(
+ "You must allow \"helper.exe\" to make changes to the system."),
QMessageBox::Yes})
- .button({QObject::tr("Continue"), QObject::tr("The program might fail to run."),
- QMessageBox::No})
+ .button({QObject::tr("Continue"),
+ QObject::tr("The program might fail to run."), QMessageBox::No})
.button({QObject::tr("Cancel"), QMessageBox::Cancel})
.remember("steamAdminQuery", sp.binary.fileName())
.exec();
}
-bool eventLogNotRunning(QWidget* parent, const env::Service& s,
- const SpawnParameters& sp)
-{
- const auto title = QObject::tr("Event Log not running");
+bool eventLogNotRunning(QWidget *parent, const env::Service &s,
+ const SpawnParameters &sp) {
+ const auto title = QObject::tr("Event Log not running");
const auto mainText = QObject::tr("The Event Log service is not running");
- const auto content = QObject::tr(
- "The Windows Event Log service is not running. This can prevent USVFS from "
- "running properly and your mods may not be recognized by the program being "
- "launched.");
+ const auto content = QObject::tr("The Windows Event Log service is not "
+ "running. This can prevent USVFS from "
+ "running properly and your mods may not be "
+ "recognized by the program being "
+ "launched.");
const auto r =
MOBase::TaskDialog(parent, title)
@@ -421,18 +427,18 @@ bool eventLogNotRunning(QWidget* parent, const env::Service& s,
.details(s.toString())
.icon(QMessageBox::Question)
.remember("eventLogService", sp.binary.fileName())
- .button({QObject::tr("Continue"), QObject::tr("Your mods might not work."),
- QMessageBox::Yes})
+ .button({QObject::tr("Continue"),
+ QObject::tr("Your mods might not work."), QMessageBox::Yes})
.button({QObject::tr("Cancel"), QMessageBox::Cancel})
.exec();
return (r == QMessageBox::Yes);
}
-#endif // _WIN32
+#endif // _WIN32
-QMessageBox::StandardButton
-confirmBlacklisted(QWidget* parent, const SpawnParameters& sp, Settings& settings)
-{
+QMessageBox::StandardButton confirmBlacklisted(QWidget *parent,
+ const SpawnParameters &sp,
+ Settings &settings) {
const auto title = QObject::tr("Blacklisted program");
const auto mainText =
QObject::tr("The program %1 is blacklisted").arg(sp.binary.fileName());
@@ -446,17 +452,18 @@ confirmBlacklisted(QWidget* parent, const SpawnParameters& sp, Settings& setting
"Current blacklist: " +
settings.executablesBlacklist();
- auto r = MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(content)
- .details(details)
- .icon(QMessageBox::Question)
- .remember("blacklistedExecutable", sp.binary.fileName())
- .button({QObject::tr("Continue"),
- QObject::tr("Your mods might not work."), QMessageBox::Yes})
- .button({QObject::tr("Change the blacklist"), QMessageBox::Retry})
- .button({QObject::tr("Cancel"), QMessageBox::Cancel})
- .exec();
+ auto r =
+ MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(content)
+ .details(details)
+ .icon(QMessageBox::Question)
+ .remember("blacklistedExecutable", sp.binary.fileName())
+ .button({QObject::tr("Continue"),
+ QObject::tr("Your mods might not work."), QMessageBox::Yes})
+ .button({QObject::tr("Change the blacklist"), QMessageBox::Retry})
+ .button({QObject::tr("Cancel"), QMessageBox::Cancel})
+ .exec();
if (r == QMessageBox::Retry) {
if (!WorkaroundsSettingsTab::changeBlacklistNow(parent, settings)) {
@@ -467,13 +474,11 @@ confirmBlacklisted(QWidget* parent, const SpawnParameters& sp, Settings& setting
return r;
}
-} // namespace spawn::dialogs
+} // namespace spawn::dialogs
-namespace spawn
-{
+namespace spawn {
-void logSpawning(const SpawnParameters& sp, const QString& realCmd)
-{
+void logSpawning(const SpawnParameters &sp, const QString &realCmd) {
log::debug("spawning binary:\n"
" . exe: '{}'\n"
" . args: '{}'\n"
@@ -489,23 +494,21 @@ void logSpawning(const SpawnParameters& sp, const QString& realCmd)
(sp.stdOut == INVALID_HANDLE_VALUE ? "no" : "yes"),
(sp.stdErr == INVALID_HANDLE_VALUE ? "no" : "yes"),
#else
- (sp.stdOut == -1 ? "no" : "yes"),
- (sp.stdErr == -1 ? "no" : "yes"),
+ (sp.stdOut == -1 ? "no" : "yes"), (sp.stdErr == -1 ? "no" : "yes"),
#endif
realCmd);
}
#ifndef _WIN32
-uint32_t parseSteamAppId(const QString& steamAppId)
-{
- bool ok = false;
+uint32_t parseSteamAppId(const QString &steamAppId) {
+ bool ok = false;
const auto n = steamAppId.toUInt(&ok);
return (ok ? n : 0u);
}
-QString firstExistingSetting(const QSettings& settings, const QStringList& keys)
-{
- for (const QString& key : keys) {
+QString firstExistingSetting(const QSettings &settings,
+ const QStringList &keys) {
+ for (const QString &key : keys) {
const QString value = settings.value(key).toString().trimmed();
if (!value.isEmpty()) {
return value;
@@ -515,13 +518,13 @@ QString firstExistingSetting(const QSettings& settings, const QStringList& keys)
return {};
}
-QString resolvePrefixPath()
-{
- if (auto cfg = FluorineConfig::load(); cfg.has_value() && cfg->prefixExists()) {
+QString resolvePrefixPath() {
+ if (auto cfg = FluorineConfig::load();
+ cfg.has_value() && cfg->prefixExists()) {
return cfg->prefix_path.trimmed();
}
- const Settings* settings = Settings::maybeInstance();
+ const Settings *settings = Settings::maybeInstance();
if (settings == nullptr) {
return {};
}
@@ -532,8 +535,7 @@ QString resolvePrefixPath()
"Proton/prefix_path", "fluorine/prefix_path"});
}
-QString resolveProtonPath()
-{
+QString resolveProtonPath() {
if (auto cfg = FluorineConfig::load(); cfg.has_value()) {
const QString protonPath = cfg->proton_path.trimmed();
if (!protonPath.isEmpty()) {
@@ -541,35 +543,34 @@ QString resolveProtonPath()
}
}
- const Settings* settings = Settings::maybeInstance();
+ const Settings *settings = Settings::maybeInstance();
if (settings == nullptr) {
return {};
}
const QSettings instanceSettings(settings->filename(), QSettings::IniFormat);
- return firstExistingSetting(instanceSettings,
- {"Settings/proton_path", "Proton/path",
- "fluorine/proton_path"});
+ return firstExistingSetting(
+ instanceSettings,
+ {"Settings/proton_path", "Proton/path", "fluorine/proton_path"});
}
#endif
#ifdef _WIN32
-DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle)
-{
+DWORD spawn(const SpawnParameters &sp, HANDLE &processHandle) {
BOOL inheritHandles = FALSE;
STARTUPINFO si = {};
- si.cb = sizeof(si);
+ si.cb = sizeof(si);
// inherit handles if we plan to use stdout or stderr reroute
if (sp.stdOut != INVALID_HANDLE_VALUE) {
- si.hStdOutput = sp.stdOut;
+ si.hStdOutput = sp.stdOut;
inheritHandles = TRUE;
si.dwFlags |= STARTF_USESTDHANDLES;
}
if (sp.stdErr != INVALID_HANDLE_VALUE) {
- si.hStdError = sp.stdErr;
+ si.hStdError = sp.stdErr;
inheritHandles = TRUE;
si.dwFlags |= STARTF_USESTDHANDLES;
}
@@ -583,26 +584,26 @@ DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle)
}
const QString moPath = QCoreApplication::applicationDirPath();
- const auto oldPath = env::appendToPath(QDir::toNativeSeparators(moPath));
+ const auto oldPath = env::appendToPath(QDir::toNativeSeparators(moPath));
PROCESS_INFORMATION pi = {};
- BOOL success = FALSE;
+ BOOL success = FALSE;
logSpawning(sp, commandLine);
const auto wcommandLine = commandLine.toStdWString();
- const auto wcwd = cwd.toStdWString();
+ const auto wcwd = cwd.toStdWString();
const DWORD flags = CREATE_BREAKAWAY_FROM_JOB;
if (sp.hooked) {
success = ::usvfsCreateProcessHooked(
- nullptr, const_cast<wchar_t*>(wcommandLine.c_str()), nullptr, nullptr,
+ nullptr, const_cast<wchar_t *>(wcommandLine.c_str()), nullptr, nullptr,
inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi);
} else {
- success = ::CreateProcess(nullptr, const_cast<wchar_t*>(wcommandLine.c_str()),
- nullptr, nullptr, inheritHandles, flags, nullptr,
- wcwd.c_str(), &si, &pi);
+ success = ::CreateProcess(
+ nullptr, const_cast<wchar_t *>(wcommandLine.c_str()), nullptr, nullptr,
+ inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi);
}
const auto e = GetLastError();
@@ -618,10 +619,11 @@ DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle)
return ERROR_SUCCESS;
}
#else
-int spawn(const SpawnParameters& sp, pid_t& processId)
-{
- const QString bin = MOBase::normalizePathForHost(sp.binary.absoluteFilePath());
- QString cwd = MOBase::normalizePathForHost(sp.currentDirectory.absolutePath());
+int spawn(const SpawnParameters &sp, pid_t &processId) {
+ const QString bin =
+ MOBase::normalizePathForHost(sp.binary.absoluteFilePath());
+ QString cwd =
+ MOBase::normalizePathForHost(sp.currentDirectory.absolutePath());
QStringList argList;
if (!sp.arguments.isEmpty()) {
@@ -634,6 +636,21 @@ int spawn(const SpawnParameters& sp, pid_t& processId)
logSpawning(sp, bin + " " + sp.arguments);
+ // Read per-instance settings from the instance INI (not the global
+ // QSettings).
+ const Settings *instanceForLaunch = Settings::maybeInstance();
+ bool useSteamDrm = true; // default
+ QString storeVariant;
+ if (instanceForLaunch) {
+ const QSettings instanceIni(instanceForLaunch->filename(),
+ QSettings::IniFormat);
+ useSteamDrm = instanceIni.value("fluorine/steam_drm", true).toBool();
+ // settingName() maps "General" section keys to top-level (no section
+ // prefix), so game_edition is stored as a plain key under [General] in the
+ // INI.
+ storeVariant = instanceIni.value("game_edition").toString().trimmed();
+ }
+
ProtonLauncher launcher;
launcher.setBinary(bin)
.setArguments(argList)
@@ -644,14 +661,18 @@ int spawn(const SpawnParameters& sp, pid_t& processId)
QSettings().value("fluorine/prefer_system_umu", false).toBool())
.setUseSteamRun(
QSettings().value("fluorine/use_steam_run", false).toBool())
- .setSteamDrm(QSettings().value("fluorine/steam_drm", true).toBool());
+ .setSteamDrm(useSteamDrm)
+ .setStoreVariant(storeVariant);
const QString prefixPath = resolvePrefixPath();
if (prefixPath.isEmpty()) {
- MOBase::log::warn("No Wine prefix configured - games may not launch correctly. "
- "Configure a prefix in Settings > Proton or via fluorine-manager.");
+ MOBase::log::warn(
+ "No Wine prefix configured - games may not launch correctly. "
+ "Configure a prefix in Settings > Proton or via fluorine-manager.");
} else if (!QDir(QDir(prefixPath).filePath("drive_c")).exists()) {
- MOBase::log::warn("Wine prefix '{}' does not contain drive_c/ - prefix may be invalid", prefixPath);
+ MOBase::log::warn(
+ "Wine prefix '{}' does not contain drive_c/ - prefix may be invalid",
+ prefixPath);
} else {
MOBase::log::info("Using Wine prefix: {}", prefixPath);
launcher.setPrefix(prefixPath);
@@ -662,7 +683,8 @@ int spawn(const SpawnParameters& sp, pid_t& processId)
launcher.setProtonPath(protonPath);
}
- const QString wrapper = QSettings().value("fluorine/launch_wrapper").toString().trimmed();
+ const QString wrapper =
+ QSettings().value("fluorine/launch_wrapper").toString().trimmed();
if (!wrapper.isEmpty()) {
launcher.setWrapper(wrapper);
}
@@ -678,8 +700,7 @@ int spawn(const SpawnParameters& sp, pid_t& processId)
#endif
#ifdef _WIN32
-bool restartAsAdmin(QWidget* parent)
-{
+bool restartAsAdmin(QWidget *parent) {
WCHAR cwd[MAX_PATH] = {};
if (!GetCurrentDirectory(MAX_PATH, cwd)) {
cwd[0] = L'\0';
@@ -698,8 +719,7 @@ bool restartAsAdmin(QWidget* parent)
return true;
}
-void startBinaryAdmin(QWidget* parent, const SpawnParameters& sp)
-{
+void startBinaryAdmin(QWidget *parent, const SpawnParameters &sp) {
if (!dialogs::confirmRestartAsAdmin(parent, sp)) {
log::debug("user declined");
return;
@@ -708,25 +728,23 @@ void startBinaryAdmin(QWidget* parent, const SpawnParameters& sp)
log::info("restarting MO as administrator");
restartAsAdmin(parent);
}
-#endif // _WIN32
+#endif // _WIN32
-struct SteamStatus
-{
- bool running = false;
+struct SteamStatus {
+ bool running = false;
bool accessible = false;
};
-SteamStatus getSteamStatus()
-{
+SteamStatus getSteamStatus() {
SteamStatus ss;
#ifdef _WIN32
const auto ps = env::Environment().runningProcesses();
- for (const auto& p : ps) {
+ for (const auto &p : ps) {
if ((p.name().compare("Steam.exe", Qt::CaseInsensitive) == 0) ||
(p.name().compare("SteamService.exe", Qt::CaseInsensitive) == 0)) {
- ss.running = true;
+ ss.running = true;
ss.accessible = p.canAccess();
log::debug("'{}' is running, accessible={}", p.name(),
@@ -742,7 +760,7 @@ SteamStatus getSteamStatus()
pgrep.waitForFinished(3000);
if (pgrep.exitCode() == 0) {
- ss.running = true;
+ ss.running = true;
ss.accessible = true;
log::debug("steam is running");
}
@@ -751,8 +769,7 @@ SteamStatus getSteamStatus()
return ss;
}
-QString makeSteamArguments(const QString& username, const QString& password)
-{
+QString makeSteamArguments(const QString &username, const QString &password) {
QString args;
if (username != "") {
@@ -766,17 +783,17 @@ QString makeSteamArguments(const QString& username, const QString& password)
return args;
}
-bool startSteam(QWidget* parent)
-{
+bool startSteam(QWidget *parent) {
#ifdef _WIN32
- const QString keyName = "HKEY_CURRENT_USER\\Software\\Valve\\Steam";
+ const QString keyName = "HKEY_CURRENT_USER\\Software\\Valve\\Steam";
const QString valueName = "SteamExe";
const QSettings steamSettings(keyName, QSettings::NativeFormat);
const QString exe = steamSettings.value(valueName, "").toString();
if (exe.isEmpty()) {
- return (dialogs::badSteamReg(parent, keyName, valueName) == QMessageBox::Yes);
+ return (dialogs::badSteamReg(parent, keyName, valueName) ==
+ QMessageBox::Yes);
}
SpawnParameters sp;
@@ -797,10 +814,11 @@ bool startSteam(QWidget* parent)
log::debug("starting steam process:\n"
" . program: '{}'\n"
" . username={}, password={}",
- sp.binary.filePath().toStdString(), (username.isEmpty() ? "no" : "yes"),
+ sp.binary.filePath().toStdString(),
+ (username.isEmpty() ? "no" : "yes"),
(password.isEmpty() ? "no" : "yes"));
- HANDLE ph = INVALID_HANDLE_VALUE;
+ HANDLE ph = INVALID_HANDLE_VALUE;
const auto e = spawn(sp, ph);
::CloseHandle(ph);
@@ -809,7 +827,8 @@ bool startSteam(QWidget* parent)
sp.arguments = makeSteamArguments((username.isEmpty() ? "" : "USERNAME"),
(password.isEmpty() ? "" : "PASSWORD"));
- const auto r = dialogs::startSteamFailed(parent, keyName, valueName, exe, sp, e);
+ const auto r =
+ dialogs::startSteamFailed(parent, keyName, valueName, exe, sp, e);
return (r == QMessageBox::Yes);
}
@@ -840,10 +859,11 @@ bool startSteam(QWidget* parent)
MOBase::TaskDialog(parent, title)
.main(title)
.content(QObject::tr("The Steam executable could not be found. "
- "Make sure Steam is installed."))
+ "Make sure Steam is installed."))
.icon(QMessageBox::Critical)
.button({QObject::tr("Continue without starting Steam"),
- QObject::tr("The program may fail to launch."), QMessageBox::Yes})
+ QObject::tr("The program may fail to launch."),
+ QMessageBox::Yes})
.button({QObject::tr("Cancel"), QMessageBox::Cancel})
.exec();
@@ -869,11 +889,12 @@ bool startSteam(QWidget* parent)
#endif
}
-bool checkSteam(QWidget* parent, const SpawnParameters& sp, const QDir& gameDirectory,
- const QString& steamAppID, const Settings& settings)
-{
+bool checkSteam(QWidget *parent, const SpawnParameters &sp,
+ const QDir &gameDirectory, const QString &steamAppID,
+ const Settings &settings) {
#ifdef _WIN32
- static const std::vector<QString> steamFiles = {"steam_api.dll", "steam_api64.dll"};
+ static const std::vector<QString> steamFiles = {"steam_api.dll",
+ "steam_api64.dll"};
#else
static const std::vector<QString> steamFiles = {"libsteam_api.so"};
#endif
@@ -889,7 +910,7 @@ bool checkSteam(QWidget* parent, const SpawnParameters& sp, const QDir& gameDire
bool steamRequired = false;
QString details;
- for (const auto& file : steamFiles) {
+ for (const auto &file : steamFiles) {
const QFileInfo fi(gameDirectory.absoluteFilePath(file));
if (fi.exists()) {
details = QString("managed game is located at '%1' and file '%2' exists")
@@ -959,8 +980,8 @@ bool checkSteam(QWidget* parent, const SpawnParameters& sp, const QDir& gameDire
return true;
}
-bool checkBlacklist(QWidget* parent, const SpawnParameters& sp, Settings& settings)
-{
+bool checkBlacklist(QWidget *parent, const SpawnParameters &sp,
+ Settings &settings) {
for (;;) {
if (!settings.isExecutableBlacklisted(sp.binary.fileName())) {
return true;
@@ -975,10 +996,9 @@ bool checkBlacklist(QWidget* parent, const SpawnParameters& sp, Settings& settin
}
#ifdef _WIN32
-HANDLE startBinary(QWidget* parent, const SpawnParameters& sp)
-{
+HANDLE startBinary(QWidget *parent, const SpawnParameters &sp) {
HANDLE handle = INVALID_HANDLE_VALUE;
- const auto e = spawn::spawn(sp, handle);
+ const auto e = spawn::spawn(sp, handle);
switch (e) {
case ERROR_SUCCESS: {
@@ -997,8 +1017,7 @@ HANDLE startBinary(QWidget* parent, const SpawnParameters& sp)
}
}
#else
-pid_t startBinary(QWidget* parent, const SpawnParameters& sp)
-{
+pid_t startBinary(QWidget *parent, const SpawnParameters &sp) {
pid_t pid = -1;
const auto e = spawn::spawn(sp, pid);
@@ -1012,19 +1031,18 @@ pid_t startBinary(QWidget* parent, const SpawnParameters& sp)
#endif
#ifdef _WIN32
-QString getExecutableForJarFile(const QString& jarFile)
-{
+QString getExecutableForJarFile(const QString &jarFile) {
const std::wstring jarFileW = jarFile.toStdWString();
WCHAR buffer[MAX_PATH];
const auto hinst = ::FindExecutableW(jarFileW.c_str(), nullptr, buffer);
- const auto r = static_cast<int>(reinterpret_cast<std::uintptr_t>(hinst));
+ const auto r = static_cast<int>(reinterpret_cast<std::uintptr_t>(hinst));
// anything <= 32 signals failure
if (r <= 32) {
- log::warn("failed to find executable associated with file '{}', {}", jarFile,
- shell::formatError(r));
+ log::warn("failed to find executable associated with file '{}', {}",
+ jarFile, shell::formatError(r));
return {};
}
@@ -1050,8 +1068,7 @@ QString getExecutableForJarFile(const QString& jarFile)
return QString::fromWCharArray(buffer);
}
-QString getJavaHome()
-{
+QString getJavaHome() {
const QString key =
"HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment";
const QString value = "CurrentVersion";
@@ -1064,11 +1081,12 @@ QString getJavaHome()
}
const QString currentVersion = reg.value("CurrentVersion").toString();
- const QString javaHome = QString("%1/JavaHome").arg(currentVersion);
+ const QString javaHome = QString("%1/JavaHome").arg(currentVersion);
if (!reg.contains(javaHome)) {
- log::warn("java version '{}' was found at '{}\\{}', but '{}\\{}' doesn't exist",
- currentVersion, key, value, key, javaHome);
+ log::warn(
+ "java version '{}' was found at '{}\\{}', but '{}\\{}' doesn't exist",
+ currentVersion, key, value, key, javaHome);
return {};
}
@@ -1076,10 +1094,9 @@ QString getJavaHome()
const auto path = reg.value(javaHome).toString();
return path + "\\bin\\javaw.exe";
}
-#endif // _WIN32
+#endif // _WIN32
-QString findJavaInstallation(const QString& jarFile)
-{
+QString findJavaInstallation(const QString &jarFile) {
#ifdef _WIN32
// try to find java automatically based on the given jar file
if (!jarFile.isEmpty()) {
@@ -1108,8 +1125,7 @@ QString findJavaInstallation(const QString& jarFile)
return {};
}
-bool isBatchFile(const QFileInfo& target)
-{
+bool isBatchFile(const QFileInfo &target) {
#ifdef _WIN32
const auto batchExtensions = {"cmd", "bat"};
#else
@@ -1117,7 +1133,7 @@ bool isBatchFile(const QFileInfo& target)
#endif
const QString extension = target.suffix();
- for (auto&& e : batchExtensions) {
+ for (auto &&e : batchExtensions) {
if (extension.compare(e, Qt::CaseInsensitive) == 0) {
return true;
}
@@ -1126,8 +1142,7 @@ bool isBatchFile(const QFileInfo& target)
return false;
}
-bool isExeFile(const QFileInfo& target)
-{
+bool isExeFile(const QFileInfo &target) {
#ifdef _WIN32
return (target.suffix().compare("exe", Qt::CaseInsensitive) == 0);
#else
@@ -1136,13 +1151,11 @@ bool isExeFile(const QFileInfo& target)
#endif
}
-bool isJavaFile(const QFileInfo& target)
-{
+bool isJavaFile(const QFileInfo &target) {
return (target.suffix().compare("jar", Qt::CaseInsensitive) == 0);
}
-QFileInfo getCmdPath()
-{
+QFileInfo getCmdPath() {
#ifdef _WIN32
const auto p = env::get("COMSPEC");
if (!p.isEmpty()) {
@@ -1151,7 +1164,7 @@ QFileInfo getCmdPath()
QString systemDirectory;
- const std::size_t buffer_size = 1000;
+ const std::size_t buffer_size = 1000;
wchar_t buffer[buffer_size + 1] = {};
const auto length = ::GetSystemDirectoryW(buffer, buffer_size);
@@ -1176,8 +1189,7 @@ QFileInfo getCmdPath()
#endif
}
-FileExecutionTypes getFileExecutionType(const QFileInfo& target)
-{
+FileExecutionTypes getFileExecutionType(const QFileInfo &target) {
if (isExeFile(target) || isBatchFile(target) || isJavaFile(target)) {
return FileExecutionTypes::Executable;
}
@@ -1185,23 +1197,21 @@ FileExecutionTypes getFileExecutionType(const QFileInfo& target)
return FileExecutionTypes::Other;
}
-FileExecutionContext getFileExecutionContext(QWidget* parent, const QFileInfo& target)
-{
+FileExecutionContext getFileExecutionContext(QWidget *parent,
+ const QFileInfo &target) {
if (isExeFile(target)) {
return {target, "", FileExecutionTypes::Executable};
}
if (isBatchFile(target)) {
#ifdef _WIN32
- return {
- getCmdPath(),
- QString("/C \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())),
- FileExecutionTypes::Executable};
+ return {getCmdPath(),
+ QString("/C \"%1\"")
+ .arg(QDir::toNativeSeparators(target.absoluteFilePath())),
+ FileExecutionTypes::Executable};
#else
- return {
- getCmdPath(),
- QString("\"%1\"").arg(target.absoluteFilePath()),
- FileExecutionTypes::Executable};
+ return {getCmdPath(), QString("\"%1\"").arg(target.absoluteFilePath()),
+ FileExecutionTypes::Executable};
#endif
}
@@ -1210,13 +1220,13 @@ FileExecutionContext getFileExecutionContext(QWidget* parent, const QFileInfo& t
if (java.isEmpty()) {
#ifdef _WIN32
- java =
- QFileDialog::getOpenFileName(parent, QObject::tr("Select binary"), QString(),
- QObject::tr("Binary") + " (*.exe)");
+ java = QFileDialog::getOpenFileName(parent, QObject::tr("Select binary"),
+ QString(),
+ QObject::tr("Binary") + " (*.exe)");
#else
- java =
- QFileDialog::getOpenFileName(parent, QObject::tr("Select binary"), QString(),
- QObject::tr("Binary") + " (*)");
+ java = QFileDialog::getOpenFileName(parent, QObject::tr("Select binary"),
+ QString(),
+ QObject::tr("Binary") + " (*)");
#endif
}
@@ -1231,15 +1241,13 @@ FileExecutionContext getFileExecutionContext(QWidget* parent, const QFileInfo& t
return {{}, {}, FileExecutionTypes::Other};
}
-} // namespace spawn
+} // namespace spawn
#ifdef _WIN32
-namespace helper
-{
+namespace helper {
-bool helperExec(QWidget* parent, const std::wstring& moDirectory,
- const std::wstring& commandLine, BOOL async)
-{
+bool helperExec(QWidget *parent, const std::wstring &moDirectory,
+ const std::wstring &commandLine, BOOL async) {
const std::wstring fileName = moDirectory + L"\\helper.exe";
env::HandlePtr process;
@@ -1251,14 +1259,14 @@ bool helperExec(QWidget* parent, const std::wstring& moDirectory,
if (!async)
flags |= SEE_MASK_NOCLOSEPROCESS;
- execInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
- execInfo.fMask = flags;
- execInfo.hwnd = 0;
- execInfo.lpVerb = L"runas";
- execInfo.lpFile = fileName.c_str();
+ execInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
+ execInfo.fMask = flags;
+ execInfo.hwnd = 0;
+ execInfo.lpVerb = L"runas";
+ execInfo.lpFile = fileName.c_str();
execInfo.lpParameters = commandLine.c_str();
- execInfo.lpDirectory = moDirectory.c_str();
- execInfo.nShow = SW_SHOW;
+ execInfo.lpDirectory = moDirectory.c_str();
+ execInfo.nShow = SW_SHOW;
if (!::ShellExecuteExW(&execInfo) && execInfo.hProcess == 0) {
const auto e = GetLastError();
@@ -1282,10 +1290,11 @@ bool helperExec(QWidget* parent, const std::wstring& moDirectory,
// for WAIT_ABANDONED, the documentation doesn't mention that GetLastError()
// returns something meaningful, but code ERROR_ABANDONED_WAIT_0 exists, so
// use that instead
- const auto code = (r == WAIT_ABANDONED ? ERROR_ABANDONED_WAIT_0 : GetLastError());
+ const auto code =
+ (r == WAIT_ABANDONED ? ERROR_ABANDONED_WAIT_0 : GetLastError());
- spawn::dialogs::helperFailed(parent, code, "WaitForSingleObject()", fileName,
- moDirectory, commandLine);
+ spawn::dialogs::helperFailed(parent, code, "WaitForSingleObject()",
+ fileName, moDirectory, commandLine);
return false;
}
@@ -1303,22 +1312,21 @@ bool helperExec(QWidget* parent, const std::wstring& moDirectory,
return (exitCode == 0);
}
-bool backdateBSAs(QWidget* parent, const std::wstring& moPath,
- const std::wstring& dataPath)
-{
+bool backdateBSAs(QWidget *parent, const std::wstring &moPath,
+ const std::wstring &dataPath) {
const std::wstring commandLine = std::format(L"backdateBSA \"{}\"", dataPath);
return helperExec(parent, moPath, commandLine, FALSE);
}
-bool adminLaunch(QWidget* parent, const std::wstring& moPath,
- const std::wstring& moFile, const std::wstring& workingDir)
-{
- const std::wstring commandLine = std::format(
- L"adminLaunch {} \"{}\" \"{}\"", ::GetCurrentProcessId(), moFile, workingDir);
+bool adminLaunch(QWidget *parent, const std::wstring &moPath,
+ const std::wstring &moFile, const std::wstring &workingDir) {
+ const std::wstring commandLine =
+ std::format(L"adminLaunch {} \"{}\" \"{}\"", ::GetCurrentProcessId(),
+ moFile, workingDir);
return helperExec(parent, moPath, commandLine, true);
}
-} // namespace helper
-#endif // _WIN32
+} // namespace helper
+#endif // _WIN32
diff --git a/src/src/wineprefix.cpp b/src/src/wineprefix.cpp
index bb4e166..e144460 100644
--- a/src/src/wineprefix.cpp
+++ b/src/src/wineprefix.cpp
@@ -10,10 +10,17 @@
#include <log.h>
#include <uibase/filesystemutilities.h>
+#ifndef _WIN32
+#include <sys/stat.h>
+#include <utime.h>
+#endif
+
namespace
{
constexpr const char* BackupIniSuffix = ".mo2linux_backup";
+// Copy a file, preserving its modification time so that games see the
+// original save timestamps rather than "now".
bool copyFileWithParents(const QString& source, const QString& destination)
{
const QFileInfo destinationInfo(destination);
@@ -25,7 +32,23 @@ bool copyFileWithParents(const QString& source, const QString& destination)
return false;
}
- return QFile::copy(source, destination);
+ if (!QFile::copy(source, destination)) {
+ return false;
+ }
+
+#ifndef _WIN32
+ // QFile::copy() does not preserve timestamps. Restore the source file's
+ // mtime so that games display the correct save date.
+ struct stat srcStat;
+ if (stat(source.toUtf8().constData(), &srcStat) == 0) {
+ struct utimbuf times;
+ times.actime = srcStat.st_atime;
+ times.modtime = srcStat.st_mtime;
+ utime(destination.toUtf8().constData(), &times);
+ }
+#endif
+
+ return true;
}
bool copyTreeContents(const QString& sourceRoot, const QString& destinationRoot)
@@ -49,16 +72,22 @@ bool restoreBackedUpSaves(const QString& liveUpper, const QString& liveLower,
const QString& backupUpper, const QString& backupLower)
{
if (QDir(liveUpper).exists() && !QDir(liveUpper).removeRecursively()) {
+ MOBase::log::warn("restoreBackedUpSaves: failed to remove '{}'", liveUpper);
return false;
}
if (QDir(liveLower).exists() && !QDir(liveLower).removeRecursively()) {
+ MOBase::log::warn("restoreBackedUpSaves: failed to remove '{}'", liveLower);
return false;
}
if (QDir(backupUpper).exists() && !QDir().rename(backupUpper, liveUpper)) {
+ MOBase::log::warn("restoreBackedUpSaves: failed to rename '{}' -> '{}'",
+ backupUpper, liveUpper);
return false;
}
if (QDir(backupLower).exists() && !QDir().rename(backupLower, liveLower)) {
+ MOBase::log::warn("restoreBackedUpSaves: failed to rename '{}' -> '{}'",
+ backupLower, liveLower);
return false;
}
@@ -296,16 +325,21 @@ bool WinePrefix::deployProfileSaves(const QString& profileSaveDir,
if ((QDir(backupUpper).exists() || QDir(backupLower).exists()) &&
!restoreBackedUpSaves(absoluteSaveDir, lowerSaveDir,
backupUpper, backupLower)) {
+ MOBase::log::warn("deployProfileSaves: failed to restore stale backup");
return false;
}
if (QDir(absoluteSaveDir).exists() &&
!QDir().rename(absoluteSaveDir, backupUpper)) {
+ MOBase::log::warn("deployProfileSaves: failed to backup '{}' -> '{}'",
+ absoluteSaveDir, backupUpper);
return false;
}
if (absoluteSaveDir != lowerSaveDir &&
QDir(lowerSaveDir).exists() &&
!QDir().rename(lowerSaveDir, backupLower)) {
+ MOBase::log::warn("deployProfileSaves: failed to backup '{}' -> '{}'",
+ lowerSaveDir, backupLower);
return false;
}
}