Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub struct GameConfig {
pub struct LauncherConfig {
pub steam_library_path: String,
pub proton_version: String,
pub steam_setup_path: Option<String>,
pub enable_cloud_sync: bool,
#[serde(default)]
pub use_shared_compat_data: bool,
Expand Down Expand Up @@ -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(),
Expand Down
121 changes: 121 additions & 0 deletions src/launch.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf> {
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<PathBuf> {
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"))
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
31 changes: 29 additions & 2 deletions src/steam_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand Down