From b42bd7f306630c688babc3b5cb7168822dac12de Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:27:23 +0000 Subject: [PATCH 1/3] Implement stable Ghost Steam installation and launch logic. - Task 1: Add `find_wine_binary` to dynamically locate the wine executable in various Proton versions. - Task 2: Implement `install_ghost_steam_in_prefix` for a clean install into Proton prefixes using raw wine and removing stubs. - Task 3: Update `spawn_game_process` to trigger Ghost Steam install before launch and force real Steam binaries via WINEDLLOVERRIDES. - Add `steam_setup_path` configuration and UI field. Co-authored-by: weter11 <14630689+weter11@users.noreply.github.com> --- src/config.rs | 2 ++ src/launch.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/steam_client.rs | 18 +++++++++++ src/ui.rs | 10 ++++++ 5 files changed, 105 insertions(+) create mode 100644 src/launch.rs diff --git a/src/config.rs b/src/config.rs index 10d02792..6257a619 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,7 @@ pub struct GameConfig { pub struct LauncherConfig { pub steam_library_path: String, pub proton_version: String, + pub steam_setup_path: Option, pub enable_cloud_sync: bool, #[serde(default)] pub use_shared_compat_data: bool, @@ -46,6 +47,7 @@ impl Default for LauncherConfig { Self { steam_library_path, proton_version: "experimental".to_string(), + steam_setup_path: None, enable_cloud_sync: true, use_shared_compat_data: false, preferred_launch_options: HashMap::new(), diff --git a/src/launch.rs b/src/launch.rs new file mode 100644 index 00000000..37ebd835 --- /dev/null +++ b/src/launch.rs @@ -0,0 +1,74 @@ +use anyhow::{anyhow, Context, Result}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use tracing::{info, warn}; + +/// Task 1: Find the wine binary within a Proton directory dynamically. +pub fn find_wine_binary(proton_path: &Path) -> Result { + let candidates = [ + proton_path.join("files/bin/wine"), // Modern Proton / GE-Proton + proton_path.join("dist/bin/wine"), // Older Proton + proton_path.join("bin/wine"), // Ancient/System + ]; + + for candidate in &candidates { + if candidate.exists() { + info!("Found wine binary at: {:?}", candidate); + return Ok(candidate.clone()); + } + } + + Err(anyhow!("Wine binary not found in Proton dir: {:?}", proton_path)) +} + +/// Task 2: Install the official Windows Steam Client into the game's Proton prefix. +pub fn install_ghost_steam_in_prefix( + proton_path: &Path, + prefix_path: &Path, + steam_setup_exe: &Path, +) -> Result<()> { + if !steam_setup_exe.exists() { + return Err(anyhow!("SteamSetup.exe not found at {:?}", steam_setup_exe)); + } + + // Step A: Sanitize Target + let steam_dir = prefix_path.join("drive_c/Program Files (x86)/Steam"); + if steam_dir.exists() { + info!("Sanitizing target Steam directory: {:?}", steam_dir); + let stubs = ["steam.exe", "lsteamclient.dll", "tier0_s.dll", "vstdlib_s.dll"]; + for stub in stubs { + let stub_path = steam_dir.join(stub); + if stub_path.exists() { + info!("Removing Proton stub: {:?}", stub_path); + let _ = std::fs::remove_file(stub_path); + } + } + } + + // Step B: Execute Installer (Raw Wine) + let wine_bin = find_wine_binary(proton_path)?; + let absolute_prefix = prefix_path.canonicalize() + .context("Failed to canonicalize prefix path")?; + + info!("Installing Ghost Steam into prefix: {:?}", absolute_prefix); + + let mut cmd = Command::new(&wine_bin); + cmd.arg(steam_setup_exe) + .arg("/S"); + + // Env Vars (CRITICAL) + cmd.env("WINEPREFIX", &absolute_prefix); + cmd.env("WINEDLLOVERRIDES", "mscoree=d;mshtml=d"); + + // REMOVE Steam-specific env vars to ensure clean install + cmd.env_remove("STEAM_COMPAT_DATA_PATH"); + cmd.env_remove("STEAM_COMPAT_CLIENT_INSTALL_PATH"); + cmd.env_remove("SteamAppId"); + + let status = cmd.status().context("Failed to execute Steam installer")?; + if !status.success() { + warn!("Steam installer exited with non-zero status: {:?}", status.code()); + } + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index e127044e..c2cb2eee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod cloud_sync; pub mod cm_list; pub mod config; pub mod depot_browser; +pub mod launch; pub mod library; pub mod models; pub mod steam_client; diff --git a/src/steam_client.rs b/src/steam_client.rs index 84a56eb5..30954822 100644 --- a/src/steam_client.rs +++ b/src/steam_client.rs @@ -2188,6 +2188,21 @@ impl SteamClient { std::fs::create_dir_all(&compat_data_path) .with_context(|| format!("failed creating {}", compat_data_path.display()))?; + let proton_root = resolved_proton.parent().unwrap_or(&resolved_proton); + let steam_setup_path = launcher_config.steam_setup_path.clone() + .map(PathBuf::from) + .unwrap_or_else(|| { + crate::config::config_dir().unwrap_or_default().join("SteamSetup.exe") + }); + + if let Err(e) = crate::launch::install_ghost_steam_in_prefix( + proton_root, + &compat_data_path, + &steam_setup_path, + ) { + tracing::warn!("Ghost Steam installation failed for App {}: {e}", app.app_id); + } + let mut cmd = Command::new(&resolved_proton); cmd.arg("run").arg(&executable).args(&args); cmd.current_dir(&install_dir); @@ -2195,6 +2210,9 @@ impl SteamClient { cmd.env("STEAM_COMPAT_DATA_PATH", &compat_data_path); cmd.env("STEAM_COMPAT_CLIENT_INSTALL_PATH", &library_root); + // Task 3: Force use of real Steam files + cmd.env("WINEDLLOVERRIDES", "steam.exe=n;lsteamclient=n;steam_api=n"); + if let Some(config) = user_config { for (key, val) in &config.env_variables { cmd.env(key, val); diff --git a/src/ui.rs b/src/ui.rs index bbea5edb..93573b08 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1896,6 +1896,16 @@ impl eframe::App for SteamLauncher { &self.custom_protons }; + ui.label("SteamSetup.exe Path"); + let mut setup_path = self.launcher_config.steam_setup_path.clone().unwrap_or_default(); + if ui.text_edit_singleline(&mut setup_path).changed() { + self.launcher_config.steam_setup_path = if setup_path.trim().is_empty() { + None + } else { + Some(setup_path.trim().to_string()) + }; + } + egui::ComboBox::from_label("Proton Version") .selected_text(self.launcher_config.proton_version.clone()) .show_ui(ui, |ui| { From 22e042ca36ea49ed845ebd47d9005b52429c447b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:41:27 +0000 Subject: [PATCH 2/3] Implement hybrid Ghost Steam installation using Proton Runtime. - Revert to using the Proton script (`proton run`) for installation to ensure TLS/SSL network libraries are available. - Implement "Brain Surgery" environment variables: set `WINEDLLOVERRIDES` and unset Proton's internal Steam hooks. - Use the correct `pfx` subdirectory for the Wine prefix. - Add sanitization to remove existing Proton stubs before install. - Optimize by skipping installation if `steam.exe` already exists. - Force use of real Steam files during game runtime via `WINEDLLOVERRIDES`. Co-authored-by: weter11 <14630689+weter11@users.noreply.github.com> --- src/launch.rs | 71 +++++++++++++++++++++++---------------------- src/steam_client.rs | 12 ++++---- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/launch.rs b/src/launch.rs index 37ebd835..2b3c4722 100644 --- a/src/launch.rs +++ b/src/launch.rs @@ -1,27 +1,10 @@ use anyhow::{anyhow, Context, Result}; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::Command; use tracing::{info, warn}; -/// Task 1: Find the wine binary within a Proton directory dynamically. -pub fn find_wine_binary(proton_path: &Path) -> Result { - let candidates = [ - proton_path.join("files/bin/wine"), // Modern Proton / GE-Proton - proton_path.join("dist/bin/wine"), // Older Proton - proton_path.join("bin/wine"), // Ancient/System - ]; - - for candidate in &candidates { - if candidate.exists() { - info!("Found wine binary at: {:?}", candidate); - return Ok(candidate.clone()); - } - } - - Err(anyhow!("Wine binary not found in Proton dir: {:?}", proton_path)) -} - -/// Task 2: Install the official Windows Steam Client into the game's Proton prefix. +/// Install the official Windows Steam Client into the game's Proton prefix. +/// Now using the Proton Script (proton run) but with overrides to bypass fake Steam stubs. pub fn install_ghost_steam_in_prefix( proton_path: &Path, prefix_path: &Path, @@ -31,44 +14,64 @@ pub fn install_ghost_steam_in_prefix( return Err(anyhow!("SteamSetup.exe not found at {:?}", steam_setup_exe)); } - // Step A: Sanitize Target let steam_dir = prefix_path.join("drive_c/Program Files (x86)/Steam"); + let installed_steam_exe = steam_dir.join("steam.exe"); + + // Optimization: Skip installation if Ghost Steam is already present + if installed_steam_exe.exists() { + info!("Ghost Steam already installed at {:?}", installed_steam_exe); + return Ok(()); + } + + // Step 1: Sanitize Target (Crucial) + // We remove Proton's fake stubs to ensure a clean environment for the real installer. if steam_dir.exists() { info!("Sanitizing target Steam directory: {:?}", steam_dir); - let stubs = ["steam.exe", "lsteamclient.dll", "tier0_s.dll", "vstdlib_s.dll"]; + let stubs = ["lsteamclient.dll", "steam.exe"]; for stub in stubs { let stub_path = steam_dir.join(stub); if stub_path.exists() { - info!("Removing Proton stub: {:?}", stub_path); + info!("Removing Proton stub/file: {:?}", stub_path); let _ = std::fs::remove_file(stub_path); } } } - // Step B: Execute Installer (Raw Wine) - let wine_bin = find_wine_binary(proton_path)?; + // Step 2: Execute Installer (Proton Run) let absolute_prefix = prefix_path.canonicalize() .context("Failed to canonicalize prefix path")?; - info!("Installing Ghost Steam into prefix: {:?}", absolute_prefix); + info!("Installing Ghost Steam into prefix using Proton: {:?}", absolute_prefix); - let mut cmd = Command::new(&wine_bin); - cmd.arg(steam_setup_exe) + // proton_path is expected to be the path to the 'proton' executable/script + let mut cmd = Command::new(proton_path); + cmd.arg("run") + .arg(steam_setup_exe) .arg("/S"); - // Env Vars (CRITICAL) - cmd.env("WINEPREFIX", &absolute_prefix); - cmd.env("WINEDLLOVERRIDES", "mscoree=d;mshtml=d"); + // Env Vars (The 'Brain Surgery') + // 1. Force Native DLLs (This ignores Proton's fake lsteamclient.dll) + cmd.env("WINEDLLOVERRIDES", "steam.exe=n;lsteamclient=n;steam_api=n;steam_api64=n;steamclient=n;mscoree=d;mshtml=d"); - // REMOVE Steam-specific env vars to ensure clean install - cmd.env_remove("STEAM_COMPAT_DATA_PATH"); + // 2. Unset Proton's Steam Hooks (Hide the fact we are in Steam) cmd.env_remove("STEAM_COMPAT_CLIENT_INSTALL_PATH"); cmd.env_remove("SteamAppId"); + cmd.env_remove("SteamGameId"); - let status = cmd.status().context("Failed to execute Steam installer")?; + // 3. Set the Prefix Explicitly + cmd.env("WINEPREFIX", &absolute_prefix); + + let status = cmd.status().context("Failed to execute Steam installer via Proton")?; if !status.success() { warn!("Steam installer exited with non-zero status: {:?}", status.code()); } + // Step 4: Post-Install Check + if installed_steam_exe.exists() { + info!("Ghost Steam successfully installed at {:?}", installed_steam_exe); + } else { + warn!("Ghost Steam installation might have failed: steam.exe not found at {:?}", installed_steam_exe); + } + Ok(()) } diff --git a/src/steam_client.rs b/src/steam_client.rs index 30954822..f6b84a81 100644 --- a/src/steam_client.rs +++ b/src/steam_client.rs @@ -2185,10 +2185,10 @@ impl SteamClient { .join("compatdata") .join(app.app_id.to_string()); - std::fs::create_dir_all(&compat_data_path) - .with_context(|| format!("failed creating {}", compat_data_path.display()))?; + let prefix_path = compat_data_path.join("pfx"); + std::fs::create_dir_all(&prefix_path) + .with_context(|| format!("failed creating {}", prefix_path.display()))?; - let proton_root = resolved_proton.parent().unwrap_or(&resolved_proton); let steam_setup_path = launcher_config.steam_setup_path.clone() .map(PathBuf::from) .unwrap_or_else(|| { @@ -2196,8 +2196,8 @@ impl SteamClient { }); if let Err(e) = crate::launch::install_ghost_steam_in_prefix( - proton_root, - &compat_data_path, + &resolved_proton, + &prefix_path, &steam_setup_path, ) { tracing::warn!("Ghost Steam installation failed for App {}: {e}", app.app_id); @@ -2211,7 +2211,7 @@ impl SteamClient { cmd.env("STEAM_COMPAT_CLIENT_INSTALL_PATH", &library_root); // Task 3: Force use of real Steam files - cmd.env("WINEDLLOVERRIDES", "steam.exe=n;lsteamclient=n;steam_api=n"); + cmd.env("WINEDLLOVERRIDES", "steam.exe=n;lsteamclient=n;steam_api=n;steam_api64=n;steamclient=n"); if let Some(config) = user_config { for (key, val) in &config.env_variables { From f6f5e864a6086942b46fa654fc8e03d492eed890 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 18:37:15 +0000 Subject: [PATCH 3/3] Implement fuzzy discovery for Ghost Steam executable. - Implement `get_installed_steam_path` in `src/launch.rs` to scan for `steam.exe` in multiple common paths with fuzzy case matching. - Update `install_ghost_steam_in_prefix` to return the discovered path and log directory contents on failure. - Update `spawn_game_process` in `src/steam_client.rs` to use the found filename for `WINEDLLOVERRIDES`, handling case-sensitivity quirks. Co-authored-by: weter11 <14630689+weter11@users.noreply.github.com> --- src/launch.rs | 72 ++++++++++++++++++++++++++++++++++++--------- src/steam_client.rs | 17 ++++++++--- 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/launch.rs b/src/launch.rs index 2b3c4722..6e0946aa 100644 --- a/src/launch.rs +++ b/src/launch.rs @@ -1,30 +1,65 @@ use anyhow::{anyhow, Context, Result}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; use tracing::{info, warn}; +/// Helper to find the Steam executable with fuzzy case matching in common paths. +pub fn get_installed_steam_path(prefix: &Path) -> Option { + let drive_c = prefix.join("drive_c"); + let candidates = vec![ + "Program Files (x86)/Steam/steam.exe", + "Program Files (x86)/Steam/Steam.exe", + "Program Files/Steam/steam.exe", + "Program Files/Steam/Steam.exe", + "Steam/steam.exe", + "Steam/Steam.exe", + "Program Files (x86)/steam/steam.exe", + ]; + + for subpath in candidates { + let p = drive_c.join(subpath); + if p.exists() { + return Some(p); + } + } + None +} + +/// Debug helper to list directory contents. +fn list_directory_contents(path: &Path) { + if let Ok(entries) = std::fs::read_dir(path) { + info!("Listing directory contents for {:?}:", path); + for entry in entries.flatten() { + if let Ok(meta) = entry.metadata() { + let type_str = if meta.is_dir() { "DIR" } else { "FILE" }; + info!(" [{}] {:?}", type_str, entry.file_name()); + } + } + } else { + warn!("Failed to read directory {:?}", path); + } +} + /// Install the official Windows Steam Client into the game's Proton prefix. -/// Now using the Proton Script (proton run) but with overrides to bypass fake Steam stubs. +/// Returns the path to the installed steam.exe on success. pub fn install_ghost_steam_in_prefix( proton_path: &Path, prefix_path: &Path, steam_setup_exe: &Path, -) -> Result<()> { +) -> Result { if !steam_setup_exe.exists() { return Err(anyhow!("SteamSetup.exe not found at {:?}", steam_setup_exe)); } - let steam_dir = prefix_path.join("drive_c/Program Files (x86)/Steam"); - let installed_steam_exe = steam_dir.join("steam.exe"); - // Optimization: Skip installation if Ghost Steam is already present - if installed_steam_exe.exists() { - info!("Ghost Steam already installed at {:?}", installed_steam_exe); - return Ok(()); + if let Some(steam_exe) = get_installed_steam_path(prefix_path) { + info!("Ghost Steam already installed at {:?}", steam_exe); + return Ok(steam_exe); } // Step 1: Sanitize Target (Crucial) // We remove Proton's fake stubs to ensure a clean environment for the real installer. + let steam_dir = prefix_path.join("drive_c/Program Files (x86)/Steam"); if steam_dir.exists() { info!("Sanitizing target Steam directory: {:?}", steam_dir); let stubs = ["lsteamclient.dll", "steam.exe"]; @@ -67,11 +102,20 @@ pub fn install_ghost_steam_in_prefix( } // Step 4: Post-Install Check - if installed_steam_exe.exists() { - info!("Ghost Steam successfully installed at {:?}", installed_steam_exe); + if let Some(steam_exe) = get_installed_steam_path(prefix_path) { + info!("Ghost Steam successfully installed at {:?}", steam_exe); + Ok(steam_exe) } else { - warn!("Ghost Steam installation might have failed: steam.exe not found at {:?}", installed_steam_exe); + warn!("Ghost Steam installation might have failed: steam.exe not found in common paths."); + let drive_c = prefix_path.join("drive_c"); + list_directory_contents(&drive_c); + if let Ok(entries) = std::fs::read_dir(&drive_c) { + for entry in entries.flatten() { + if entry.metadata().map(|m| m.is_dir()).unwrap_or(false) { + list_directory_contents(&entry.path()); + } + } + } + Err(anyhow!("Ghost Steam installation failed: steam.exe not found")) } - - Ok(()) } diff --git a/src/steam_client.rs b/src/steam_client.rs index f6b84a81..eb2b601e 100644 --- a/src/steam_client.rs +++ b/src/steam_client.rs @@ -2195,13 +2195,17 @@ impl SteamClient { crate::config::config_dir().unwrap_or_default().join("SteamSetup.exe") }); - if let Err(e) = crate::launch::install_ghost_steam_in_prefix( + let _ghost_steam_exe = match crate::launch::install_ghost_steam_in_prefix( &resolved_proton, &prefix_path, &steam_setup_path, ) { - tracing::warn!("Ghost Steam installation failed for App {}: {e}", app.app_id); - } + Ok(path) => Some(path), + Err(e) => { + tracing::warn!("Ghost Steam installation failed for App {}: {e}", app.app_id); + None + } + }; let mut cmd = Command::new(&resolved_proton); cmd.arg("run").arg(&executable).args(&args); @@ -2211,7 +2215,12 @@ impl SteamClient { cmd.env("STEAM_COMPAT_CLIENT_INSTALL_PATH", &library_root); // Task 3: Force use of real Steam files - cmd.env("WINEDLLOVERRIDES", "steam.exe=n;lsteamclient=n;steam_api=n;steam_api64=n;steamclient=n"); + let steam_filename = _ghost_steam_exe.as_ref() + .and_then(|p| p.file_name()) + .and_then(|f| f.to_str()) + .unwrap_or("steam.exe"); + + cmd.env("WINEDLLOVERRIDES", format!("{}={};lsteamclient=n;steam_api=n;steam_api64=n;steamclient=n", steam_filename, "n")); if let Some(config) = user_config { for (key, val) in &config.env_variables {