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 @@
1
 //! Game launching with WeMod
1
 //! Game launching with WeMod
2
 //!
2
 //!
3
 //! Handles launching Steam games alongside WeMod through Proton.
3
 //! 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).
4
 
8
 
5
 use crate::error::{Result, WandaError};
9
 use crate::error::{Result, WandaError};
6
 use crate::prefix::WandaPrefix;
10
 use crate::prefix::WandaPrefix;
@@ -10,7 +14,7 @@ use std::path::PathBuf;
10
 use std::process::Stdio;
14
 use std::process::Stdio;
11
 use std::time::Duration;
15
 use std::time::Duration;
12
 use tokio::process::{Child, Command};
16
 use tokio::process::{Child, Command};
13
-use tracing::{debug, info, warn};
17
+use tracing::info;
14
 
18
 
15
 /// Configuration for launching a game
19
 /// Configuration for launching a game
16
 #[derive(Debug, Clone)]
20
 #[derive(Debug, Clone)]
@@ -54,8 +58,8 @@ impl LaunchHandle {
54
     pub fn is_running(&mut self) -> bool {
58
     pub fn is_running(&mut self) -> bool {
55
         if let Some(ref mut wemod) = self.wemod_process {
59
         if let Some(ref mut wemod) = self.wemod_process {
56
             match wemod.try_wait() {
60
             match wemod.try_wait() {
57
-                Ok(Some(_)) => return false, // WeMod exited
61
+                Ok(Some(_)) => return false,
58
-                Ok(None) => return true,     // Still running
62
+                Ok(None) => return true,
59
                 Err(_) => return false,
63
                 Err(_) => return false,
60
             }
64
             }
61
         }
65
         }
@@ -86,11 +90,8 @@ impl LaunchHandle {
86
 
90
 
87
 /// Game launcher that coordinates WeMod and game startup
91
 /// Game launcher that coordinates WeMod and game startup
88
 pub struct GameLauncher<'a> {
92
 pub struct GameLauncher<'a> {
89
-    /// Steam installation
90
     steam: &'a SteamInstallation,
93
     steam: &'a SteamInstallation,
91
-    /// WANDA prefix with WeMod
92
     prefix: &'a WandaPrefix,
94
     prefix: &'a WandaPrefix,
93
-    /// Proton version to use
94
     proton: &'a ProtonVersion,
95
     proton: &'a ProtonVersion,
95
 }
96
 }
96
 
97
 
@@ -101,42 +102,27 @@ impl<'a> GameLauncher<'a> {
101
         prefix: &'a WandaPrefix,
102
         prefix: &'a WandaPrefix,
102
         proton: &'a ProtonVersion,
103
         proton: &'a ProtonVersion,
103
     ) -> Self {
104
     ) -> Self {
104
-        Self {
105
+        Self { steam, prefix, proton }
105
-            steam,
106
-            prefix,
107
-            proton,
108
-        }
109
     }
106
     }
110
 
107
 
111
     /// Build environment variables for launching
108
     /// Build environment variables for launching
112
-    fn build_env(&self, game: &SteamApp) -> HashMap<String, String> {
109
+    fn build_env(&self) -> HashMap<String, String> {
113
         let mut env = HashMap::new();
110
         let mut env = HashMap::new();
114
 
111
 
115
-        // Wine prefix (WANDA's prefix for WeMod)
116
         env.insert(
112
         env.insert(
117
             "WINEPREFIX".to_string(),
113
             "WINEPREFIX".to_string(),
118
-            self.prefix.path.to_string_lossy().to_string(),
114
+            self.prefix.pfx_path().to_string_lossy().to_string(),
119
         );
115
         );
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(
116
         env.insert(
124
             "STEAM_COMPAT_DATA_PATH".to_string(),
117
             "STEAM_COMPAT_DATA_PATH".to_string(),
125
-                compat_path.to_string_lossy().to_string(),
118
+            self.prefix.path.to_string_lossy().to_string(),
126
         );
119
         );
127
-        }
128
-
129
-        // Steam client install path
130
         env.insert(
120
         env.insert(
131
             "STEAM_COMPAT_CLIENT_INSTALL_PATH".to_string(),
121
             "STEAM_COMPAT_CLIENT_INSTALL_PATH".to_string(),
132
             self.steam.root_path.to_string_lossy().to_string(),
122
             self.steam.root_path.to_string_lossy().to_string(),
133
         );
123
         );
134
-
135
-        // Proton flags that may help stability
136
         env.insert("PROTON_NO_ESYNC".to_string(), "1".to_string());
124
         env.insert("PROTON_NO_ESYNC".to_string(), "1".to_string());
137
         env.insert("PROTON_NO_FSYNC".to_string(), "1".to_string());
125
         env.insert("PROTON_NO_FSYNC".to_string(), "1".to_string());
138
-
139
-        // Reduce Wine debug noise
140
         env.insert("WINEDEBUG".to_string(), "-all".to_string());
126
         env.insert("WINEDEBUG".to_string(), "-all".to_string());
141
 
127
 
142
         env
128
         env
@@ -157,28 +143,30 @@ impl<'a> GameLauncher<'a> {
157
         let game = self
143
         let game = self
158
             .steam
144
             .steam
159
             .find_game(config.app_id)
145
             .find_game(config.app_id)
160
-            .ok_or(WandaError::GameNotFound {
146
+            .ok_or(WandaError::GameNotFound { app_id: config.app_id })?;
161
-                app_id: config.app_id,
162
-            })?;
163
 
147
 
164
         info!(
148
         info!(
165
             "Launching {} (AppID: {}) {}",
149
             "Launching {} (AppID: {}) {}",
166
             game.name,
150
             game.name,
167
             game.app_id,
151
             game.app_id,
168
-            if config.with_wemod {
152
+            if config.with_wemod { "with WeMod" } else { "without WeMod" }
169
-                "with WeMod"
170
-            } else {
171
-                "without WeMod"
172
-            }
173
         );
153
         );
174
 
154
 
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());
176
         env.extend(config.extra_env);
165
         env.extend(config.extra_env);
177
 
166
 
178
         let wine = self.wine_exe();
167
         let wine = self.wine_exe();
179
         let mut wemod_process = None;
168
         let mut wemod_process = None;
180
 
169
 
181
-        // Start WeMod first if requested
182
         if config.with_wemod {
170
         if config.with_wemod {
183
             let wemod_exe = self.prefix.wemod_exe();
171
             let wemod_exe = self.prefix.wemod_exe();
184
             if !wemod_exe.exists() {
172
             if !wemod_exe.exists() {
@@ -186,11 +174,14 @@ impl<'a> GameLauncher<'a> {
186
             }
174
             }
187
 
175
 
188
             info!("Starting WeMod...");
176
             info!("Starting WeMod...");
177
+            info!("WeMod path: {}", wemod_exe.display());
178
+
189
             let child = Command::new(&wine)
179
             let child = Command::new(&wine)
190
                 .arg(&wemod_exe)
180
                 .arg(&wemod_exe)
181
+                .arg("--no-sandbox")
191
                 .envs(&env)
182
                 .envs(&env)
192
-                .stdout(Stdio::null())
183
+                .stdout(Stdio::inherit())
193
-                .stderr(Stdio::null())
184
+                .stderr(Stdio::inherit())
194
                 .spawn()
185
                 .spawn()
195
                 .map_err(|e| WandaError::LaunchFailed {
186
                 .map_err(|e| WandaError::LaunchFailed {
196
                     reason: format!("Failed to start WeMod: {}", e),
187
                     reason: format!("Failed to start WeMod: {}", e),
@@ -198,18 +189,12 @@ impl<'a> GameLauncher<'a> {
198
 
189
 
199
             wemod_process = Some(child);
190
             wemod_process = Some(child);
200
 
191
 
201
-            // Wait for WeMod to initialize
192
+            info!("Waiting {} seconds for WeMod to initialize...", config.wemod_delay);
202
-            info!(
203
-                "Waiting {} seconds for WeMod to initialize...",
204
-                config.wemod_delay
205
-            );
206
             tokio::time::sleep(Duration::from_secs(config.wemod_delay)).await;
193
             tokio::time::sleep(Duration::from_secs(config.wemod_delay)).await;
207
         }
194
         }
208
 
195
 
209
-        // Launch the game via Steam
196
+        info!("Launching game via Proton...");
210
-        // Using steam:// URL protocol ensures Steam handles Proton setup correctly
197
+        self.launch_game_via_proton(&game, &config.extra_args, &env).await?;
211
-        info!("Launching game via Steam...");
212
-        self.launch_via_steam(config.app_id).await?;
213
 
198
 
214
         Ok(LaunchHandle {
199
         Ok(LaunchHandle {
215
             wemod_process,
200
             wemod_process,
@@ -218,97 +203,42 @@ impl<'a> GameLauncher<'a> {
218
         })
203
         })
219
     }
204
     }
220
 
205
 
221
-    /// Launch a game via Steam's URL protocol
206
+    /// Launch game via Proton
222
-    async fn launch_via_steam(&self, app_id: u32) -> Result<()> {
207
+    async fn launch_game_via_proton(
223
-        // Use xdg-open to launch via steam:// protocol
208
+        &self,
224
-        // This ensures Steam handles all the Proton setup correctly
209
+        game: &SteamApp,
225
-        let steam_url = format!("steam://rungameid/{}", app_id);
210
+        args: &[String],
226
-
211
+        env: &HashMap<String, String>,
227
-        let status = Command::new("xdg-open")
212
+    ) -> Result<()> {
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> {
262
         let proton_exe = self.proton.proton_exe();
213
         let proton_exe = self.proton.proton_exe();
263
-
264
         if !proton_exe.exists() {
214
         if !proton_exe.exists() {
265
             return Err(WandaError::ProtonNotFound);
215
             return Err(WandaError::ProtonNotFound);
266
         }
216
         }
267
 
217
 
268
-        // Find the game executable
269
         let game_exe = self.find_game_executable(game)?;
218
         let game_exe = self.find_game_executable(game)?;
270
-
219
+        info!("Game executable: {}", game_exe.display());
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
-        );
286
 
220
 
287
         let mut cmd = Command::new(&proton_exe);
221
         let mut cmd = Command::new(&proton_exe);
288
-        cmd.arg("run").arg(&game_exe);
222
+        cmd.arg("run").arg(&game_exe).args(args).envs(env);
289
-        cmd.args(args);
223
+        cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
290
-        cmd.envs(&env);
291
-        cmd.stdout(Stdio::null());
292
-        cmd.stderr(Stdio::null());
293
 
224
 
294
-        let child = cmd.spawn().map_err(|e| WandaError::LaunchFailed {
225
+        info!("Running: {} run {}", proton_exe.display(), game_exe.display());
226
+
227
+        cmd.spawn().map_err(|e| WandaError::LaunchFailed {
295
             reason: format!("Failed to start game: {}", e),
228
             reason: format!("Failed to start game: {}", e),
296
         })?;
229
         })?;
297
 
230
 
298
-        Ok(child)
231
+        Ok(())
299
     }
232
     }
300
 
233
 
301
-    /// Try to find the main executable for a game
234
+    /// Find the main executable for a game
302
     fn find_game_executable(&self, game: &SteamApp) -> Result<PathBuf> {
235
     fn find_game_executable(&self, game: &SteamApp) -> Result<PathBuf> {
303
         let install_path = &game.install_path;
236
         let install_path = &game.install_path;
304
 
237
 
305
         if !install_path.exists() {
238
         if !install_path.exists() {
306
-            return Err(WandaError::GameNotFound {
239
+            return Err(WandaError::GameNotFound { app_id: game.app_id });
307
-                app_id: game.app_id,
308
-            });
309
         }
240
         }
310
 
241
 
311
-        // Common executable patterns
312
         let patterns = [
242
         let patterns = [
313
             format!("{}.exe", game.install_dir),
243
             format!("{}.exe", game.install_dir),
314
             "game.exe".to_string(),
244
             "game.exe".to_string(),
@@ -316,7 +246,6 @@ impl<'a> GameLauncher<'a> {
316
             "launcher.exe".to_string(),
246
             "launcher.exe".to_string(),
317
         ];
247
         ];
318
 
248
 
319
-        // Try to find a matching executable
320
         for pattern in &patterns {
249
         for pattern in &patterns {
321
             let exe_path = install_path.join(pattern);
250
             let exe_path = install_path.join(pattern);
322
             if exe_path.exists() {
251
             if exe_path.exists() {
@@ -324,7 +253,6 @@ impl<'a> GameLauncher<'a> {
324
             }
253
             }
325
         }
254
         }
326
 
255
 
327
-        // Walk the directory looking for .exe files
328
         for entry in walkdir::WalkDir::new(install_path)
256
         for entry in walkdir::WalkDir::new(install_path)
329
             .max_depth(2)
257
             .max_depth(2)
330
             .into_iter()
258
             .into_iter()
@@ -334,7 +262,6 @@ impl<'a> GameLauncher<'a> {
334
             if let Some(ext) = path.extension() {
262
             if let Some(ext) = path.extension() {
335
                 if ext == "exe" {
263
                 if ext == "exe" {
336
                     let name = path.file_name().unwrap_or_default().to_string_lossy();
264
                     let name = path.file_name().unwrap_or_default().to_string_lossy();
337
-                    // Skip common non-game executables
338
                     if !name.to_lowercase().contains("unins")
265
                     if !name.to_lowercase().contains("unins")
339
                         && !name.to_lowercase().contains("redist")
266
                         && !name.to_lowercase().contains("redist")
340
                         && !name.to_lowercase().contains("setup")
267
                         && !name.to_lowercase().contains("setup")
crates/wanda-core/src/prefix/builder.rsmodified
@@ -236,8 +236,11 @@ impl<'a> PrefixBuilder<'a> {
236
     async fn install_dependencies(&self) -> Result<()> {
236
     async fn install_dependencies(&self) -> Result<()> {
237
         info!("Installing additional dependencies...");
237
         info!("Installing additional dependencies...");
238
 
238
 
239
-        // Install common dependencies WeMod might need
239
+        // Install common dependencies WeMod needs
240
-        let deps = ["vcrun2019", "corefonts"];
240
+        // - vcrun2019: Visual C++ runtime
241
+        // - corefonts: Windows fonts
242
+        // - winhttp/wininet: Network components for WeMod authentication
243
+        let deps = ["vcrun2019", "corefonts", "winhttp", "wininet"];
241
 
244
 
242
         for dep in deps {
245
         for dep in deps {
243
             info!("Installing {}...", dep);
246
             info!("Installing {}...", dep);
crates/wanda-core/src/steam/proton.rsmodified
@@ -208,21 +208,27 @@ impl ProtonManager {
208
         is_ge: bool,
208
         is_ge: bool,
209
         is_experimental: bool,
209
         is_experimental: bool,
210
     ) -> ProtonCompatibility {
210
     ) -> ProtonCompatibility {
211
-        // GE-Proton 8+ is recommended for WeMod
211
+        // Proton Experimental is recommended for WeMod
212
-        if is_ge && version.0 >= 8 {
212
+        // GE-Proton 10.x has wow64 mode issues that cause WeMod to crash
213
+        if is_experimental {
213
             return ProtonCompatibility::Recommended;
214
             return ProtonCompatibility::Recommended;
214
         }
215
         }
215
 
216
 
216
-        // GE-Proton 7.x should work
217
+        // GE-Proton 9.x and below should work (before wow64 mode)
217
-        if is_ge && version.0 >= 7 {
218
+        if is_ge && version.0 >= 8 && version.0 <= 9 {
218
             return ProtonCompatibility::Supported;
219
             return ProtonCompatibility::Supported;
219
         }
220
         }
220
 
221
 
221
-        // Proton Experimental might work
222
+        // GE-Proton 10.x has known wow64 issues with WeMod
222
-        if is_experimental {
223
+        if is_ge && version.0 >= 10 {
223
             return ProtonCompatibility::Experimental;
224
             return ProtonCompatibility::Experimental;
224
         }
225
         }
225
 
226
 
227
+        // GE-Proton 7.x might work
228
+        if is_ge && version.0 >= 7 {
229
+            return ProtonCompatibility::Supported;
230
+        }
231
+
226
         // Standard Proton 8+ should work
232
         // Standard Proton 8+ should work
227
         if version.0 >= 8 {
233
         if version.0 >= 8 {
228
             return ProtonCompatibility::Supported;
234
             return ProtonCompatibility::Supported;
@@ -315,10 +321,21 @@ mod tests {
315
 
321
 
316
     #[test]
322
     #[test]
317
     fn test_compatibility() {
323
     fn test_compatibility() {
324
+        // Proton Experimental is now recommended (GE-Proton 10.x has wow64 issues)
318
         let compat =
325
         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);
320
         assert_eq!(compat, ProtonCompatibility::Recommended);
327
         assert_eq!(compat, ProtonCompatibility::Recommended);
321
 
328
 
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
+
322
         let compat =
339
         let compat =
323
             ProtonManager::determine_compatibility("Proton 6.0", (6, 0, 0), false, false);
340
             ProtonManager::determine_compatibility("Proton 6.0", (6, 0, 0), false, false);
324
         assert_eq!(compat, ProtonCompatibility::Unsupported);
341
         assert_eq!(compat, ProtonCompatibility::Unsupported);
crates/wanda-core/src/wemod/installer.rsmodified
@@ -320,15 +320,11 @@ impl<'a> WemodInstaller<'a> {
320
         info!("Starting WeMod from: {}", wemod_exe.display());
320
         info!("Starting WeMod from: {}", wemod_exe.display());
321
         info!("Using wine: {}", wine);
321
         info!("Using wine: {}", wine);
322
 
322
 
323
-        // Run WeMod with required flags for Wine/Proton compatibility
323
+        // Run WeMod with minimal flags for Wine/Proton compatibility
324
-        // These Electron flags are necessary for proper rendering under Wine
324
+        // Only --no-sandbox is required; additional GPU flags can cause issues
325
         let child = Command::new(&wine)
325
         let child = Command::new(&wine)
326
             .arg(&wemod_exe)
326
             .arg(&wemod_exe)
327
-            .arg("--no-sandbox")              // Required for Wine
327
+            .arg("--no-sandbox")  // Required for Electron under 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
332
             .envs(&env)
328
             .envs(&env)
333
             .stdout(Stdio::inherit())
329
             .stdout(Stdio::inherit())
334
             .stderr(Stdio::inherit())
330
             .stderr(Stdio::inherit())