| 1 | //! wanda init - Initialize WANDA |
| 2 | |
| 3 | use clap::Args; |
| 4 | use indicatif::{ProgressBar, ProgressStyle}; |
| 5 | use std::path::PathBuf; |
| 6 | use wanda_core::{ |
| 7 | config::WandaConfig, |
| 8 | prefix::{PrefixBuilder, PrefixManager}, |
| 9 | steam::{ProtonCompatibility, ProtonManager, SteamInstallation}, |
| 10 | wemod::{WemodDownloader, WemodInstaller}, |
| 11 | Result, WandaError, |
| 12 | }; |
| 13 | |
| 14 | #[derive(Args)] |
| 15 | pub struct InitArgs { |
| 16 | /// Override Steam installation path |
| 17 | #[arg(long)] |
| 18 | steam_path: Option<PathBuf>, |
| 19 | |
| 20 | /// Specify Proton version to use |
| 21 | #[arg(long)] |
| 22 | proton: Option<String>, |
| 23 | |
| 24 | /// Skip WeMod installation |
| 25 | #[arg(long)] |
| 26 | skip_wemod: bool, |
| 27 | |
| 28 | /// Skip .NET Framework installation (WeMod needs it for trainers) |
| 29 | #[arg(long)] |
| 30 | skip_dotnet: bool, |
| 31 | |
| 32 | /// Force reinitialization even if already set up |
| 33 | #[arg(long, short)] |
| 34 | force: bool, |
| 35 | } |
| 36 | |
| 37 | pub async fn run(args: InitArgs, config_path: Option<PathBuf>) -> Result<()> { |
| 38 | println!("Initializing WANDA...\n"); |
| 39 | |
| 40 | // Load or create config |
| 41 | let mut config = match &config_path { |
| 42 | Some(path) => WandaConfig::load_from(path)?, |
| 43 | None => WandaConfig::load()?, |
| 44 | }; |
| 45 | |
| 46 | // Override steam path if provided |
| 47 | if let Some(ref path) = args.steam_path { |
| 48 | config.steam.install_path = Some(path.clone()); |
| 49 | } |
| 50 | |
| 51 | // Discover Steam |
| 52 | println!("Looking for Steam installation..."); |
| 53 | let steam = SteamInstallation::discover(&config)?; |
| 54 | println!( |
| 55 | " Found Steam at: {}{}", |
| 56 | steam.root_path.display(), |
| 57 | if steam.is_flatpak { " (Flatpak)" } else { "" } |
| 58 | ); |
| 59 | println!(" Libraries: {}", steam.libraries.len()); |
| 60 | |
| 61 | let total_games: usize = steam.libraries.iter().map(|l| l.apps.len()).sum(); |
| 62 | println!(" Games: {}", total_games); |
| 63 | |
| 64 | // Discover Proton |
| 65 | println!("\nLooking for Proton versions..."); |
| 66 | let proton_manager = ProtonManager::discover(&steam, &config)?; |
| 67 | |
| 68 | if proton_manager.versions.is_empty() { |
| 69 | return Err(WandaError::ProtonNotFound); |
| 70 | } |
| 71 | |
| 72 | // Select Proton version |
| 73 | // Priority: 1) --proton arg, 2) config preferred_version, 3) get_recommended() |
| 74 | // Experimental/Unsupported versions from config are auto-overridden unless --proton is explicit |
| 75 | let proton = if let Some(ref name) = args.proton { |
| 76 | let v = proton_manager.find_by_name(name).ok_or_else(|| { |
| 77 | eprintln!("Available Proton versions:"); |
| 78 | for v in &proton_manager.versions { |
| 79 | eprintln!(" - {} ({})", v.name, v.compatibility); |
| 80 | } |
| 81 | WandaError::ProtonNotFound |
| 82 | })?; |
| 83 | if matches!(v.compatibility, ProtonCompatibility::Experimental | ProtonCompatibility::Unsupported) { |
| 84 | eprintln!( |
| 85 | " Warning: '{}' is {} — this may cause WeMod to fail", |
| 86 | v.name, v.compatibility |
| 87 | ); |
| 88 | } |
| 89 | v |
| 90 | } else if let Some(ref preferred) = config.proton.preferred_version { |
| 91 | match proton_manager.find_by_name(preferred) { |
| 92 | Some(v) if matches!(v.compatibility, ProtonCompatibility::Experimental | ProtonCompatibility::Unsupported) => { |
| 93 | let recommended = proton_manager.get_recommended().ok_or(WandaError::ProtonNotFound)?; |
| 94 | eprintln!( |
| 95 | " Configured Proton '{}' is {} — auto-switching to '{}' ({})", |
| 96 | v.name, v.compatibility, recommended.name, recommended.compatibility |
| 97 | ); |
| 98 | recommended |
| 99 | } |
| 100 | Some(v) => v, |
| 101 | None => { |
| 102 | eprintln!(" Configured Proton '{}' not found, using recommended", preferred); |
| 103 | proton_manager.get_recommended().ok_or(WandaError::ProtonNotFound)? |
| 104 | } |
| 105 | } |
| 106 | } else { |
| 107 | proton_manager.get_recommended().ok_or(WandaError::ProtonNotFound)? |
| 108 | }; |
| 109 | |
| 110 | println!(" Using: {} ({})", proton.name, proton.compatibility); |
| 111 | |
| 112 | // Set up prefix |
| 113 | println!("\nSetting up Wine prefix..."); |
| 114 | let mut prefix_manager = PrefixManager::new(&config); |
| 115 | prefix_manager.load()?; |
| 116 | |
| 117 | let existing = prefix_manager.get("default"); |
| 118 | if existing.is_some() && !args.force { |
| 119 | println!(" Prefix already exists. Use --force to reinitialize."); |
| 120 | } else { |
| 121 | if existing.is_some() && args.force { |
| 122 | println!(" Removing existing prefix..."); |
| 123 | prefix_manager.delete("default")?; |
| 124 | } |
| 125 | |
| 126 | println!(" Creating prefix (this may take several minutes)..."); |
| 127 | let pb = ProgressBar::new_spinner(); |
| 128 | pb.set_style( |
| 129 | ProgressStyle::default_spinner() |
| 130 | .template("{spinner:.green} {msg}") |
| 131 | .unwrap(), |
| 132 | ); |
| 133 | pb.set_message("Initializing Wine prefix..."); |
| 134 | pb.enable_steady_tick(std::time::Duration::from_millis(100)); |
| 135 | |
| 136 | prefix_manager.create("default", proton).await?; |
| 137 | |
| 138 | pb.finish_with_message("Prefix created successfully"); |
| 139 | } |
| 140 | |
| 141 | // Install .NET Framework 4.8 (required by WeMod's trainer engine) |
| 142 | if !args.skip_dotnet { |
| 143 | println!("\nInstalling .NET Framework 4.8..."); |
| 144 | println!(" This may take 10-15 minutes. Use --skip-dotnet to skip."); |
| 145 | |
| 146 | let prefix_path = prefix_manager.base_path.join("default"); |
| 147 | let dotnet_builder = PrefixBuilder::new(&prefix_path, proton); |
| 148 | |
| 149 | let pb = ProgressBar::new_spinner(); |
| 150 | pb.set_style( |
| 151 | ProgressStyle::default_spinner() |
| 152 | .template("{spinner:.green} {msg}") |
| 153 | .unwrap(), |
| 154 | ); |
| 155 | pb.set_message("Installing .NET Framework 4.8 via winetricks..."); |
| 156 | pb.enable_steady_tick(std::time::Duration::from_millis(100)); |
| 157 | |
| 158 | match dotnet_builder.install_dotnet().await { |
| 159 | Ok(_) => { |
| 160 | pb.finish_with_message(".NET Framework installed successfully"); |
| 161 | } |
| 162 | Err(e) => { |
| 163 | pb.finish_with_message(".NET Framework installation failed"); |
| 164 | eprintln!(" Warning: .NET install failed: {}", e); |
| 165 | eprintln!(" WeMod may show a '.NET framework' error on startup."); |
| 166 | eprintln!(" You can retry manually: winetricks -q dotnet48"); |
| 167 | } |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | // Reload prefix after creation |
| 172 | prefix_manager.load()?; |
| 173 | let prefix = prefix_manager.get("default").unwrap(); |
| 174 | |
| 175 | // Install WeMod |
| 176 | if !args.skip_wemod { |
| 177 | println!("\nInstalling WeMod..."); |
| 178 | |
| 179 | let downloader = WemodDownloader::new(&config); |
| 180 | |
| 181 | // Get latest release info |
| 182 | let release = downloader.get_latest().await?; |
| 183 | if let Some(ref version) = release.version { |
| 184 | println!(" Latest version: {}", version); |
| 185 | } |
| 186 | |
| 187 | // Download with progress |
| 188 | let pb = ProgressBar::new(release.size.unwrap_or(0)); |
| 189 | pb.set_style( |
| 190 | ProgressStyle::default_bar() |
| 191 | .template("{spinner:.green} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})") |
| 192 | .unwrap() |
| 193 | .progress_chars("#>-"), |
| 194 | ); |
| 195 | |
| 196 | let installer_path = downloader |
| 197 | .download(&release, |downloaded, total| { |
| 198 | pb.set_length(total); |
| 199 | pb.set_position(downloaded); |
| 200 | }) |
| 201 | .await?; |
| 202 | |
| 203 | pb.finish_with_message("Download complete"); |
| 204 | |
| 205 | // Install |
| 206 | println!(" Installing WeMod..."); |
| 207 | let installer = WemodInstaller::new(prefix, proton); |
| 208 | installer.install(&installer_path).await?; |
| 209 | |
| 210 | // Update prefix metadata |
| 211 | let version = release.version.clone(); |
| 212 | prefix_manager.update_metadata("default", |p| { |
| 213 | p.wemod_installed = true; |
| 214 | p.wemod_version = version; |
| 215 | })?; |
| 216 | |
| 217 | println!(" WeMod installed successfully"); |
| 218 | } |
| 219 | |
| 220 | // Save config |
| 221 | config.proton.preferred_version = Some(proton.name.clone()); |
| 222 | match &config_path { |
| 223 | Some(path) => config.save_to(path)?, |
| 224 | None => config.save()?, |
| 225 | } |
| 226 | |
| 227 | println!("\nWANDA initialization complete!"); |
| 228 | println!("\nNext steps:"); |
| 229 | println!(" wanda scan - View available games"); |
| 230 | println!(" wanda launch <game> - Launch a game with WeMod"); |
| 231 | println!(" wanda doctor - Check for issues"); |
| 232 | |
| 233 | Ok(()) |
| 234 | } |
| 235 |