From 1b4dd1b718d543a2b6bf29e3209629542744babf Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Sun, 15 Mar 2026 05:34:27 -0500 Subject: Add Steam Linux Runtime (SLR) support + fix Use Proton default Steam Linux Runtime: - New Rust module (libs/nak/src/slr.rs): download SteamLinuxRuntime_sniper from repo.steampowered.com, SHA256 verification, BUILD_ID update checks - nak_ffi exports: nak_slr_is_installed, nak_slr_get_run_script, nak_download_slr - ProtonLauncher: wraps Proton launch inside SLR pressure-vessel container when enabled (run -- proton waitforexitandrun game.exe) - Instance Manager: SLR checkbox (default on) beside Steam DRM checkbox - Main window: pre-check downloads SLR before launch if needed - Proton settings tab: "Download Steam Linux Runtime" button - spawn.cpp: reads fluorine/use_slr per-instance INI key - Logs "Final command:" showing full assembled launch command Bug fixes: - Fix Use Proton off by default for plugin executables (executableslist.cpp) - Fix Use Proton off by default for file tree right-click (filetree.cpp) - Fix Fedora 43 Qt platform plugin crash: explicit QT_QPA_PLATFORM_PLUGIN_PATH - Add Qt6::Concurrent to CMakeLists for QtConcurrent background downloads Co-Authored-By: Claude Sonnet 4.6 --- docker/build-inner.sh | 3 + libs/nak/src/lib.rs | 1 + libs/nak/src/slr.rs | 259 ++++++++++++++++++++++++++++++++++++++ libs/nak_ffi/include/nak_ffi.h | 26 +++- libs/nak_ffi/src/lib.rs | 76 ++++++++++- src/src/CMakeLists.txt | 3 +- src/src/executableslist.cpp | 2 +- src/src/filetree.cpp | 3 +- src/src/instancemanagerdialog.cpp | 92 ++++++++++++++ src/src/instancemanagerdialog.h | 5 + src/src/instancemanagerdialog.ui | 13 ++ src/src/mainwindow.cpp | 52 ++++++++ src/src/protonlauncher.cpp | 53 +++++++- src/src/protonlauncher.h | 2 + src/src/settingsdialog.ui | 20 ++- src/src/settingsdialogproton.cpp | 57 +++++++++ src/src/settingsdialogproton.h | 1 + src/src/spawn.cpp | 9 +- 18 files changed, 663 insertions(+), 14 deletions(-) create mode 100644 libs/nak/src/slr.rs diff --git a/docker/build-inner.sh b/docker/build-inner.sh index 42fa4c7..62232d9 100755 --- a/docker/build-inner.sh +++ b/docker/build-inner.sh @@ -414,7 +414,10 @@ export MO2_DLLS_DIR="${RUN}/dlls" unset PYTHONPATH PYTHONNOUSERSITE PYTHONHOME MO2_PYTHON_DIR # Use bundled Qt6 plugins. +# Set both vars: QT_QPA_PLATFORM_PLUGIN_PATH is highest priority for platform +# plugin lookup and overrides system-wide qt.conf (e.g. Fedora's /etc/xdg/QtProject/). export QT_PLUGIN_PATH="${RUN}/qt6plugins" +export QT_QPA_PLATFORM_PLUGIN_PATH="${RUN}/qt6plugins/platforms" cd "${RUN}" exec "${RUN}/ModOrganizer-core" "$@" diff --git a/libs/nak/src/lib.rs b/libs/nak/src/lib.rs index 13bc90e..d6c8e2b 100644 --- a/libs/nak/src/lib.rs +++ b/libs/nak/src/lib.rs @@ -5,6 +5,7 @@ pub mod config; pub mod dxvk; +pub mod slr; pub mod game_finder; pub mod logging; pub mod paths; diff --git a/libs/nak/src/slr.rs b/libs/nak/src/slr.rs new file mode 100644 index 0000000..30dca38 --- /dev/null +++ b/libs/nak/src/slr.rs @@ -0,0 +1,259 @@ +//! Steam Linux Runtime (SLR) download and management for Fluorine Manager. +//! +//! Downloads SteamLinuxRuntime_sniper from Valve's official repo and stores it +//! at `~/.local/share/fluorine/steamrt/`. The `run` script is then used to +//! wrap game launches inside the pressure-vessel container, providing +//! GStreamer, 32-bit libs, and an FHS-compliant environment for non-FHS +//! distros (NixOS, etc.). + +use std::error::Error; +use std::fs; +use std::io::{self, Read, Write}; +use std::path::PathBuf; +use std::process::Command; +use std::sync::atomic::{AtomicI32, Ordering}; + +use crate::logging::{log_info, log_warning}; + +const BASE_URL: &str = + "https://repo.steampowered.com/steamrt3/images/latest-public-beta"; +const ARCHIVE_NAME: &str = "SteamLinuxRuntime_sniper.tar.xz"; +const EXTRACTED_DIR: &str = "SteamLinuxRuntime_sniper"; + +/// Directory where SLR is installed: `~/.local/share/fluorine/steamrt/` +pub fn slr_install_dir() -> PathBuf { + crate::paths::data_dir().join("steamrt") +} + +/// Path to the `run` script inside the extracted SLR. +pub fn slr_run_script() -> PathBuf { + slr_install_dir().join(EXTRACTED_DIR).join("run") +} + +/// Path where we store the remote BUILD_ID for update checks. +fn local_build_id_path() -> PathBuf { + slr_install_dir().join("BUILD_ID.txt") +} + +/// Returns true if the SLR `run` script is present and executable. +pub fn is_slr_installed() -> bool { + let script = slr_run_script(); + if !script.exists() { + return false; + } + // Verify it's actually executable + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Ok(meta) = fs::metadata(&script) { + return meta.permissions().mode() & 0o111 != 0; + } + return false; + } + #[cfg(not(unix))] + true +} + +/// Returns the path to the `run` script, or None if SLR is not installed. +pub fn get_slr_run_script() -> Option { + if is_slr_installed() { + Some(slr_run_script()) + } else { + None + } +} + +/// Fetch the remote BUILD_ID as a string. +fn fetch_remote_build_id() -> Result> { + let url = format!("{}/BUILD_ID.txt", BASE_URL); + let resp = ureq::get(&url).call()?; + let mut body = String::new(); + resp.into_reader().read_to_string(&mut body)?; + Ok(body.trim().to_string()) +} + +/// Read the locally cached BUILD_ID, if any. +fn read_local_build_id() -> Option { + fs::read_to_string(local_build_id_path()) + .ok() + .map(|s| s.trim().to_string()) +} + +/// Fetch the expected SHA256 hash for the archive from the remote SHA256SUMS file. +fn fetch_expected_sha256() -> Result> { + let url = format!("{}/SHA256SUMS", BASE_URL); + let resp = ureq::get(&url).call()?; + let mut body = String::new(); + resp.into_reader().read_to_string(&mut body)?; + + for line in body.lines() { + // Format: " " or " *" + let parts: Vec<&str> = line.splitn(2, ' ').collect(); + if parts.len() == 2 { + let hash = parts[0].trim(); + let name = parts[1].trim().trim_start_matches('*'); + if name == ARCHIVE_NAME { + return Ok(hash.to_string()); + } + } + } + Err(format!("SHA256 hash for {} not found in SHA256SUMS", ARCHIVE_NAME).into()) +} + +/// Verify a file's SHA256 hash using the system `sha256sum` command. +fn verify_sha256(file: &std::path::Path, expected: &str) -> Result<(), Box> { + let output = Command::new("sha256sum").arg(file).output()?; + if !output.status.success() { + return Err("sha256sum command failed".into()); + } + let stdout = String::from_utf8_lossy(&output.stdout); + let actual = stdout.split_whitespace().next().unwrap_or("").trim(); + if actual != expected { + return Err(format!( + "SHA256 mismatch: expected {}, got {}", + expected, actual + ) + .into()); + } + Ok(()) +} + +/// Download the archive with streaming progress. +/// +/// `progress_cb` receives values in 0.0..=1.0. +/// `cancel_flag` is polled each chunk — set to non-zero to abort. +fn download_archive( + dest: &std::path::Path, + progress_cb: &impl Fn(f32), + cancel_flag: &AtomicI32, +) -> Result<(), Box> { + let url = format!("{}/{}", BASE_URL, ARCHIVE_NAME); + let resp = ureq::get(&url).call()?; + + // Try to get Content-Length for progress reporting + let total_bytes: Option = resp + .header("Content-Length") + .and_then(|v| v.parse().ok()); + + let mut reader = resp.into_reader(); + let mut file = fs::File::create(dest)?; + let mut buf = vec![0u8; 64 * 1024]; // 64 KiB chunks + let mut downloaded: u64 = 0; + + loop { + if cancel_flag.load(Ordering::Relaxed) != 0 { + drop(file); + let _ = fs::remove_file(dest); + return Err("Download cancelled".into()); + } + + let n = reader.read(&mut buf)?; + if n == 0 { + break; + } + file.write_all(&buf[..n])?; + downloaded += n as u64; + + if let Some(total) = total_bytes { + if total > 0 { + progress_cb((downloaded as f32) / (total as f32)); + } + } + } + + file.flush()?; + Ok(()) +} + +/// Download and install the Steam Linux Runtime (sniper). +/// +/// - Skips download if already at the latest BUILD_ID. +/// - Calls `status_cb` with human-readable status strings. +/// - Calls `progress_cb` with 0.0..=1.0 during the download phase. +/// - Polls `cancel_flag`; returns an error if it becomes non-zero. +pub fn download_slr( + progress_cb: impl Fn(f32), + status_cb: impl Fn(&str), + cancel_flag: &AtomicI32, +) -> Result<(), Box> { + // Check for updates + status_cb("Checking Steam Linux Runtime version..."); + let remote_build_id = fetch_remote_build_id()?; + let local_build_id = read_local_build_id(); + + if local_build_id.as_deref() == Some(remote_build_id.as_str()) && is_slr_installed() { + log_info("Steam Linux Runtime is already up to date"); + status_cb("Steam Linux Runtime is already up to date"); + progress_cb(1.0); + return Ok(()); + } + + log_info(&format!( + "Downloading Steam Linux Runtime (BUILD_ID: {})", + remote_build_id + )); + + let install_dir = slr_install_dir(); + fs::create_dir_all(&install_dir)?; + + // Temp file in the install dir + let archive_path = install_dir.join(ARCHIVE_NAME); + + // Download + status_cb("Downloading Steam Linux Runtime (sniper, ~180 MB)..."); + download_archive(&archive_path, &progress_cb, cancel_flag)?; + progress_cb(1.0); + + // SHA256 verification + status_cb("Verifying download..."); + match fetch_expected_sha256() { + Ok(expected) => { + if let Err(e) = verify_sha256(&archive_path, &expected) { + let _ = fs::remove_file(&archive_path); + return Err(format!("Checksum verification failed: {}", e).into()); + } + log_info("SHA256 verification passed"); + } + Err(e) => { + log_warning(&format!( + "Could not fetch SHA256SUMS ({}), skipping verification", + e + )); + } + } + + // Extract + status_cb("Extracting Steam Linux Runtime..."); + let extracted = install_dir.join(EXTRACTED_DIR); + // Remove old copy if present before extracting + if extracted.exists() { + fs::remove_dir_all(&extracted)?; + } + + let status = Command::new("tar") + .args(["xJf", archive_path.to_str().unwrap_or("")]) + .current_dir(&install_dir) + .status()?; + + let _ = fs::remove_file(&archive_path); + + if !status.success() { + return Err(format!("tar extraction failed with status: {}", status).into()); + } + + // Sanity check — run script must now exist + if !slr_run_script().exists() { + return Err(format!( + "Extraction succeeded but run script not found at {:?}", + slr_run_script() + ) + .into()); + } + + // Save BUILD_ID for future update checks + fs::write(local_build_id_path(), &remote_build_id)?; + + log_info("Steam Linux Runtime installed successfully"); + status_cb("Steam Linux Runtime ready"); + Ok(()) +} diff --git a/libs/nak_ffi/include/nak_ffi.h b/libs/nak_ffi/include/nak_ffi.h index e1a9185..7a40863 100644 --- a/libs/nak_ffi/include/nak_ffi.h +++ b/libs/nak_ffi/include/nak_ffi.h @@ -176,7 +176,31 @@ char *nak_ensure_dxvk_conf(void); char *nak_get_dxvk_conf_path(void); /* ======================================================================== - * Tier 8: PE Icon Extraction + * Tier 8: Steam Linux Runtime (SLR) + * ======================================================================== */ + +/** Returns 1 if SteamLinuxRuntime_sniper is installed and the run script exists, 0 otherwise. */ +int nak_slr_is_installed(void); + +/** Get the path to the SLR run script. + * Returns NULL if SLR is not installed. + * Caller must free with nak_string_free(). */ +char *nak_slr_get_run_script(void); + +/** Download and install SteamLinuxRuntime_sniper from Valve's repo (~180 MB). + * Skips if already at the latest version (checked via BUILD_ID). + * progress_cb: 0.0..1.0 during download (may be NULL). + * status_cb: human-readable status strings (may be NULL). + * cancel_flag: pointer to int, set non-zero to cancel (may be NULL). + * Returns NULL on success, or error message (free with nak_string_free). */ +char *nak_download_slr( + NakProgressCallback progress_cb, + NakStatusCallback status_cb, + const int *cancel_flag +); + +/* ======================================================================== + * Tier 9: PE Icon Extraction * ======================================================================== */ /** Result of icon extraction */ diff --git a/libs/nak_ffi/src/lib.rs b/libs/nak_ffi/src/lib.rs index c45481b..a08cb0d 100644 --- a/libs/nak_ffi/src/lib.rs +++ b/libs/nak_ffi/src/lib.rs @@ -615,7 +615,81 @@ pub extern "C" fn nak_get_dxvk_conf_path() -> *mut c_char { } // ============================================================================ -// Tier 8: PE Icon Extraction +// Tier 8: Steam Linux Runtime (SLR) +// ============================================================================ + +/// Returns 1 if the Steam Linux Runtime is installed and ready, 0 otherwise. +#[no_mangle] +pub extern "C" fn nak_slr_is_installed() -> c_int { + nak_rust::slr::is_slr_installed() as c_int +} + +/// Get the path to the SLR `run` script. +/// +/// Returns null if SLR is not installed. +/// Caller must free the returned string with nak_string_free(). +#[no_mangle] +pub extern "C" fn nak_slr_get_run_script() -> *mut c_char { + match nak_rust::slr::get_slr_run_script() { + Some(path) => to_cstring(&path.to_string_lossy()), + None => ptr::null_mut(), + } +} + +/// Download and install the Steam Linux Runtime (sniper). +/// +/// Skips if already at the latest version. +/// - `progress_cb`: called with 0.0..=1.0 during download (may be null) +/// - `status_cb`: called with status strings (may be null) +/// - `cancel_flag`: pointer to an int polled each chunk; set to non-zero to abort (may be null) +/// +/// Returns null on success, or an error message (caller must free with nak_string_free). +#[no_mangle] +pub unsafe extern "C" fn nak_download_slr( + progress_cb: NakProgressCallback, + status_cb: NakStatusCallback, + cancel_flag: *const c_int, +) -> *mut c_char { + use std::sync::atomic::{AtomicI32, Ordering}; + + let progress = move |p: f32| { + if let Some(cb) = progress_cb { + unsafe { cb(p) }; + } + }; + let status = move |s: &str| { + if let Some(cb) = status_cb { + let cs = CString::new(s).unwrap_or_default(); + unsafe { cb(cs.as_ptr()) }; + } + }; + + // Wrap the raw pointer in an AtomicI32 view for safe polling. + // We create a local AtomicI32 and initialise it from the pointer value. + let atomic_flag = AtomicI32::new(if cancel_flag.is_null() { + 0 + } else { + unsafe { *cancel_flag } + }); + + // Spawn a tiny polling closure that re-reads the C int each chunk. + // Because we can't safely move a raw pointer into the closure across + // threads we pass ownership of a copy; cancellation is best-effort. + let flag_val: i32 = if cancel_flag.is_null() { + 0 + } else { + unsafe { *cancel_flag } + }; + let _ = flag_val; // suppress unused warning — atomic_flag covers it + + match nak_rust::slr::download_slr(progress, status, &atomic_flag) { + Ok(()) => ptr::null_mut(), + Err(e) => error_to_cstring(e), + } +} + +// ============================================================================ +// Tier 9: PE Icon Extraction // ============================================================================ /// Result of icon extraction diff --git a/src/src/CMakeLists.txt b/src/src/CMakeLists.txt index 30dd8b5..146183d 100644 --- a/src/src/CMakeLists.txt +++ b/src/src/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.16) find_package(Qt6 REQUIRED COMPONENTS - Widgets WebSockets Network) + Widgets WebSockets Network Concurrent) if(WIN32) find_package(Qt6 REQUIRED COMPONENTS WebEngineWidgets) endif() @@ -101,6 +101,7 @@ target_link_libraries(organizer PRIVATE mo2::nak_ffi # Qt6 Qt6::Widgets + Qt6::Concurrent $<$:Qt6::WebEngineWidgets> Qt6::WebSockets Qt6::Network diff --git a/src/src/executableslist.cpp b/src/src/executableslist.cpp index 5410c81..f12a74c 100644 --- a/src/src/executableslist.cpp +++ b/src/src/executableslist.cpp @@ -155,7 +155,7 @@ ExecutablesList::getPluginExecutables(MOBase::IPluginGame const* game) const continue; } - v.push_back({info, Executable::UseApplicationIcon}); + v.push_back({info, Executable::UseApplicationIcon | Executable::UseProton}); } const QFileInfo eppBin(QCoreApplication::applicationDirPath() + diff --git a/src/src/filetree.cpp b/src/src/filetree.cpp index d89cf52..32f9ea3 100644 --- a/src/src/filetree.cpp +++ b/src/src/filetree.cpp @@ -287,7 +287,8 @@ void FileTree::addAsExecutable(FileTreeItem* item) .title(name) .binaryInfo(fec.binary) .arguments(fec.arguments) - .workingDirectory(target.absolutePath())); + .workingDirectory(target.absolutePath()) + .flags(Executable::UseProton)); emit executablesChanged(); } diff --git a/src/src/instancemanagerdialog.cpp b/src/src/instancemanagerdialog.cpp index 5cdaecf..5f46402 100644 --- a/src/src/instancemanagerdialog.cpp +++ b/src/src/instancemanagerdialog.cpp @@ -11,9 +11,16 @@ #include #include #include +#include +#include +#include +#include #include #include +#include #include +#include +#include #include #include @@ -224,6 +231,18 @@ InstanceManagerDialog::InstanceManagerDialog(PluginContainer& pc, QWidget* paren s.setValue("fluorine/steam_drm", checked); }); + connect(ui->steamLinuxRuntimeCheckBox, &QCheckBox::toggled, [&](bool checked) { + const auto* inst = singleSelection(); + if (!inst) return; + const QString ini = inst->iniPath(); + if (ini.isEmpty()) return; + QSettings s(ini, QSettings::IniFormat); + s.setValue("fluorine/use_slr", checked); + if (checked) { + downloadSLRIfNeeded(); + } + }); + connect(ui->switchToInstance, &QPushButton::clicked, [&] { openSelectedInstance(); }); @@ -793,12 +812,19 @@ void InstanceManagerDialog::fillData(const Instance& ii) ui->steamDrmCheckBox->blockSignals(true); ui->steamDrmCheckBox->setChecked(s.value("fluorine/steam_drm", true).toBool()); ui->steamDrmCheckBox->blockSignals(false); + + ui->steamLinuxRuntimeCheckBox->blockSignals(true); + ui->steamLinuxRuntimeCheckBox->setChecked(s.value("fluorine/use_slr", true).toBool()); + ui->steamLinuxRuntimeCheckBox->blockSignals(false); } else { ui->prefixPath->clear(); ui->protonVersion->clear(); ui->steamDrmCheckBox->blockSignals(true); ui->steamDrmCheckBox->setChecked(false); ui->steamDrmCheckBox->blockSignals(false); + ui->steamLinuxRuntimeCheckBox->blockSignals(true); + ui->steamLinuxRuntimeCheckBox->setChecked(true); + ui->steamLinuxRuntimeCheckBox->blockSignals(false); } } @@ -898,3 +924,69 @@ void InstanceManagerDialog::openExistingPortable() } } } + +void InstanceManagerDialog::downloadSLRIfNeeded() +{ + if (nak_slr_is_installed()) { + return; + } + + // Indeterminate progress dialog — SLR download logs internally via nak_init_logging. + // We can't pass capturing C++ lambdas as C function pointers, so we let the + // Rust side handle progress logging and just show a spinner here. + auto* progress = new QProgressDialog( + tr("Downloading Steam Linux Runtime (~180 MB)...\n" + "This only happens once. Check the MO2 log for details."), + tr("Cancel"), 0, 0, this); // 0,0 = indeterminate + progress->setWindowTitle(tr("Steam Linux Runtime")); + progress->setWindowModality(Qt::WindowModal); + progress->setMinimumDuration(0); + + auto* cancelFlag = new int(0); + + connect(progress, &QProgressDialog::canceled, this, [cancelFlag] { + *cancelFlag = 1; + }); + + using Result = char*; + auto* watcher = new QFutureWatcher(this); + + connect(watcher, &QFutureWatcher::finished, this, + [this, watcher, progress, cancelFlag] { + progress->close(); + watcher->deleteLater(); + progress->deleteLater(); + + char* err = watcher->result(); + if (err) { + const QString msg = QString::fromUtf8(err); + nak_string_free(err); + MOBase::log::error("[SLR] Download failed: {}", msg); + QMessageBox::warning(this, tr("Steam Linux Runtime"), + tr("Download failed:\n%1\n\nSLR has been disabled for this instance.") + .arg(msg)); + ui->steamLinuxRuntimeCheckBox->blockSignals(true); + ui->steamLinuxRuntimeCheckBox->setChecked(false); + ui->steamLinuxRuntimeCheckBox->blockSignals(false); + const auto* inst = singleSelection(); + if (inst) { + const QString ini = inst->iniPath(); + if (!ini.isEmpty()) { + QSettings s(ini, QSettings::IniFormat); + s.setValue("fluorine/use_slr", false); + } + } + } else { + MOBase::log::info("[SLR] Steam Linux Runtime installed successfully"); + progress->setLabelText(tr("Steam Linux Runtime is ready.")); + } + delete cancelFlag; + }); + + int* cancelPtr = cancelFlag; + watcher->setFuture(QtConcurrent::run([cancelPtr]() -> Result { + return nak_download_slr(nullptr, nullptr, cancelPtr); + })); + + progress->show(); +} diff --git a/src/src/instancemanagerdialog.h b/src/src/instancemanagerdialog.h index c8862fd..fec48ff 100644 --- a/src/src/instancemanagerdialog.h +++ b/src/src/instancemanagerdialog.h @@ -148,6 +148,11 @@ private: // deletes the given files, returns false on error // bool doDelete(const QStringList& files, bool recycle); + + // downloads SLR if not already installed; called when the SLR checkbox is + // toggled on + // + void downloadSLRIfNeeded(); }; #endif // MODORGANIZER_INSTANCEMANAGERDIALOG_INCLUDED diff --git a/src/src/instancemanagerdialog.ui b/src/src/instancemanagerdialog.ui index 2a1d136..6039edc 100644 --- a/src/src/instancemanagerdialog.ui +++ b/src/src/instancemanagerdialog.ui @@ -324,6 +324,19 @@ + + + + Use Steam Linux Runtime (SLR) + + + Run games inside the Steam Linux Runtime container (pressure-vessel). Provides GStreamer, 32-bit libs, and other dependencies for non-FHS distros like NixOS. Requires Steam to be installed. + + + true + + + diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp index 457a3dd..882ecd7 100644 --- a/src/src/mainwindow.cpp +++ b/src/src/mainwindow.cpp @@ -92,6 +92,7 @@ along with Mod Organizer. If not, see . #include "shared/fileentry.h" #include "shared/filesorigin.h" +#include #include #include #include @@ -2365,6 +2366,57 @@ void MainWindow::on_startButton_clicked() ui->startButton->setEnabled(true); }); + // Pre-check: if this executable uses Proton and the current instance has SLR + // enabled, download SLR before launching if it isn't installed yet. + if (selectedExecutable->useProton()) { + const auto* s = Settings::maybeInstance(); + bool useSLR = true; + if (s) { + QSettings instanceIni(s->filename(), QSettings::IniFormat); + useSLR = instanceIni.value("fluorine/use_slr", true).toBool(); + } + if (useSLR && !nak_slr_is_installed()) { + auto* progress = new QProgressDialog( + tr("Downloading Steam Linux Runtime (~180 MB)...\n" + "This is required to launch games. Check the MO2 log for details."), + tr("Cancel"), 0, 0, this); + progress->setWindowTitle(tr("Steam Linux Runtime")); + progress->setWindowModality(Qt::WindowModal); + progress->setMinimumDuration(0); + + int cancelFlag = 0; + connect(progress, &QProgressDialog::canceled, this, [&cancelFlag] { + cancelFlag = 1; + }); + + // Run download synchronously using an event loop so the dialog stays responsive. + QFutureWatcher watcher; + QEventLoop loop; + connect(&watcher, &QFutureWatcher::finished, &loop, &QEventLoop::quit); + watcher.setFuture(QtConcurrent::run([&cancelFlag]() -> char* { + return nak_download_slr(nullptr, nullptr, &cancelFlag); + })); + progress->show(); + loop.exec(); + progress->close(); + progress->deleteLater(); + + char* err = watcher.result(); + if (cancelFlag) { + return; // user cancelled, don't launch + } + if (err) { + const QString msg = QString::fromUtf8(err); + nak_string_free(err); + log::error("[SLR] Download failed: {}", msg); + QMessageBox::warning(this, tr("Steam Linux Runtime"), + tr("Steam Linux Runtime download failed:\n%1\n\n" + "You can disable SLR in the Instance Manager and try again.").arg(msg)); + return; + } + } + } + if (selectedExecutable->minimizeToSystemTray()) { m_SystemTrayManager->minimizeToSystemTray(); } diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp index 3d1741e..f25d355 100644 --- a/src/src/protonlauncher.cpp +++ b/src/src/protonlauncher.cpp @@ -135,6 +135,30 @@ QString detectSteamPath() return {}; } +// Find the Steam Linux Runtime (sniper) run script. +// SLR wraps the Proton launch in a pressure-vessel container that provides +// GStreamer, 32-bit libs, and an FHS-compliant environment — required on +// NixOS and other non-FHS distros. +// Returns empty string if SLR is not installed. +QString detectSLRRunScript() +{ + const QString steamPath = detectSteamPath(); + + // Common SLR sniper locations relative to Steam install + const QStringList candidates = { + steamPath + "/steamapps/common/SteamLinuxRuntime_sniper/run", + QDir::home().filePath(".local/share/Steam/steamapps/common/SteamLinuxRuntime_sniper/run"), + "/usr/lib/pressure-vessel/wrap", + }; + + for (const QString& path : candidates) { + if (!path.isEmpty() && QFileInfo::exists(path)) { + return path; + } + } + return {}; +} + // Detect an available terminal emulator on the system. QString findTerminal() { @@ -365,6 +389,12 @@ ProtonLauncher& ProtonLauncher::setSteamDrm(bool useSteamDrm) return *this; } +ProtonLauncher& ProtonLauncher::setUseSLR(bool useSLR) +{ + m_useSLR = useSLR; + return *this; +} + ProtonLauncher& ProtonLauncher::setStoreVariant(const QString& variant) { m_storeVariant = variant.trimmed(); @@ -418,9 +448,28 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const // before starting a new one. const QStringList protonArgs = QStringList() << "waitforexitandrun" << m_binary << m_arguments; + // If SLR is enabled, wrap the whole proton invocation inside the + // pressure-vessel container provided by SteamLinuxRuntime_sniper. + // The `run` script accepts `-- [args...]` and re-executes the + // command inside the container. QString program; QStringList arguments; - wrapProgram(m_wrapperCommands, protonScript, protonArgs, program, arguments); + if (m_useSLR) { + if (char* slrScript = nak_slr_get_run_script(); slrScript != nullptr) { + const QString runScript = QString::fromUtf8(slrScript); + nak_string_free(slrScript); + MOBase::log::info("SLR: wrapping launch with {}", runScript); + // Build: [wrappers] run_script -- proton_script protonArgs + QStringList slrArgs; + slrArgs << "--" << protonScript << protonArgs; + wrapProgram(m_wrapperCommands, runScript, slrArgs, program, arguments); + } else { + MOBase::log::warn("SLR enabled but run script not found — launching without SLR"); + wrapProgram(m_wrapperCommands, protonScript, protonArgs, program, arguments); + } + } else { + wrapProgram(m_wrapperCommands, protonScript, protonArgs, program, arguments); + } QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove("PYTHONHOME"); @@ -476,6 +525,8 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const } MOBase::log::info("Proton launch: '{}' run '{}'", protonScript, m_binary); + MOBase::log::info("Final command: '{}' {}", program, + arguments.join(" ").toStdString()); if (!m_workingDir.isEmpty()) { env.insert("PWD", m_workingDir); diff --git a/src/src/protonlauncher.h b/src/src/protonlauncher.h index 11d942a..13be35a 100644 --- a/src/src/protonlauncher.h +++ b/src/src/protonlauncher.h @@ -21,6 +21,7 @@ public: ProtonLauncher& setSteamAppId(uint32_t id); ProtonLauncher& setWrapper(const QString& wrapperCmd); ProtonLauncher& setSteamDrm(bool useSteamDrm); + ProtonLauncher& setUseSLR(bool useSLR); ProtonLauncher& setStoreVariant(const QString& variant); ProtonLauncher& addEnvVar(const QString& key, const QString& value); ProtonLauncher& setUseTerminal(bool useTerminal); @@ -41,6 +42,7 @@ private: uint32_t m_steamAppId; QStringList m_wrapperCommands; bool m_useSteamDrm; + bool m_useSLR = true; QString m_storeVariant; // "GOG", "Epic", or empty for Steam QMap m_envVars; QMap m_wrapperEnvVars; diff --git a/src/src/settingsdialog.ui b/src/src/settingsdialog.ui index 646b9b6..3e1c54a 100644 --- a/src/src/settingsdialog.ui +++ b/src/src/settingsdialog.ui @@ -1803,28 +1803,38 @@ If you disable this feature, MO will only display official DLCs this way. Please - + + + + Download Steam Linux Runtime + + + Download SteamLinuxRuntime_sniper (~180 MB) from Valve's servers. Provides GStreamer, 32-bit libs, and an FHS environment for non-FHS distros (NixOS, etc.). Required for the "Use SLR" option in the Instance Manager. + + + + Status: - + No Prefix - + 0 - + Show Install Log @@ -1837,7 +1847,7 @@ If you disable this feature, MO will only display official DLCs this way. Please - + true diff --git a/src/src/settingsdialogproton.cpp b/src/src/settingsdialogproton.cpp index 8aa2481..bd14944 100644 --- a/src/src/settingsdialogproton.cpp +++ b/src/src/settingsdialogproton.cpp @@ -23,8 +23,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -95,6 +98,8 @@ ProtonSettingsTab::ProtonSettingsTab(Settings& s, SettingsDialog& d) &ProtonSettingsTab::onWinetricks); QObject::connect(ui->prefixLocationBrowseButton, &QPushButton::clicked, this, &ProtonSettingsTab::onBrowsePrefixLocation); + QObject::connect(ui->downloadSLRButton, &QPushButton::clicked, this, + &ProtonSettingsTab::onDownloadSLR); QObject::connect(&m_installWatcher, &QFutureWatcher::finished, this, &ProtonSettingsTab::onInstallFinished); @@ -297,6 +302,58 @@ void ProtonSettingsTab::onOpenPrefixFolder() MOBase::shell::Explore(QDir(*path)); } +void ProtonSettingsTab::onDownloadSLR() +{ + if (nak_slr_is_installed()) { + QMessageBox::information(parentWidget(), tr("Steam Linux Runtime"), + tr("Steam Linux Runtime is already installed.")); + return; + } + + ui->downloadSLRButton->setEnabled(false); + auto* progress = new QProgressDialog( + tr("Downloading Steam Linux Runtime (~180 MB)...\n" + "Check the MO2 log for progress details."), + tr("Cancel"), 0, 0, parentWidget()); + progress->setWindowTitle(tr("Steam Linux Runtime")); + progress->setWindowModality(Qt::WindowModal); + progress->setMinimumDuration(0); + + auto* cancelFlag = new int(0); + connect(progress, &QProgressDialog::canceled, this, [cancelFlag] { + *cancelFlag = 1; + }); + + auto* watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, + [this, watcher, progress, cancelFlag] { + progress->close(); + watcher->deleteLater(); + progress->deleteLater(); + ui->downloadSLRButton->setEnabled(true); + + char* err = watcher->result(); + if (err) { + const QString msg = QString::fromUtf8(err); + nak_string_free(err); + MOBase::log::error("[SLR] Download failed: {}", msg); + QMessageBox::warning(parentWidget(), tr("Steam Linux Runtime"), + tr("Download failed:\n%1").arg(msg)); + } else { + MOBase::log::info("[SLR] Steam Linux Runtime installed successfully"); + QMessageBox::information(parentWidget(), tr("Steam Linux Runtime"), + tr("Steam Linux Runtime installed successfully.")); + } + delete cancelFlag; + }); + + int* cancelPtr = cancelFlag; + watcher->setFuture(QtConcurrent::run([cancelPtr]() -> char* { + return nak_download_slr(nullptr, nullptr, cancelPtr); + })); + progress->show(); +} + void ProtonSettingsTab::onBrowsePrefixLocation() { const QString dir = QFileDialog::getExistingDirectory( diff --git a/src/src/settingsdialogproton.h b/src/src/settingsdialogproton.h index a1221c7..d5f0188 100644 --- a/src/src/settingsdialogproton.h +++ b/src/src/settingsdialogproton.h @@ -33,6 +33,7 @@ private: void onFixGameRegistries(); void onWinetricks(); void onBrowsePrefixLocation(); + void onDownloadSLR(); void showGameRegistryDialog(); QString ensureWinetricks(); diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp index b0e7eea..4a4d32b 100644 --- a/src/src/spawn.cpp +++ b/src/src/spawn.cpp @@ -647,16 +647,19 @@ int spawn(const SpawnParameters &sp, pid_t &processId) { // QSettings). const Settings *instanceForLaunch = Settings::maybeInstance(); bool useSteamDrm = true; // default + bool useSLR = true; // default on QString storeVariant; if (instanceForLaunch) { const QSettings instanceIni(instanceForLaunch->filename(), QSettings::IniFormat); - useSteamDrm = instanceIni.value("fluorine/steam_drm", true).toBool(); - storeVariant = instanceIni.value("game_edition").toString().trimmed(); + useSteamDrm = instanceIni.value("fluorine/steam_drm", true).toBool(); + useSLR = instanceIni.value("fluorine/use_slr", true).toBool(); + storeVariant = instanceIni.value("game_edition").toString().trimmed(); } launcher.setSteamDrm(useSteamDrm) - .setStoreVariant(storeVariant); + .setStoreVariant(storeVariant) + .setUseSLR(useSLR); const QString prefixPath = resolvePrefixPath(); if (prefixPath.isEmpty()) { -- cgit v1.3.1