zeroed-some/wanda / 03d6277

Browse files

launch games via proton in wanda prefix

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
03d6277cafe24a14dbe066cff4d3bb04b6d4f74b
Parents
36648f4
Tree
c093aef

4 changed files

StatusFile+-
M crates/wanda-core/src/launcher.rs 49 122
M crates/wanda-core/src/prefix/builder.rs 5 2
M crates/wanda-core/src/steam/proton.rs 24 7
M crates/wanda-core/src/wemod/installer.rs 3 7
crates/wanda-core/src/launcher.rsmodified
@@ -1,6 +1,10 @@
11
 //! Game launching with WeMod
22
 //!
33
 //! Handles launching Steam games alongside WeMod through Proton.
4
+//!
5
+//! Key insight: WeMod and the game MUST run in the same Wine prefix
6
+//! for WeMod to be able to hook into the game process. We achieve this by
7
+//! running both from wanda's prefix (which has .NET and WeMod installed).
48
 
59
 use crate::error::{Result, WandaError};
610
 use crate::prefix::WandaPrefix;
@@ -10,7 +14,7 @@ use std::path::PathBuf;
1014
 use std::process::Stdio;
1115
 use std::time::Duration;
1216
 use tokio::process::{Child, Command};
13
-use tracing::{debug, info, warn};
17
+use tracing::info;
1418
 
1519
 /// Configuration for launching a game
1620
 #[derive(Debug, Clone)]
@@ -54,8 +58,8 @@ impl LaunchHandle {
5458
     pub fn is_running(&mut self) -> bool {
5559
         if let Some(ref mut wemod) = self.wemod_process {
5660
             match wemod.try_wait() {
57
-                Ok(Some(_)) => return false, // WeMod exited
58
-                Ok(None) => return true,     // Still running
61
+                Ok(Some(_)) => return false,
62
+                Ok(None) => return true,
5963
                 Err(_) => return false,
6064
             }
6165
         }
@@ -86,11 +90,8 @@ impl LaunchHandle {
8690
 
8791
 /// Game launcher that coordinates WeMod and game startup
8892
 pub struct GameLauncher<'a> {
89
-    /// Steam installation
9093
     steam: &'a SteamInstallation,
91
-    /// WANDA prefix with WeMod
9294
     prefix: &'a WandaPrefix,
93
-    /// Proton version to use
9495
     proton: &'a ProtonVersion,
9596
 }
9697
 
@@ -101,42 +102,27 @@ impl<'a> GameLauncher<'a> {
101102
         prefix: &'a WandaPrefix,
102103
         proton: &'a ProtonVersion,
103104
     ) -> Self {
104
-        Self {
105
-            steam,
106
-            prefix,
107
-            proton,
108
-        }
105
+        Self { steam, prefix, proton }
109106
     }
110107
 
111108
     /// Build environment variables for launching
112
-    fn build_env(&self, game: &SteamApp) -> HashMap<String, String> {
109
+    fn build_env(&self) -> HashMap<String, String> {
113110
         let mut env = HashMap::new();
114111
 
115
-        // Wine prefix (WANDA's prefix for WeMod)
116112
         env.insert(
117113
             "WINEPREFIX".to_string(),
114
+            self.prefix.pfx_path().to_string_lossy().to_string(),
115
+        );
116
+        env.insert(
117
+            "STEAM_COMPAT_DATA_PATH".to_string(),
118118
             self.prefix.path.to_string_lossy().to_string(),
119119
         );
120
-
121
-        // Steam compatibility data path (for the game's own prefix if needed)
122
-        if let Some(ref compat_path) = game.compat_data_path {
123
-            env.insert(
124
-                "STEAM_COMPAT_DATA_PATH".to_string(),
125
-                compat_path.to_string_lossy().to_string(),
126
-            );
127
-        }
128
-
129
-        // Steam client install path
130120
         env.insert(
131121
             "STEAM_COMPAT_CLIENT_INSTALL_PATH".to_string(),
132122
             self.steam.root_path.to_string_lossy().to_string(),
133123
         );
134
-
135
-        // Proton flags that may help stability
136124
         env.insert("PROTON_NO_ESYNC".to_string(), "1".to_string());
137125
         env.insert("PROTON_NO_FSYNC".to_string(), "1".to_string());
138
-
139
-        // Reduce Wine debug noise
140126
         env.insert("WINEDEBUG".to_string(), "-all".to_string());
141127
 
142128
         env
@@ -157,28 +143,30 @@ impl<'a> GameLauncher<'a> {
157143
         let game = self
158144
             .steam
159145
             .find_game(config.app_id)
160
-            .ok_or(WandaError::GameNotFound {
161
-                app_id: config.app_id,
162
-            })?;
146
+            .ok_or(WandaError::GameNotFound { app_id: config.app_id })?;
163147
 
164148
         info!(
165149
             "Launching {} (AppID: {}) {}",
166150
             game.name,
167151
             game.app_id,
168
-            if config.with_wemod {
169
-                "with WeMod"
170
-            } else {
171
-                "without WeMod"
172
-            }
152
+            if config.with_wemod { "with WeMod" } else { "without WeMod" }
173153
         );
174154
 
175
-        let mut env = self.build_env(game);
155
+        info!("Using WANDA prefix: {}", self.prefix.path.display());
156
+
157
+        // Kill stale wineservers to avoid conflicts
158
+        info!("Cleaning up stale wineservers...");
159
+        let _ = Command::new("pkill").args(["-9", "wineserver"]).output().await;
160
+        tokio::time::sleep(Duration::from_secs(1)).await;
161
+
162
+        let mut env = self.build_env();
163
+        env.insert("SteamAppId".to_string(), config.app_id.to_string());
164
+        env.insert("SteamGameId".to_string(), config.app_id.to_string());
176165
         env.extend(config.extra_env);
177166
 
178167
         let wine = self.wine_exe();
179168
         let mut wemod_process = None;
180169
 
181
-        // Start WeMod first if requested
182170
         if config.with_wemod {
183171
             let wemod_exe = self.prefix.wemod_exe();
184172
             if !wemod_exe.exists() {
@@ -186,11 +174,14 @@ impl<'a> GameLauncher<'a> {
186174
             }
187175
 
188176
             info!("Starting WeMod...");
177
+            info!("WeMod path: {}", wemod_exe.display());
178
+
189179
             let child = Command::new(&wine)
190180
                 .arg(&wemod_exe)
181
+                .arg("--no-sandbox")
191182
                 .envs(&env)
192
-                .stdout(Stdio::null())
193
-                .stderr(Stdio::null())
183
+                .stdout(Stdio::inherit())
184
+                .stderr(Stdio::inherit())
194185
                 .spawn()
195186
                 .map_err(|e| WandaError::LaunchFailed {
196187
                     reason: format!("Failed to start WeMod: {}", e),
@@ -198,18 +189,12 @@ impl<'a> GameLauncher<'a> {
198189
 
199190
             wemod_process = Some(child);
200191
 
201
-            // Wait for WeMod to initialize
202
-            info!(
203
-                "Waiting {} seconds for WeMod to initialize...",
204
-                config.wemod_delay
205
-            );
192
+            info!("Waiting {} seconds for WeMod to initialize...", config.wemod_delay);
206193
             tokio::time::sleep(Duration::from_secs(config.wemod_delay)).await;
207194
         }
208195
 
209
-        // Launch the game via Steam
210
-        // Using steam:// URL protocol ensures Steam handles Proton setup correctly
211
-        info!("Launching game via Steam...");
212
-        self.launch_via_steam(config.app_id).await?;
196
+        info!("Launching game via Proton...");
197
+        self.launch_game_via_proton(&game, &config.extra_args, &env).await?;
213198
 
214199
         Ok(LaunchHandle {
215200
             wemod_process,
@@ -218,97 +203,42 @@ impl<'a> GameLauncher<'a> {
218203
         })
219204
     }
220205
 
221
-    /// Launch a game via Steam's URL protocol
222
-    async fn launch_via_steam(&self, app_id: u32) -> Result<()> {
223
-        // Use xdg-open to launch via steam:// protocol
224
-        // This ensures Steam handles all the Proton setup correctly
225
-        let steam_url = format!("steam://rungameid/{}", app_id);
226
-
227
-        let status = Command::new("xdg-open")
228
-            .arg(&steam_url)
229
-            .stdout(Stdio::null())
230
-            .stderr(Stdio::null())
231
-            .status()
232
-            .await
233
-            .map_err(|e| WandaError::LaunchFailed {
234
-                reason: format!("Failed to launch Steam URL: {}", e),
235
-            })?;
236
-
237
-        if !status.success() {
238
-            // Try alternative: steam command directly
239
-            let status = Command::new("steam")
240
-                .arg(&steam_url)
241
-                .stdout(Stdio::null())
242
-                .stderr(Stdio::null())
243
-                .status()
244
-                .await
245
-                .map_err(|e| WandaError::LaunchFailed {
246
-                    reason: format!("Failed to launch via steam command: {}", e),
247
-                })?;
248
-
249
-            if !status.success() {
250
-                warn!("Steam launch returned non-zero, game may still start");
251
-            }
252
-        }
253
-
254
-        debug!("Game launch initiated via Steam");
255
-        Ok(())
256
-    }
257
-
258
-    /// Launch a game directly via Proton (without Steam)
259
-    /// This is an alternative method that gives more control
260
-    #[allow(dead_code)]
261
-    async fn launch_directly(&self, game: &SteamApp, args: &[String]) -> Result<Child> {
206
+    /// Launch game via Proton
207
+    async fn launch_game_via_proton(
208
+        &self,
209
+        game: &SteamApp,
210
+        args: &[String],
211
+        env: &HashMap<String, String>,
212
+    ) -> Result<()> {
262213
         let proton_exe = self.proton.proton_exe();
263
-
264214
         if !proton_exe.exists() {
265215
             return Err(WandaError::ProtonNotFound);
266216
         }
267217
 
268
-        // Find the game executable
269218
         let game_exe = self.find_game_executable(game)?;
270
-
271
-        let mut env = self.build_env(game);
272
-
273
-        // Set up Proton environment
274
-        env.insert("STEAM_COMPAT_DATA_PATH".to_string(),
275
-            game.compat_data_path
276
-                .as_ref()
277
-                .map(|p| p.to_string_lossy().to_string())
278
-                .unwrap_or_else(|| {
279
-                    self.steam.root_path
280
-                        .join("steamapps/compatdata")
281
-                        .join(game.app_id.to_string())
282
-                        .to_string_lossy()
283
-                        .to_string()
284
-                })
285
-        );
219
+        info!("Game executable: {}", game_exe.display());
286220
 
287221
         let mut cmd = Command::new(&proton_exe);
288
-        cmd.arg("run").arg(&game_exe);
289
-        cmd.args(args);
290
-        cmd.envs(&env);
291
-        cmd.stdout(Stdio::null());
292
-        cmd.stderr(Stdio::null());
222
+        cmd.arg("run").arg(&game_exe).args(args).envs(env);
223
+        cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
224
+
225
+        info!("Running: {} run {}", proton_exe.display(), game_exe.display());
293226
 
294
-        let child = cmd.spawn().map_err(|e| WandaError::LaunchFailed {
227
+        cmd.spawn().map_err(|e| WandaError::LaunchFailed {
295228
             reason: format!("Failed to start game: {}", e),
296229
         })?;
297230
 
298
-        Ok(child)
231
+        Ok(())
299232
     }
300233
 
301
-    /// Try to find the main executable for a game
234
+    /// Find the main executable for a game
302235
     fn find_game_executable(&self, game: &SteamApp) -> Result<PathBuf> {
303236
         let install_path = &game.install_path;
304237
 
305238
         if !install_path.exists() {
306
-            return Err(WandaError::GameNotFound {
307
-                app_id: game.app_id,
308
-            });
239
+            return Err(WandaError::GameNotFound { app_id: game.app_id });
309240
         }
310241
 
311
-        // Common executable patterns
312242
         let patterns = [
313243
             format!("{}.exe", game.install_dir),
314244
             "game.exe".to_string(),
@@ -316,7 +246,6 @@ impl<'a> GameLauncher<'a> {
316246
             "launcher.exe".to_string(),
317247
         ];
318248
 
319
-        // Try to find a matching executable
320249
         for pattern in &patterns {
321250
             let exe_path = install_path.join(pattern);
322251
             if exe_path.exists() {
@@ -324,7 +253,6 @@ impl<'a> GameLauncher<'a> {
324253
             }
325254
         }
326255
 
327
-        // Walk the directory looking for .exe files
328256
         for entry in walkdir::WalkDir::new(install_path)
329257
             .max_depth(2)
330258
             .into_iter()
@@ -334,7 +262,6 @@ impl<'a> GameLauncher<'a> {
334262
             if let Some(ext) = path.extension() {
335263
                 if ext == "exe" {
336264
                     let name = path.file_name().unwrap_or_default().to_string_lossy();
337
-                    // Skip common non-game executables
338265
                     if !name.to_lowercase().contains("unins")
339266
                         && !name.to_lowercase().contains("redist")
340267
                         && !name.to_lowercase().contains("setup")
crates/wanda-core/src/prefix/builder.rsmodified
@@ -236,8 +236,11 @@ impl<'a> PrefixBuilder<'a> {
236236
     async fn install_dependencies(&self) -> Result<()> {
237237
         info!("Installing additional dependencies...");
238238
 
239
-        // Install common dependencies WeMod might need
240
-        let deps = ["vcrun2019", "corefonts"];
239
+        // Install common dependencies WeMod needs
240
+        // - vcrun2019: Visual C++ runtime
241
+        // - corefonts: Windows fonts
242
+        // - winhttp/wininet: Network components for WeMod authentication
243
+        let deps = ["vcrun2019", "corefonts", "winhttp", "wininet"];
241244
 
242245
         for dep in deps {
243246
             info!("Installing {}...", dep);
crates/wanda-core/src/steam/proton.rsmodified
@@ -208,21 +208,27 @@ impl ProtonManager {
208208
         is_ge: bool,
209209
         is_experimental: bool,
210210
     ) -> ProtonCompatibility {
211
-        // GE-Proton 8+ is recommended for WeMod
212
-        if is_ge && version.0 >= 8 {
211
+        // Proton Experimental is recommended for WeMod
212
+        // GE-Proton 10.x has wow64 mode issues that cause WeMod to crash
213
+        if is_experimental {
213214
             return ProtonCompatibility::Recommended;
214215
         }
215216
 
216
-        // GE-Proton 7.x should work
217
-        if is_ge && version.0 >= 7 {
217
+        // GE-Proton 9.x and below should work (before wow64 mode)
218
+        if is_ge && version.0 >= 8 && version.0 <= 9 {
218219
             return ProtonCompatibility::Supported;
219220
         }
220221
 
221
-        // Proton Experimental might work
222
-        if is_experimental {
222
+        // GE-Proton 10.x has known wow64 issues with WeMod
223
+        if is_ge && version.0 >= 10 {
223224
             return ProtonCompatibility::Experimental;
224225
         }
225226
 
227
+        // GE-Proton 7.x might work
228
+        if is_ge && version.0 >= 7 {
229
+            return ProtonCompatibility::Supported;
230
+        }
231
+
226232
         // Standard Proton 8+ should work
227233
         if version.0 >= 8 {
228234
             return ProtonCompatibility::Supported;
@@ -315,10 +321,21 @@ mod tests {
315321
 
316322
     #[test]
317323
     fn test_compatibility() {
324
+        // Proton Experimental is now recommended (GE-Proton 10.x has wow64 issues)
318325
         let compat =
319
-            ProtonManager::determine_compatibility("GE-Proton9-5", (9, 5, 0), true, false);
326
+            ProtonManager::determine_compatibility("Proton - Experimental", (0, 0, 0), false, true);
320327
         assert_eq!(compat, ProtonCompatibility::Recommended);
321328
 
329
+        // GE-Proton 9.x is supported (before wow64 issues)
330
+        let compat =
331
+            ProtonManager::determine_compatibility("GE-Proton9-5", (9, 5, 0), true, false);
332
+        assert_eq!(compat, ProtonCompatibility::Supported);
333
+
334
+        // GE-Proton 10.x is experimental (wow64 issues)
335
+        let compat =
336
+            ProtonManager::determine_compatibility("GE-Proton10-28", (10, 28, 0), true, false);
337
+        assert_eq!(compat, ProtonCompatibility::Experimental);
338
+
322339
         let compat =
323340
             ProtonManager::determine_compatibility("Proton 6.0", (6, 0, 0), false, false);
324341
         assert_eq!(compat, ProtonCompatibility::Unsupported);
crates/wanda-core/src/wemod/installer.rsmodified
@@ -320,15 +320,11 @@ impl<'a> WemodInstaller<'a> {
320320
         info!("Starting WeMod from: {}", wemod_exe.display());
321321
         info!("Using wine: {}", wine);
322322
 
323
-        // Run WeMod with required flags for Wine/Proton compatibility
324
-        // These Electron flags are necessary for proper rendering under Wine
323
+        // Run WeMod with minimal flags for Wine/Proton compatibility
324
+        // Only --no-sandbox is required; additional GPU flags can cause issues
325325
         let child = Command::new(&wine)
326326
             .arg(&wemod_exe)
327
-            .arg("--no-sandbox")              // Required for Wine
328
-            .arg("--disable-gpu")             // Disable GPU acceleration
329
-            .arg("--disable-gpu-compositing") // Disable GPU compositing
330
-            .arg("--disable-software-rasterizer") // Force hardware path (paradoxically helps)
331
-            .arg("--in-process-gpu")          // Run GPU in main process
327
+            .arg("--no-sandbox")  // Required for Electron under Wine
332328
             .envs(&env)
333329
             .stdout(Stdio::inherit())
334330
             .stderr(Stdio::inherit())