| 1 | //! wanda launch - Launch a game with WeMod |
| 2 | |
| 3 | use clap::Args; |
| 4 | use console::style; |
| 5 | use std::path::PathBuf; |
| 6 | use wanda_core::{ |
| 7 | config::WandaConfig, |
| 8 | launcher::{GameLauncher, LaunchConfig}, |
| 9 | prefix::PrefixManager, |
| 10 | steam::{ProtonManager, SteamInstallation}, |
| 11 | Result, WandaError, |
| 12 | }; |
| 13 | |
| 14 | #[derive(Args)] |
| 15 | pub struct LaunchArgs { |
| 16 | /// Game name or App ID |
| 17 | game: String, |
| 18 | |
| 19 | /// Launch without WeMod |
| 20 | #[arg(long)] |
| 21 | no_wemod: bool, |
| 22 | |
| 23 | /// Delay in seconds before launching game (after WeMod starts) |
| 24 | #[arg(long, default_value = "3")] |
| 25 | delay: u64, |
| 26 | |
| 27 | /// Additional arguments to pass to the game |
| 28 | #[arg(long)] |
| 29 | args: Option<String>, |
| 30 | |
| 31 | /// Wait for the game to exit |
| 32 | #[arg(long, short)] |
| 33 | wait: bool, |
| 34 | |
| 35 | /// Standalone mode: launch WeMod only, start the game from WeMod's UI |
| 36 | #[arg(long)] |
| 37 | standalone: bool, |
| 38 | |
| 39 | /// Specify Proton version to use (overrides config) |
| 40 | #[arg(long)] |
| 41 | proton: Option<String>, |
| 42 | } |
| 43 | |
| 44 | pub async fn run(args: LaunchArgs, config_path: Option<PathBuf>) -> Result<()> { |
| 45 | let config = match &config_path { |
| 46 | Some(path) => WandaConfig::load_from(path)?, |
| 47 | None => WandaConfig::load()?, |
| 48 | }; |
| 49 | |
| 50 | // Discover Steam and find the game |
| 51 | let steam = SteamInstallation::discover(&config)?; |
| 52 | |
| 53 | // Try to parse as App ID first |
| 54 | let game = if let Ok(app_id) = args.game.parse::<u32>() { |
| 55 | steam.find_game(app_id).ok_or(WandaError::GameNotFound { app_id })? |
| 56 | } else { |
| 57 | // Search by name |
| 58 | let matches = steam.find_game_by_name(&args.game); |
| 59 | match matches.len() { |
| 60 | 0 => { |
| 61 | return Err(WandaError::LaunchFailed { |
| 62 | reason: format!("No game found matching '{}'", args.game), |
| 63 | }) |
| 64 | } |
| 65 | 1 => matches[0], |
| 66 | _ => { |
| 67 | println!("Multiple games found matching '{}':", args.game); |
| 68 | for game in &matches { |
| 69 | println!(" {} - {}", game.app_id, game.name); |
| 70 | } |
| 71 | return Err(WandaError::LaunchFailed { |
| 72 | reason: "Please specify the exact App ID".to_string(), |
| 73 | }); |
| 74 | } |
| 75 | } |
| 76 | }; |
| 77 | |
| 78 | println!( |
| 79 | "Launching {} {}", |
| 80 | style(&game.name).bold(), |
| 81 | style(format!("({})", game.app_id)).dim() |
| 82 | ); |
| 83 | |
| 84 | // Check if launching with WeMod |
| 85 | let with_wemod = !args.no_wemod; |
| 86 | |
| 87 | if with_wemod { |
| 88 | // Load WANDA prefix |
| 89 | let mut prefix_manager = PrefixManager::new(&config); |
| 90 | prefix_manager.load()?; |
| 91 | |
| 92 | let prefix = prefix_manager.get("default").ok_or_else(|| WandaError::LaunchFailed { |
| 93 | reason: "WANDA not initialized. Run 'wanda init' first.".to_string(), |
| 94 | })?; |
| 95 | |
| 96 | // Check if WeMod is installed |
| 97 | if !prefix.wemod_installed { |
| 98 | return Err(WandaError::WemodNotInstalled); |
| 99 | } |
| 100 | |
| 101 | // In standalone mode, use the GAME's compat data prefix instead |
| 102 | // of wanda's prefix. This ensures WeMod and the game share the |
| 103 | // same wineserver. WeMod's files are symlinked from wanda's |
| 104 | // prefix into the game's prefix. |
| 105 | let game_prefix; |
| 106 | let launch_prefix = if args.standalone { |
| 107 | let compat_data = game.compat_data_path.as_ref().ok_or_else(|| { |
| 108 | WandaError::LaunchFailed { |
| 109 | reason: format!( |
| 110 | "Game '{}' has no Proton compat data. Has it been launched with Proton before?", |
| 111 | game.name |
| 112 | ), |
| 113 | } |
| 114 | })?; |
| 115 | |
| 116 | println!( |
| 117 | " Using game prefix: {}", |
| 118 | style(compat_data.display()).dim() |
| 119 | ); |
| 120 | |
| 121 | // Symlink WeMod into the game's prefix |
| 122 | let wemod_src = prefix.wemod_path(); |
| 123 | let target_local = compat_data.join("pfx/drive_c/users/steamuser/AppData/Local"); |
| 124 | let _ = std::fs::create_dir_all(&target_local); |
| 125 | let wemod_dir_name = wemod_src.file_name().unwrap_or_default(); |
| 126 | let link_path = target_local.join(wemod_dir_name); |
| 127 | if !link_path.exists() { |
| 128 | let _ = std::os::unix::fs::symlink(&wemod_src, &link_path); |
| 129 | } |
| 130 | |
| 131 | // Symlink WeMod roaming data |
| 132 | let roaming_src = prefix.path.join("pfx/drive_c/users/steamuser/AppData/Roaming/WeMod"); |
| 133 | if roaming_src.exists() { |
| 134 | let target_roaming = compat_data.join("pfx/drive_c/users/steamuser/AppData/Roaming"); |
| 135 | let _ = std::fs::create_dir_all(&target_roaming); |
| 136 | let roaming_link = target_roaming.join("WeMod"); |
| 137 | if !roaming_link.exists() { |
| 138 | let _ = std::os::unix::fs::symlink(&roaming_src, &roaming_link); |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | // Patch mscorlib in the game's prefix |
| 143 | // (done by the launcher's setup_steam_library, but mscorlib |
| 144 | // needs manual patching here) |
| 145 | |
| 146 | // Create a WandaPrefix pointing to the game's compat data |
| 147 | game_prefix = wanda_core::prefix::WandaPrefix { |
| 148 | name: "game".to_string(), |
| 149 | path: compat_data.clone(), |
| 150 | wemod_installed: true, |
| 151 | wemod_version: prefix.wemod_version.clone(), |
| 152 | proton_version: prefix.proton_version.clone(), |
| 153 | created_at: None, |
| 154 | last_used: None, |
| 155 | }; |
| 156 | &game_prefix |
| 157 | } else { |
| 158 | prefix |
| 159 | }; |
| 160 | |
| 161 | // Get Proton version |
| 162 | let proton_manager = ProtonManager::discover(&steam, &config)?; |
| 163 | let proton = if let Some(ref name) = args.proton { |
| 164 | proton_manager.find_by_name(name).ok_or_else(|| { |
| 165 | eprintln!("Available Proton versions:"); |
| 166 | for v in &proton_manager.versions { |
| 167 | eprintln!(" - {} ({})", v.name, v.compatibility); |
| 168 | } |
| 169 | WandaError::LaunchFailed { |
| 170 | reason: format!("Proton version '{}' not found", name), |
| 171 | } |
| 172 | })? |
| 173 | } else { |
| 174 | proton_manager.get_preferred(&config)? |
| 175 | }; |
| 176 | |
| 177 | println!(" Using WeMod with {}", proton.name); |
| 178 | |
| 179 | // Create launcher |
| 180 | let launcher = GameLauncher::new(&steam, launch_prefix, proton); |
| 181 | |
| 182 | let standalone = args.standalone; |
| 183 | let launch_config = LaunchConfig { |
| 184 | app_id: game.app_id, |
| 185 | with_wemod: true, |
| 186 | standalone, |
| 187 | wemod_delay: args.delay, |
| 188 | extra_args: args |
| 189 | .args |
| 190 | .map(|a| a.split_whitespace().map(String::from).collect()) |
| 191 | .unwrap_or_default(), |
| 192 | ..Default::default() |
| 193 | }; |
| 194 | |
| 195 | let mut handle = launcher.launch(launch_config).await?; |
| 196 | |
| 197 | if standalone { |
| 198 | println!( |
| 199 | "\n{} WeMod launched in standalone mode!", |
| 200 | style("SUCCESS").green().bold() |
| 201 | ); |
| 202 | println!("\nWeMod is running in the game's prefix."); |
| 203 | println!("Find your game in WeMod and click Play."); |
| 204 | println!("WeMod will launch the game directly.\n"); |
| 205 | } else { |
| 206 | println!( |
| 207 | "\n{} Game launched with WeMod!", |
| 208 | style("SUCCESS").green().bold() |
| 209 | ); |
| 210 | println!("\nWeMod should appear in a separate window."); |
| 211 | println!("Select your game in WeMod and click Play to activate trainers.\n"); |
| 212 | } |
| 213 | |
| 214 | if args.wait { |
| 215 | println!("Waiting for session to end..."); |
| 216 | handle.wait().await?; |
| 217 | println!( |
| 218 | "Session ended. Play time: {:?}", |
| 219 | handle.elapsed() |
| 220 | ); |
| 221 | } |
| 222 | } else { |
| 223 | // Launch without WeMod (just use Steam) |
| 224 | println!(" Launching via Steam (without WeMod)..."); |
| 225 | |
| 226 | let steam_url = format!("steam://rungameid/{}", game.app_id); |
| 227 | let status = tokio::process::Command::new("xdg-open") |
| 228 | .arg(&steam_url) |
| 229 | .status() |
| 230 | .await |
| 231 | .map_err(|e| WandaError::LaunchFailed { |
| 232 | reason: format!("Failed to launch: {}", e), |
| 233 | })?; |
| 234 | |
| 235 | if status.success() { |
| 236 | println!("\n{} Game launched!", style("SUCCESS").green().bold()); |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | Ok(()) |
| 241 | } |
| 242 |