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..6e0946aa --- /dev/null +++ b/src/launch.rs @@ -0,0 +1,121 @@ +use anyhow::{anyhow, Context, Result}; +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. +/// 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 { + if !steam_setup_exe.exists() { + return Err(anyhow!("SteamSetup.exe not found at {:?}", steam_setup_exe)); + } + + // Optimization: Skip installation if Ghost Steam is already present + 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"]; + for stub in stubs { + let stub_path = steam_dir.join(stub); + if stub_path.exists() { + info!("Removing Proton stub/file: {:?}", stub_path); + let _ = std::fs::remove_file(stub_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 using Proton: {:?}", absolute_prefix); + + // 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 (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"); + + // 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"); + + // 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 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 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")) + } +} 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..eb2b601e 100644 --- a/src/steam_client.rs +++ b/src/steam_client.rs @@ -2185,8 +2185,27 @@ 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 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") + }); + + let _ghost_steam_exe = match crate::launch::install_ghost_steam_in_prefix( + &resolved_proton, + &prefix_path, + &steam_setup_path, + ) { + 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); @@ -2195,6 +2214,14 @@ 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 + 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 { 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| {