diff options
| -rw-r--r-- | data/com.fluorine.manager.metainfo.xml | 1 | ||||
| -rw-r--r-- | docker/AppRun.sh | 5 | ||||
| -rwxr-xr-x | docker/build-inner.sh | 5 | ||||
| -rw-r--r-- | docs/FAQ.md | 3 | ||||
| -rw-r--r-- | libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp | 21 | ||||
| -rw-r--r-- | libs/game_bethesda/src/gamebryo/gamegamebryo.cpp | 21 | ||||
| -rw-r--r-- | libs/nak/src/installers/prefix_setup.rs | 20 | ||||
| -rw-r--r-- | libs/nak/src/runtime_wrap.rs | 39 | ||||
| -rw-r--r-- | libs/uibase/include/uibase/registry.h | 6 | ||||
| -rw-r--r-- | libs/uibase/src/registry.cpp | 81 | ||||
| -rw-r--r-- | libs/uibase/src/utility.cpp | 41 | ||||
| -rw-r--r-- | src/src/CMakeLists.txt | 38 | ||||
| -rw-r--r-- | src/src/commandline.cpp | 4 | ||||
| -rw-r--r-- | src/src/organizercore.cpp | 28 | ||||
| -rw-r--r-- | src/src/protonlauncher.cpp | 231 | ||||
| -rw-r--r-- | src/src/protonlauncher.h | 7 | ||||
| -rw-r--r-- | src/src/settingsdialog.ui | 23 | ||||
| -rw-r--r-- | src/src/settingsdialogproton.cpp | 36 | ||||
| -rw-r--r-- | src/src/settingsdialogproton.h | 1 | ||||
| -rw-r--r-- | src/src/spawn.cpp | 3 |
20 files changed, 216 insertions, 398 deletions
diff --git a/data/com.fluorine.manager.metainfo.xml b/data/com.fluorine.manager.metainfo.xml index 1200d3e..814607f 100644 --- a/data/com.fluorine.manager.metainfo.xml +++ b/data/com.fluorine.manager.metainfo.xml @@ -21,7 +21,6 @@ <li>Mod conflict detection and resolution</li> <li>Nexus Mods integration for downloading</li> <li>Proton/Wine prefix management for running Windows games</li> - <li>UMU launcher support</li> </ul> </description> diff --git a/docker/AppRun.sh b/docker/AppRun.sh index f0ae807..e897309 100644 --- a/docker/AppRun.sh +++ b/docker/AppRun.sh @@ -89,6 +89,9 @@ if [ -d "${APPIMAGE_DIR}/plugins/plugin_python" ]; then fi # ── Environment ── +# Save original LD_LIBRARY_PATH so child processes (xdg-open, kde-open, etc.) +# can use host libraries instead of the bundled (potentially older) ones. +export FLUORINE_ORIG_LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}" export PATH="${HERE}/usr/bin:${HERE}/usr/libexec:${PATH}" export LD_LIBRARY_PATH="${HERE}/usr/lib:${HERE}/usr/libexec:${LD_LIBRARY_PATH:-}" @@ -117,7 +120,7 @@ else export MO2_PYTHON_DIR="${APPIMAGE_DIR}/python" fi # Do not export PYTHONHOME/PYTHONPATH globally here. MO2 sets Python runtime -# internally for plugin_python, while child processes (umu/NaK/launchers) must +# internally for plugin_python, while child processes (NaK/launchers) must # use their own system Python environment. unset PYTHONHOME PYTHONPATH PYTHONNOUSERSITE diff --git a/docker/build-inner.sh b/docker/build-inner.sh index fa44a60..8b7ac93 100755 --- a/docker/build-inner.sh +++ b/docker/build-inner.sh @@ -39,7 +39,6 @@ mkdir -p "${OUT_DIR}/plugins" "${OUT_DIR}/dlls" "${OUT_DIR}/lib" # ── Main binary + helpers ── cp -f "${RUNDIR}/ModOrganizer" "${OUT_DIR}/ModOrganizer-core" -[ -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}/" @@ -334,7 +333,7 @@ export MO2_PLUGINS_DIR="${HERE}/plugins" export MO2_DLLS_DIR="${HERE}/dlls" 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 +# Proton which has its own Python). MO2_PYTHON_DIR lets the # binary reconstruct it internally. MO2_PYTHONHOME="${PYTHON_DST}" unset PYTHONPATH PYTHONNOUSERSITE PYTHONHOME @@ -443,7 +442,7 @@ if [ -d "${PYTHON_SRC}" ]; then 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 +# Without this, AppImage's LD_LIBRARY_PATH/PATH leak into Proton and # cause library conflicts that make games crash. export FLUORINE_ORIG_LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}" export FLUORINE_ORIG_PATH="${PATH}" diff --git a/docs/FAQ.md b/docs/FAQ.md index e47f5b5..c14583d 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -6,9 +6,6 @@ Logs are written next to the app binary in a `logs/` folder. ## Does Removing an Instance Delete My Files? Not by default. It removes the profile from the menu, and gives you the option to delete it if you want to. -## Do I Need UMU and 7ZZ? -Yes, both are bundled in release artifacts under `bin/`. - ## Do I Need to Install 9 Million Different Dependencies? No, the dependencies are handled by NaK! If there is something missing I will gladly add it to the list. This also includes WINEDLLOVERWRITES as well! diff --git a/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp b/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp index 4c1199c..2daf2c3 100644 --- a/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp +++ b/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp @@ -23,21 +23,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include <stddef.h> #include <string> -#include <QSettings> - #include "gamegamebryo.h" -// Helper to remove a key from a Bethesda-style INI file -static void removeIniValue(const QString& iniFile, const QString& section, - const QString& key) -{ - QSettings settings(iniFile, QSettings::IniFormat); - settings.beginGroup(section); - settings.remove(key); - settings.endGroup(); - settings.sync(); -} - GamebryoLocalSavegames::GamebryoLocalSavegames(const GameGamebryo* game, const QString& iniFileName) : m_Game{game}, m_IniFileName(iniFileName) @@ -119,20 +106,20 @@ bool GamebryoLocalSavegames::prepareProfile(MOBase::IProfile* profile) if (savedPath != "DELETE_ME") { MOBase::WriteRegistryValue("General", "sLocalSavePath", savedPath, iniFilePath); } else { - removeIniValue(iniFilePath, "General", "sLocalSavePath"); + MOBase::RemoveRegistryValue("General", "sLocalSavePath", iniFilePath); } if (savedMyGames != "DELETE_ME") { MOBase::WriteRegistryValue("General", "bUseMyGamesDirectory", savedMyGames, iniFilePath); } else { - removeIniValue(iniFilePath, "General", "bUseMyGamesDirectory"); + MOBase::RemoveRegistryValue("General", "bUseMyGamesDirectory", iniFilePath); } QFile::remove(saveIni); } // Otherwise just delete the setting else { - removeIniValue(iniFilePath, "General", "sLocalSavePath"); - removeIniValue(iniFilePath, "General", "bUseMyGamesDirectory"); + MOBase::RemoveRegistryValue("General", "sLocalSavePath", iniFilePath); + MOBase::RemoveRegistryValue("General", "bUseMyGamesDirectory", iniFilePath); } } diff --git a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp index 4a92440..2fd45cd 100644 --- a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp +++ b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp @@ -357,6 +357,15 @@ void GameGamebryo::ensureIniFilesExist(const QString& basePath) } #ifndef _WIN32 + // Remove broken symlinks — QFileInfo::exists() follows symlinks and + // returns false for dangling ones, but QFile::copy() fails with + // "File exists" because the symlink inode is still there. + if (targetInfo.isSymLink() && !targetInfo.exists()) { + QFile::remove(targetPath); + } +#endif + +#ifndef _WIN32 // On Linux, search for a differently-cased version of this INI file // (e.g., FalloutPrefs.ini when we expect falloutprefs.ini). // We can't use resolveFileCaseInsensitive here because the exact-case @@ -413,15 +422,21 @@ void GameGamebryo::ensureIniFilesExist(const QString& basePath) if (targetInfo.exists()) { QFile::remove(targetPath); } - if (QFile::copy(defaultIniPath, targetPath)) { + QFile srcFile(defaultIniPath); + if (srcFile.copy(targetPath)) { // Make the copy writable QFile::setPermissions( targetPath, QFile::permissions(targetPath) | QFile::WriteUser | QFile::WriteOwner); MOBase::log::info("Seeded '{}' from default INI '{}'", iniFile, defaultIniPath); } else { - MOBase::log::warn("Failed to copy default INI '{}' -> '{}'", - defaultIniPath, targetPath); + MOBase::log::warn("Failed to copy default INI '{}' -> '{}': {} " + "(srcExists={}, srcSize={}, targetExists={}, dirExists={})", + defaultIniPath, targetPath, srcFile.errorString(), + QFileInfo::exists(defaultIniPath), + QFileInfo(defaultIniPath).size(), + QFileInfo::exists(targetPath), + QFileInfo(targetPath).dir().exists()); } } diff --git a/libs/nak/src/installers/prefix_setup.rs b/libs/nak/src/installers/prefix_setup.rs index 4f27606..468aaa2 100644 --- a/libs/nak/src/installers/prefix_setup.rs +++ b/libs/nak/src/installers/prefix_setup.rs @@ -297,23 +297,9 @@ fn initialize_prefix_with_proton( ("WINEDLLOVERRIDES", "msdia80.dll=n;conhost.exe=d;cmd.exe=d".to_string()), ]; - let (exe, args): (std::path::PathBuf, Vec<&str>) = if runtime_wrap::use_umu_for_prefix() { - if let Some(umu_run) = runtime_wrap::resolve_umu_run() { - log_install(&format!("Initializing prefix with umu-run: {:?}", umu_run)); - envs.push(("PROTONPATH", proton.path.display().to_string())); - envs.push(("WINEPREFIX", prefix_root.display().to_string())); - envs.push(("GAMEID", app_id.to_string())); - (umu_run, vec!["wineboot", "-u"]) - } else { - log_warning( - "UMU prefix mode enabled but no umu-run was found; falling back to proton wrapper", - ); - (proton_script.clone(), vec!["run", "wineboot", "-u"]) - } - } else { - log_install(&format!("Initializing prefix with proton wrapper: {:?}", proton_script)); - (proton_script.clone(), vec!["run", "wineboot", "-u"]) - }; + log_install(&format!("Initializing prefix with proton wrapper: {:?}", proton_script)); + let (exe, args): (std::path::PathBuf, Vec<&str>) = + (proton_script.clone(), vec!["run", "wineboot", "-u"]); let mut cmd = runtime_wrap::build_command(&exe, &envs); cmd.args(&args); diff --git a/libs/nak/src/runtime_wrap.rs b/libs/nak/src/runtime_wrap.rs index 9b5ff89..69f693e 100644 --- a/libs/nak/src/runtime_wrap.rs +++ b/libs/nak/src/runtime_wrap.rs @@ -1,6 +1,5 @@ use std::env; use std::ffi::OsStr; -use std::path::{Path, PathBuf}; use std::process::Command; fn env_flag(name: &str) -> bool { @@ -18,35 +17,6 @@ pub fn use_steam_run() -> bool { env_flag("NAK_USE_STEAM_RUN") } -pub fn use_umu_for_prefix() -> bool { - env_flag("NAK_USE_UMU_FOR_PREFIX") -} - -pub fn prefer_system_umu() -> bool { - env_flag("NAK_PREFER_SYSTEM_UMU") -} - -fn find_in_path(binary: &str) -> Option<PathBuf> { - let path = env::var_os("PATH")?; - env::split_paths(&path) - .map(|entry| entry.join(binary)) - .find(|candidate| candidate.exists()) -} - -pub fn resolve_umu_run() -> Option<PathBuf> { - let bundled = env::var("NAK_BUNDLED_UMU_RUN") - .ok() - .map(PathBuf::from) - .filter(|p| p.exists()); - let system = find_in_path("umu-run"); - - if prefer_system_umu() { - system.or(bundled) - } else { - bundled.or(system) - } -} - /// Build a command to run `exe` with the given environment variables. /// /// In steam-run mode, the command is wrapped with `steam-run`. @@ -74,12 +44,3 @@ pub fn build_command<S: AsRef<OsStr>>( pub fn command_for(exe: impl AsRef<OsStr>) -> Command { build_command::<&str>(exe, &[]) } - -pub fn bundled_umu_path_from_appdir(appdir: &Path) -> Option<PathBuf> { - let path = appdir.join("umu-run"); - if path.exists() { - Some(path) - } else { - None - } -} diff --git a/libs/uibase/include/uibase/registry.h b/libs/uibase/include/uibase/registry.h index 00013df..6a93f9d 100644 --- a/libs/uibase/include/uibase/registry.h +++ b/libs/uibase/include/uibase/registry.h @@ -28,6 +28,12 @@ namespace MOBase QDLLEXPORT bool WriteRegistryValue(const QString& appName, const QString& keyName, const QString& value, const QString& fileName); +// Removes a key from a Bethesda-style INI file without QSettings. +// Uses a safe line-by-line approach that does not corrupt backslashes +// or URL-encode spaces in key names. +QDLLEXPORT bool RemoveRegistryValue(const QString& section, const QString& key, + const QString& fileName); + #ifdef _WIN32 // Windows-specific overload using wide strings QDLLEXPORT bool WriteRegistryValue(const wchar_t* appName, const wchar_t* keyName, diff --git a/libs/uibase/src/registry.cpp b/libs/uibase/src/registry.cpp index 0b6834f..5832075 100644 --- a/libs/uibase/src/registry.cpp +++ b/libs/uibase/src/registry.cpp @@ -174,6 +174,87 @@ bool WriteRegistryValue(const QString& appName, const QString& keyName, return false; } +bool RemoveRegistryValue(const QString& section, const QString& key, + const QString& fileName) +{ + if (!QFileInfo::exists(fileName)) { + return true; // nothing to remove + } + + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return false; + } + QStringList lines; + QTextStream in(&file); + while (!in.atEnd()) { + lines.append(in.readLine()); + } + file.close(); + + const QString sectionHeader = "[" + section + "]"; + int sectionStart = -1; + int sectionEnd = lines.size(); + + for (int i = 0; i < lines.size(); ++i) { + QString trimmed = lines[i].trimmed(); + if (trimmed.compare(sectionHeader, Qt::CaseInsensitive) == 0) { + sectionStart = i; + for (int j = i + 1; j < lines.size(); ++j) { + QString t = lines[j].trimmed(); + if (t.startsWith('[') && t.endsWith(']')) { + sectionEnd = j; + break; + } + } + break; + } + } + + if (sectionStart < 0) { + return true; // section not found, nothing to remove + } + + // Find and remove the key line + bool found = false; + for (int i = sectionStart + 1; i < sectionEnd; ++i) { + QString trimmed = lines[i].trimmed(); + if (trimmed.isEmpty() || trimmed.startsWith(';') || trimmed.startsWith('#')) { + continue; + } + int eqPos = trimmed.indexOf('='); + if (eqPos > 0) { + QString existingKey = trimmed.left(eqPos).trimmed(); + if (existingKey.compare(key, Qt::CaseInsensitive) == 0) { + lines.removeAt(i); + found = true; + break; + } + } + } + + if (!found) { + return true; // key not found, nothing to remove + } + + // Write back + QFile outFile(fileName); + if (!outFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + return false; + } + QTextStream out(&outFile); + for (int i = 0; i < lines.size(); ++i) { + out << lines[i]; + if (i < lines.size() - 1) { + out << '\n'; + } + } + out << '\n'; + outFile.close(); + + return true; +} + #ifdef _WIN32 bool WriteRegistryValue(const wchar_t* appName, const wchar_t* keyName, const wchar_t* value, const wchar_t* fileName) diff --git a/libs/uibase/src/utility.cpp b/libs/uibase/src/utility.cpp index e6f53b8..33f227d 100644 --- a/libs/uibase/src/utility.cpp +++ b/libs/uibase/src/utility.cpp @@ -388,11 +388,42 @@ namespace shell } } + // Launch an external host process with AppImage environment variables + // cleaned up, so tools like xdg-open/kde-open use the host's own + // libraries and Qt plugins instead of the bundled ones. + static bool startDetachedHostProcess(const QString& program, + const QStringList& args) + { + QProcess proc; + auto env = QProcessEnvironment::systemEnvironment(); + + // Restore original LD_LIBRARY_PATH (saved by AppRun.sh) + if (env.contains(QStringLiteral("FLUORINE_ORIG_LD_LIBRARY_PATH"))) { + const auto orig = env.value(QStringLiteral("FLUORINE_ORIG_LD_LIBRARY_PATH")); + if (orig.isEmpty()) + env.remove(QStringLiteral("LD_LIBRARY_PATH")); + else + env.insert(QStringLiteral("LD_LIBRARY_PATH"), orig); + } + + // Remove AppImage-specific Qt/path variables that would confuse host apps + env.remove(QStringLiteral("QT_PLUGIN_PATH")); + env.remove(QStringLiteral("QT_QPA_PLATFORM_PLUGIN_PATH")); + env.remove(QStringLiteral("QTWEBENGINEPROCESS_PATH")); + env.remove(QStringLiteral("QTWEBENGINE_RESOURCES_PATH")); + env.remove(QStringLiteral("QTWEBENGINE_LOCALES_PATH")); + + proc.setProgram(program); + proc.setArguments(args); + proc.setProcessEnvironment(env); + return proc.startDetached(); + } + Result ExploreDirectory(const QFileInfo& info) { const auto path = info.absoluteFilePath(); // Use xdg-open on Linux - if (QProcess::startDetached("xdg-open", {path})) { + if (startDetachedHostProcess("xdg-open", {path})) { return Result::makeSuccess(); } return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open directory")); @@ -403,7 +434,7 @@ namespace shell // Try to use the system file manager to highlight the file // dbus method for Nautilus/Files, fallback to xdg-open on parent dir const auto dir = info.absolutePath(); - if (QProcess::startDetached("xdg-open", {dir})) { + if (startDetachedHostProcess("xdg-open", {dir})) { return Result::makeSuccess(); } return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open file manager")); @@ -437,7 +468,7 @@ namespace shell Result Open(const QString& path) { - if (QProcess::startDetached("xdg-open", {path})) { + if (startDetachedHostProcess("xdg-open", {path})) { return Result::makeSuccess(); } return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open file")); @@ -448,7 +479,7 @@ namespace shell log::debug("opening url '{}'", url.toString()); if (g_urlHandler.isEmpty()) { - if (QProcess::startDetached("xdg-open", {url.toString()})) { + if (startDetachedHostProcess("xdg-open", {url.toString()})) { return Result::makeSuccess(); } return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open URL")); @@ -456,7 +487,7 @@ namespace shell // Custom URL handler QString cmd = g_urlHandler; cmd.replace("%1", url.toString()); - if (QProcess::startDetached("/bin/sh", {"-c", cmd})) { + if (startDetachedHostProcess("/bin/sh", {"-c", cmd})) { return Result::makeSuccess(); } return Result::makeFailure(ERROR_FILE_NOT_FOUND, diff --git a/src/src/CMakeLists.txt b/src/src/CMakeLists.txt index e85f730..f53f02a 100644 --- a/src/src/CMakeLists.txt +++ b/src/src/CMakeLists.txt @@ -203,44 +203,6 @@ if(NOT WIN32) endif() endif() - # ---------- Bundle umu-run (Python zipapp) ---------- - option(MO2_BUNDLE_UMU "Download and bundle umu-run launcher" ON) - set(MO2_UMU_VERSION "1.3.0" CACHE STRING "umu-launcher release version to bundle") - - if(MO2_BUNDLE_UMU) - set(UMU_TAR "${CMAKE_BINARY_DIR}/umu-launcher-${MO2_UMU_VERSION}-zipapp.tar") - set(UMU_URL "https://github.com/Open-Wine-Components/umu-launcher/releases/download/${MO2_UMU_VERSION}/umu-launcher-${MO2_UMU_VERSION}-zipapp.tar") - set(UMU_EXTRACT_DIR "${CMAKE_BINARY_DIR}/umu-extract") - - if(NOT EXISTS "${UMU_EXTRACT_DIR}/umu/umu-run") - message(STATUS "[MO2] Downloading umu-launcher ${MO2_UMU_VERSION} zipapp...") - file(DOWNLOAD "${UMU_URL}" "${UMU_TAR}" - STATUS UMU_DL_STATUS - SHOW_PROGRESS) - list(GET UMU_DL_STATUS 0 UMU_DL_CODE) - if(NOT UMU_DL_CODE EQUAL 0) - message(WARNING "[MO2] Failed to download umu-launcher: ${UMU_DL_STATUS}") - else() - file(MAKE_DIRECTORY "${UMU_EXTRACT_DIR}") - execute_process( - COMMAND ${CMAKE_COMMAND} -E tar xf "${UMU_TAR}" - WORKING_DIRECTORY "${UMU_EXTRACT_DIR}") - message(STATUS "[MO2] Extracted umu-run zipapp") - endif() - endif() - - if(EXISTS "${UMU_EXTRACT_DIR}/umu/umu-run") - add_custom_command(TARGET organizer POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${UMU_EXTRACT_DIR}/umu/umu-run" - "$<TARGET_FILE_DIR:organizer>/umu-run" - COMMAND chmod +x "$<TARGET_FILE_DIR:organizer>/umu-run") - message(STATUS "[MO2] Will bundle umu-run ${MO2_UMU_VERSION}") - else() - message(WARNING "[MO2] umu-run not found after extraction") - endif() - endif() - # ── Deploy bundled stylesheets (themes) ── add_custom_command(TARGET organizer POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory diff --git a/src/src/commandline.cpp b/src/src/commandline.cpp index 121fb58..bb08b4a 100644 --- a/src/src/commandline.cpp +++ b/src/src/commandline.cpp @@ -1017,8 +1017,6 @@ std::optional<int> CreatePortableCommand::runEarly() if (vm().count("proton")) {
ini.setValue("fluorine/proton_path", QString::fromStdString(vm()["proton"].as<std::string>()));
}
- ini.setValue("fluorine/use_umu", true);
-
ini.sync();
}
@@ -1145,8 +1143,6 @@ std::optional<int> InfoCommand::runEarly() std::cout << " Prefix: " << prefix.toStdString() << "\n";
if (!proton.isEmpty())
std::cout << " Proton: " << proton.toStdString() << "\n";
- std::cout << " UMU: " << (ini.value("fluorine/use_umu", false).toBool() ? "yes" : "no") << "\n";
-
return 0;
}
diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index 79ea90b..6e95d7e 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -34,6 +34,7 @@ #include <uibase/game_features/dataarchives.h>
#include <uibase/game_features/localsavegames.h>
#include <uibase/game_features/scriptextender.h>
+#include <uibase/registry.h>
#include <uibase/report.h>
#include <uibase/scopeguard.h>
#include <uibase/filesystemutilities.h>
@@ -2278,6 +2279,33 @@ bool OrganizerCore::beforeRun( log::debug("Deployed profile saves '{}' -> '{}' in prefix '{}'",
profileSavesDir, absoluteSaveDir, prefixPathStr);
}
+
+ // Ensure the prefix INI points to __MO_Saves so the game reads
+ // profile-specific saves, even when localSettingsEnabled() is off.
+ const QStringList iniFiles = managedGame()->iniFiles();
+ if (!iniFiles.isEmpty()) {
+ const QString prefixIni =
+ QDir(resolvePrefixGameDocumentsDir(prefix, dataDirName))
+ .filePath(QFileInfo(iniFiles.first()).fileName());
+ MOBase::WriteRegistryValue("General", "sLocalSavePath",
+ "__MO_Saves\\", prefixIni);
+ MOBase::WriteRegistryValue("General", "bUseMyGamesDirectory",
+ "1", prefixIni);
+ log::debug("Patched prefix INI '{}': sLocalSavePath=__MO_Saves\\",
+ prefixIni);
+ }
+ } else {
+ // Local saves disabled — restore default sLocalSavePath in prefix INI
+ const QStringList iniFiles = managedGame()->iniFiles();
+ if (!iniFiles.isEmpty()) {
+ const QString prefixIni =
+ QDir(resolvePrefixGameDocumentsDir(prefix, dataDirName))
+ .filePath(QFileInfo(iniFiles.first()).fileName());
+ MOBase::WriteRegistryValue("General", "sLocalSavePath",
+ "Saves\\", prefixIni);
+ log::debug("Restored prefix INI '{}': sLocalSavePath=Saves\\",
+ prefixIni);
+ }
}
} else {
log::warn("Wine prefix at '{}' is not valid (no drive_c)", prefixPathStr);
diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp index 599ef62..4c5e39a 100644 --- a/src/src/protonlauncher.cpp +++ b/src/src/protonlauncher.cpp @@ -1,20 +1,18 @@ #include "protonlauncher.h" #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 +// Restore the pre-AppImage environment for child processes (Proton, Wine). +// 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) @@ -26,7 +24,7 @@ void cleanAppImageEnv(QProcessEnvironment& env) env.remove("MO2_PYTHON_DIR"); env.remove("MO2_BASE_DIR"); - // AppImage runtime injects these — they can confuse pressure-vessel/umu-run. + // AppImage runtime injects these — they can confuse Proton/Wine. env.remove("APPIMAGE"); env.remove("APPDIR"); env.remove("OWD"); @@ -135,20 +133,33 @@ QString detectSteamPath() return {}; } -bool startDetachedWithEnv(const QString& program, const QStringList& arguments, - const QString& workingDir, - const QProcessEnvironment& environment, qint64& pid) +bool startWithEnv(const QString& program, const QStringList& arguments, + const QString& workingDir, + const QProcessEnvironment& environment, qint64& pid) { - QProcess process; - process.setProgram(program); - process.setArguments(arguments); + auto* process = new QProcess(); + process->setProgram(program); + process->setArguments(arguments); if (!workingDir.isEmpty()) { - process.setWorkingDirectory(workingDir); + process->setWorkingDirectory(workingDir); } - process.setProcessEnvironment(environment); - return process.startDetached(&pid); + process->setProcessEnvironment(environment); + process->setProcessChannelMode(QProcess::ForwardedChannels); + + QObject::connect(process, + QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), + process, &QProcess::deleteLater); + + process->start(); + if (!process->waitForStarted(5000)) { + delete process; + return false; + } + + pid = process->processId(); + return true; } void wrapProgram(const QStringList& wrapperCommands, const QString& program, @@ -224,8 +235,7 @@ bool parseEnvAssignment(const QString& token, QString& keyOut, QString& valueOut } // namespace ProtonLauncher::ProtonLauncher() - : m_steamAppId(0), m_useUmu(false), m_preferSystemUmu(false), - m_useSteamRun(false), m_useSteamDrm(true) + : m_steamAppId(0), m_useSteamRun(false), m_useSteamDrm(true) {} ProtonLauncher& ProtonLauncher::setBinary(const QString& path) @@ -286,18 +296,6 @@ ProtonLauncher& ProtonLauncher::setWrapper(const QString& wrapperCmd) return *this; } -ProtonLauncher& ProtonLauncher::setUmu(bool useUmu) -{ - m_useUmu = useUmu; - return *this; -} - -ProtonLauncher& ProtonLauncher::setPreferSystemUmu(bool preferSystemUmu) -{ - m_preferSystemUmu = preferSystemUmu; - return *this; -} - ProtonLauncher& ProtonLauncher::setUseSteamRun(bool useSteamRun) { m_useSteamRun = useSteamRun; @@ -329,13 +327,6 @@ std::pair<bool, qint64> ProtonLauncher::launch() const { qint64 pid = -1; - if (m_useUmu) { - if (launchWithUmu(pid)) { - return {true, pid}; - } - MOBase::log::warn("UMU launch failed, falling back to Proton"); - } - if (!m_protonPath.isEmpty()) { return {launchWithProton(pid), pid}; } @@ -416,173 +407,7 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const env.insert("PWD", m_workingDir); } - return startDetachedWithEnv(program, arguments, m_workingDir, env, pid); -} - -bool ProtonLauncher::launchWithUmu(qint64& pid) const -{ - if (m_binary.isEmpty()) { - return false; - } - - // Steam must be running for games with Steamworks DRM (Application Load - // Error 5:0000065434 occurs otherwise). - if (m_useSteamDrm) { - ensureSteamRunning(); - } - - // 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 - // bundled copy otherwise). - QString system; - const QStringList pathDirs = - QString::fromLocal8Bit(qgetenv("PATH")).split(QLatin1Char(':'), Qt::SkipEmptyParts); - for (const QString& dir : pathDirs) { - if (QDir(dir) == QDir(appDir)) - continue; - const QString candidate = QStandardPaths::findExecutable( - QStringLiteral("umu-run"), {dir}); - if (!candidate.isEmpty()) { - system = candidate; - break; - } - } - - // Priority: data dir > system (if preferred) > bundled > system (fallback) - QString umuRun; - 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: 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)"); - return false; - } - - const QStringList umuArgs = QStringList() << m_binary << m_arguments; - - QString program; - QStringList arguments; - wrapProgram(m_wrapperCommands, umuRun, umuArgs, program, arguments); - maybeWrapWithSteamRun(m_useSteamRun, program, arguments); - - QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - env.remove("PYTHONHOME"); - cleanAppImageEnv(env); - - if (!m_prefixPath.isEmpty()) { - env.insert("WINEPREFIX", m_prefixPath); - } - - if (!m_protonPath.isEmpty()) { - env.insert("PROTONPATH", m_protonPath); - } - - // umu-run sets STEAM_COMPAT_DATA_PATH internally from WINEPREFIX, so we - // do NOT set it here. However, ensure tracked_files exists for Proton. - ensureTrackedFilesExist(compatDataPathFromPrefix(m_prefixPath)); - - uint32_t effectiveSteamAppId = m_steamAppId; - if (effectiveSteamAppId == 0) { - bool ok = false; - const QString inheritedSteamAppId = - env.value("SteamAPPId", env.value("SteamAppId")).trimmed(); - const uint32_t parsed = inheritedSteamAppId.toUInt(&ok); - if (ok) { - effectiveSteamAppId = parsed; - } - } - - // 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)); - } else { - 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()); - } - - for (auto it = m_envVars.cbegin(); it != m_envVars.cend(); ++it) { - env.insert(it.key(), it.value()); - } - - // Set DXVK config if available - if (char* dxvkPath = nak_get_dxvk_conf_path(); dxvkPath != nullptr) { - const QString dxvkConf = QString::fromUtf8(dxvkPath); - nak_string_free(dxvkPath); - if (QFileInfo::exists(dxvkConf)) { - env.insert("DXVK_CONFIG_FILE", dxvkConf); - } - } - - 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); + return startWithEnv(program, arguments, m_workingDir, env, pid); } bool ProtonLauncher::launchDirect(qint64& pid) const @@ -606,7 +431,7 @@ bool ProtonLauncher::launchDirect(qint64& pid) const env.insert(it.key(), it.value()); } - return startDetachedWithEnv(program, arguments, m_workingDir, env, pid); + return startWithEnv(program, arguments, m_workingDir, env, pid); } bool ProtonLauncher::ensureSteamRunning() diff --git a/src/src/protonlauncher.h b/src/src/protonlauncher.h index 7ef6e76..6b3ee42 100644 --- a/src/src/protonlauncher.h +++ b/src/src/protonlauncher.h @@ -20,19 +20,16 @@ public: ProtonLauncher& setPrefix(const QString& path); ProtonLauncher& setSteamAppId(uint32_t id); ProtonLauncher& setWrapper(const QString& wrapperCmd); - ProtonLauncher& setUmu(bool useUmu); - 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 + // Launch dispatch: Proton -> Direct std::pair<bool, qint64> launch() const; private: bool launchWithProton(qint64& pid) const; - bool launchWithUmu(qint64& pid) const; bool launchDirect(qint64& pid) const; static bool ensureSteamRunning(); @@ -43,8 +40,6 @@ private: QString m_prefixPath; uint32_t m_steamAppId; QStringList m_wrapperCommands; - bool m_useUmu; - bool m_preferSystemUmu; bool m_useSteamRun; bool m_useSteamDrm; QString m_storeVariant; // "GOG", "Epic", or empty for Steam diff --git a/src/src/settingsdialog.ui b/src/src/settingsdialog.ui index 9e5c44d..6588a71 100644 --- a/src/src/settingsdialog.ui +++ b/src/src/settingsdialog.ui @@ -1866,29 +1866,6 @@ If you disable this feature, MO will only display official DLCs this way. Please </property> <layout class="QVBoxLayout" name="verticalLayout_38"> <item> - <widget class="QCheckBox" name="umuCheckBox"> - <property name="text"> - <string>Use UMU Launcher (recommended)</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - <property name="toolTip"> - <string>Launch games using umu-run instead of raw Proton. UMU provides better compatibility and automatic dependency handling.</string> - </property> - </widget> - </item> - <item> - <widget class="QCheckBox" name="umuSystemCheckBox"> - <property name="text"> - <string>Prefer system umu-run over bundled</string> - </property> - <property name="toolTip"> - <string>Use umu-run from PATH when available (helpful for NixOS). If unavailable, falls back to bundled umu-run.</string> - </property> - </widget> - </item> - <item> <widget class="QCheckBox" name="steamRunCheckBox"> <property name="text"> <string>Wrap launches with steam-run</string> diff --git a/src/src/settingsdialogproton.cpp b/src/src/settingsdialogproton.cpp index f202f5f..0199744 100644 --- a/src/src/settingsdialogproton.cpp +++ b/src/src/settingsdialogproton.cpp @@ -6,10 +6,10 @@ #include <QtConcurrent/QtConcurrentRun> #include <log.h> +#include <uibase/utility.h> #include <nak_ffi.h> #include <atomic> #include <QComboBox> -#include <QCoreApplication> #include <QDateTime> #include <QDialog> #include <QDialogButtonBox> @@ -40,9 +40,6 @@ ProtonSettingsTab::ProtonSettingsTab(Settings& s, SettingsDialog& d) ui->protonProgressBar->setValue(0); ui->protonProgressBar->setVisible(false); - ui->umuCheckBox->setChecked(QSettings().value("fluorine/use_umu", true).toBool()); - ui->umuSystemCheckBox->setChecked( - QSettings().value("fluorine/prefer_system_umu", false).toBool()); ui->steamRunCheckBox->setChecked( QSettings().value("fluorine/use_steam_run", false).toBool()); @@ -117,9 +114,6 @@ ProtonSettingsTab::ProtonSettingsTab(Settings& s, SettingsDialog& d) void ProtonSettingsTab::update() { - QSettings().setValue("fluorine/use_umu", ui->umuCheckBox->isChecked()); - QSettings().setValue("fluorine/prefer_system_umu", - ui->umuSystemCheckBox->isChecked()); QSettings().setValue("fluorine/use_steam_run", ui->steamRunCheckBox->isChecked()); QSettings().setValue("fluorine/launch_wrapper", ui->launchWrapperEdit->text()); @@ -154,7 +148,7 @@ void ProtonSettingsTab::populateProtons() ui->protonVersionCombo->setCurrentIndex(idx); } else if (ui->protonVersionCombo->count() > 0) { // Saved Proton version no longer exists — select first available and - // update the config so the stale path doesn't cause umu-run fallback. + // update the config so the stale path doesn't cause launch failures. MOBase::log::warn("Saved Proton '{}' not found, defaulting to '{}'", cfg->proton_name, ui->protonVersionCombo->itemText(0)); @@ -244,8 +238,6 @@ void ProtonSettingsTab::onCreatePrefix() ui->toggleInstallLog->setChecked(true); startInstallTask(0, pfxPath, protonName, protonPath, - ui->umuCheckBox->isChecked(), - ui->umuSystemCheckBox->isChecked(), ui->steamRunCheckBox->isChecked()); } @@ -295,8 +287,7 @@ void ProtonSettingsTab::onRecreatePrefix() ui->toggleInstallLog->setChecked(true); startInstallTask(cfg->app_id, cfg->prefix_path, cfg->proton_name, - cfg->proton_path, ui->umuCheckBox->isChecked(), - ui->umuSystemCheckBox->isChecked(), + cfg->proton_path, ui->steamRunCheckBox->isChecked()); } @@ -308,7 +299,7 @@ void ProtonSettingsTab::onOpenPrefixFolder() return; } - QProcess::startDetached("xdg-open", {*path}); + MOBase::shell::Explore(QDir(*path)); } void ProtonSettingsTab::onBrowsePrefixLocation() @@ -589,8 +580,6 @@ void ProtonSettingsTab::showGameRegistryDialog() void ProtonSettingsTab::startInstallTask(uint32_t appId, const QString& prefixPath, const QString& protonName, const QString& protonPath, - bool useUmuForPrefix, - bool preferSystemUmu, bool useSteamRun) { m_pendingAppId = appId; @@ -607,27 +596,15 @@ void ProtonSettingsTab::startInstallTask(uint32_t appId, const QString& prefixPa prefixPath, protonName, protonPath, - useUmuForPrefix, - preferSystemUmu, useSteamRun]() -> InstallResult { const QByteArray prefixPathUtf8 = prefixPath.toUtf8(); const QByteArray protonNameUtf8 = protonName.toUtf8(); const QByteArray protonPathUtf8 = protonPath.toUtf8(); - const QByteArray bundledUmuPathUtf8 = - QDir(QCoreApplication::applicationDirPath()).filePath("umu-run").toUtf8(); - qputenv("NAK_USE_UMU_FOR_PREFIX", useUmuForPrefix ? "1" : "0"); - qputenv("NAK_PREFER_SYSTEM_UMU", preferSystemUmu ? "1" : "0"); qputenv("NAK_USE_STEAM_RUN", useSteamRun ? "1" : "0"); - if (QFileInfo::exists(QString::fromUtf8(bundledUmuPathUtf8))) { - qputenv("NAK_BUNDLED_UMU_RUN", bundledUmuPathUtf8); - } else { - qunsetenv("NAK_BUNDLED_UMU_RUN"); - } - // Set WINEPREFIX so NAK (and its child processes like winetricks) always - // target the correct prefix, even if Proton init via umu-run fails. + // target the correct prefix during Proton init. qputenv("WINEPREFIX", prefixPathUtf8); // Point WINE/WINESERVER at Proton's binaries so winetricks and regedit @@ -652,10 +629,7 @@ void ProtonSettingsTab::startInstallTask(uint32_t appId, const QString& prefixPa } const auto restoreNakEnv = qScopeGuard([protonWineUtf8] { - qunsetenv("NAK_USE_UMU_FOR_PREFIX"); - qunsetenv("NAK_PREFER_SYSTEM_UMU"); qunsetenv("NAK_USE_STEAM_RUN"); - qunsetenv("NAK_BUNDLED_UMU_RUN"); qunsetenv("WINEPREFIX"); if (!protonWineUtf8.isEmpty()) { qunsetenv("WINE"); diff --git a/src/src/settingsdialogproton.h b/src/src/settingsdialogproton.h index 2b864c9..58c43f2 100644 --- a/src/src/settingsdialogproton.h +++ b/src/src/settingsdialogproton.h @@ -40,7 +40,6 @@ private: void startInstallTask(uint32_t appId, const QString& prefixPath, const QString& protonName, const QString& protonPath, - bool useUmuForPrefix, bool preferSystemUmu, bool useSteamRun); void enqueueStatus(const QString& message); diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp index 850d5a2..28010d0 100644 --- a/src/src/spawn.cpp +++ b/src/src/spawn.cpp @@ -656,9 +656,6 @@ int spawn(const SpawnParameters &sp, pid_t &processId) { .setArguments(argList)
.setWorkingDir(cwd)
.setSteamAppId(parseSteamAppId(sp.steamAppID))
- .setUmu(QSettings().value("fluorine/use_umu", true).toBool())
- .setPreferSystemUmu(
- QSettings().value("fluorine/prefer_system_umu", false).toBool())
.setUseSteamRun(
QSettings().value("fluorine/use_steam_run", false).toBool())
.setSteamDrm(useSteamDrm)
|
