Rust · 8305 bytes Raw Blame History
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