epik/launcher/
mod.rs

1// Game launcher with Wine/Proton support
2use crate::{Error, Result};
3use std::collections::HashMap;
4use std::path::PathBuf;
5use std::process::{Child, Command};
6
7pub mod wine_tools;
8
9#[derive(Debug, Clone)]
10pub struct LaunchConfig {
11    pub game_executable: PathBuf,
12    pub working_directory: PathBuf,
13    pub launch_arguments: Vec<String>,
14    pub environment_vars: HashMap<String, String>,
15    pub use_wine: bool,
16    pub wine_prefix: Option<PathBuf>,
17    pub wine_executable: Option<PathBuf>,
18    pub pre_launch_command: Option<String>,
19    pub post_launch_command: Option<String>,
20}
21
22#[derive(Debug, Clone)]
23pub struct WineInstallation {
24    pub name: String,
25    pub path: PathBuf,
26}
27
28impl Default for LaunchConfig {
29    fn default() -> Self {
30        Self {
31            game_executable: PathBuf::new(),
32            working_directory: PathBuf::new(),
33            launch_arguments: Vec::new(),
34            environment_vars: HashMap::new(),
35            use_wine: false,
36            wine_prefix: None,
37            wine_executable: None,
38            pre_launch_command: None,
39            post_launch_command: None,
40        }
41    }
42}
43
44pub struct GameLauncher {
45    config: LaunchConfig,
46}
47
48impl GameLauncher {
49    pub fn new(config: LaunchConfig) -> Self {
50        Self { config }
51    }
52
53    // Launch the game with the configured settings
54    pub fn launch(&self) -> Result<Child> {
55        // Verify executable exists
56        if !self.config.game_executable.exists() {
57            return Err(Error::Other(format!(
58                "Game executable not found: {:?}",
59                self.config.game_executable
60            )));
61        }
62
63        // Run pre-launch command if specified
64        if let Some(pre_cmd) = &self.config.pre_launch_command {
65            log::info!("Running pre-launch command: {}", pre_cmd);
66            self.run_shell_command(pre_cmd)?;
67        }
68
69        // Determine if we need Wine/Proton
70        let mut cmd = if self.config.use_wine {
71            self.setup_wine_command()?
72        } else {
73            let mut cmd = Command::new(&self.config.game_executable);
74            cmd.current_dir(&self.config.working_directory);
75            cmd
76        };
77
78        // Add launch arguments
79        for arg in &self.config.launch_arguments {
80            cmd.arg(arg);
81        }
82
83        // Set environment variables
84        for (key, value) in &self.config.environment_vars {
85            cmd.env(key, value);
86        }
87
88        log::info!("Launching game: {:?}", self.config.game_executable);
89        log::debug!("Working directory: {:?}", self.config.working_directory);
90        log::debug!("Arguments: {:?}", self.config.launch_arguments);
91
92        // Launch the game
93        let child = cmd
94            .spawn()
95            .map_err(|e| Error::Other(format!("Failed to spawn game process: {}", e)))?;
96
97        // Run post-launch command if specified (in background)
98        if let Some(post_cmd) = &self.config.post_launch_command {
99            let cmd = post_cmd.clone();
100            std::thread::spawn(move || {
101                if let Err(e) = Self::run_shell_command_static(&cmd) {
102                    log::error!("Post-launch command failed: {}", e);
103                }
104            });
105        }
106
107        Ok(child)
108    }
109
110    // Setup Wine/Proton command
111    fn setup_wine_command(&self) -> Result<Command> {
112        // Determine Wine executable
113        let wine_exe = if let Some(wine_path) = &self.config.wine_executable {
114            wine_path.clone()
115        } else {
116            // Try to find Wine in PATH
117            Self::find_wine_executable()?
118        };
119
120        log::info!("Using Wine: {:?}", wine_exe);
121
122        let mut cmd = Command::new(wine_exe);
123
124        // Set Wine prefix
125        if let Some(prefix) = &self.config.wine_prefix {
126            cmd.env("WINEPREFIX", prefix);
127            log::debug!("Wine prefix: {:?}", prefix);
128        }
129
130        // Set working directory
131        cmd.current_dir(&self.config.working_directory);
132
133        // Add the game executable as first argument to Wine
134        cmd.arg(&self.config.game_executable);
135
136        Ok(cmd)
137    }
138
139    // Find Wine executable in system
140    fn find_wine_executable() -> Result<PathBuf> {
141        // Check for Proton first (Steam compatibility tool)
142        if let Ok(proton_path) = Self::find_proton() {
143            return Ok(proton_path);
144        }
145
146        // Check for standard Wine
147        for wine_name in &["wine", "wine64", "wine32"] {
148            if let Ok(output) = Command::new("which").arg(wine_name).output() {
149                if output.status.success() {
150                    let path = String::from_utf8_lossy(&output.stdout);
151                    let path = path.trim();
152                    if !path.is_empty() {
153                        log::info!("Found Wine: {}", path);
154                        return Ok(PathBuf::from(path));
155                    }
156                }
157            }
158        }
159
160        log::error!("Wine not found in system!");
161        log::info!("To install Wine, run: ./setup-wine.sh");
162        log::info!("Or manually install Wine for your distribution");
163
164        Err(Error::Other(
165            "Wine not found. Please install Wine using: ./setup-wine.sh or manually for your distribution. See https://www.winehq.org/download".to_string(),
166        ))
167    }
168
169    // Find Proton installation
170    fn find_proton() -> Result<PathBuf> {
171        use directories::UserDirs;
172
173        // Common Proton locations
174        let user_dirs = UserDirs::new();
175        let potential_paths = vec![
176            user_dirs
177                .as_ref()
178                .map(|u| u.home_dir().join(".steam/steam/steamapps/common")),
179            user_dirs
180                .as_ref()
181                .map(|u| u.home_dir().join(".local/share/Steam/steamapps/common")),
182        ];
183
184        for base_path in potential_paths.into_iter().flatten() {
185            if !base_path.exists() {
186                continue;
187            }
188
189            // Look for Proton directories
190            if let Ok(entries) = std::fs::read_dir(&base_path) {
191                for entry in entries.flatten() {
192                    let path = entry.path();
193                    if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
194                        if name.starts_with("Proton") {
195                            let proton_exe = path.join("proton");
196                            if proton_exe.exists() {
197                                log::info!("Found Proton: {:?}", proton_exe);
198                                return Ok(proton_exe);
199                            }
200                        }
201                    }
202                }
203            }
204        }
205
206        Err(Error::Other("Proton not found".to_string()))
207    }
208
209    // Run a shell command
210    fn run_shell_command(&self, command: &str) -> Result<()> {
211        Self::run_shell_command_static(command)
212    }
213
214    fn run_shell_command_static(command: &str) -> Result<()> {
215        let shell = if cfg!(windows) { "cmd" } else { "sh" };
216        let flag = if cfg!(windows) { "/C" } else { "-c" };
217
218        let output = Command::new(shell)
219            .arg(flag)
220            .arg(command)
221            .output()
222            .map_err(|e| Error::Other(format!("Failed to run command: {}", e)))?;
223
224        if !output.status.success() {
225            return Err(Error::Other(format!(
226                "Command failed with exit code: {:?}",
227                output.status.code()
228            )));
229        }
230
231        Ok(())
232    }
233}
234
235// Builder for LaunchConfig
236pub struct LaunchConfigBuilder {
237    config: LaunchConfig,
238}
239
240impl LaunchConfigBuilder {
241    pub fn new(executable: PathBuf, working_dir: PathBuf) -> Self {
242        Self {
243            config: LaunchConfig {
244                game_executable: executable,
245                working_directory: working_dir,
246                ..Default::default()
247            },
248        }
249    }
250
251    pub fn with_arguments(mut self, args: Vec<String>) -> Self {
252        self.config.launch_arguments = args;
253        self
254    }
255
256    pub fn with_environment(mut self, env: HashMap<String, String>) -> Self {
257        self.config.environment_vars = env;
258        self
259    }
260
261    pub fn with_wine(mut self, wine_exe: Option<PathBuf>, prefix: Option<PathBuf>) -> Self {
262        self.config.use_wine = true;
263        self.config.wine_executable = wine_exe;
264        self.config.wine_prefix = prefix;
265        self
266    }
267
268    pub fn with_pre_launch_command(mut self, cmd: String) -> Self {
269        self.config.pre_launch_command = Some(cmd);
270        self
271    }
272
273    pub fn with_post_launch_command(mut self, cmd: String) -> Self {
274        self.config.post_launch_command = Some(cmd);
275        self
276    }
277
278    pub fn build(self) -> LaunchConfig {
279        self.config
280    }
281}
282
283// Discover available Wine/Proton installations on the system.
284pub fn discover_wine_installations() -> Vec<WineInstallation> {
285    use std::collections::HashSet;
286
287    let mut results: Vec<WineInstallation> = Vec::new();
288    let mut seen: HashSet<PathBuf> = HashSet::new();
289
290    // System Wine (from PATH)
291    if let Ok(path) = GameLauncher::find_wine_executable() {
292        if seen.insert(path.clone()) {
293            let name = "Wine (system)".to_string();
294            results.push(WineInstallation { name, path });
295        }
296    }
297
298    // Steam compatibility tools (Proton custom builds)
299    let mut compat_dirs: Vec<PathBuf> =
300        vec![PathBuf::from("/usr/share/steam/compatibilitytools.d")];
301    if let Some(user_dirs) = directories::UserDirs::new() {
302        let home = user_dirs.home_dir();
303        compat_dirs.push(home.join(".steam/root/compatibilitytools.d"));
304        compat_dirs.push(home.join(".local/share/Steam/compatibilitytools.d"));
305    }
306
307    for base in compat_dirs {
308        if !base.exists() {
309            continue;
310        }
311
312        if let Ok(entries) = std::fs::read_dir(&base) {
313            for entry in entries.flatten() {
314                let dir = entry.path();
315                if !dir.is_dir() {
316                    continue;
317                }
318
319                let proton_runner = dir.join("proton");
320                if proton_runner.exists() {
321                    if seen.insert(proton_runner.clone()) {
322                        let name = format!(
323                            "Proton ({})",
324                            dir.file_name()
325                                .and_then(|n| n.to_str())
326                                .unwrap_or("compatibilitytool")
327                        );
328                        results.push(WineInstallation {
329                            name,
330                            path: proton_runner,
331                        });
332                    }
333                }
334            }
335        }
336    }
337
338    results
339}