1use 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 pub fn launch(&self) -> Result<Child> {
55 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 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 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 for arg in &self.config.launch_arguments {
80 cmd.arg(arg);
81 }
82
83 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 let child = cmd
94 .spawn()
95 .map_err(|e| Error::Other(format!("Failed to spawn game process: {}", e)))?;
96
97 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 fn setup_wine_command(&self) -> Result<Command> {
112 let wine_exe = if let Some(wine_path) = &self.config.wine_executable {
114 wine_path.clone()
115 } else {
116 Self::find_wine_executable()?
118 };
119
120 log::info!("Using Wine: {:?}", wine_exe);
121
122 let mut cmd = Command::new(wine_exe);
123
124 if let Some(prefix) = &self.config.wine_prefix {
126 cmd.env("WINEPREFIX", prefix);
127 log::debug!("Wine prefix: {:?}", prefix);
128 }
129
130 cmd.current_dir(&self.config.working_directory);
132
133 cmd.arg(&self.config.game_executable);
135
136 Ok(cmd)
137 }
138
139 fn find_wine_executable() -> Result<PathBuf> {
141 if let Ok(proton_path) = Self::find_proton() {
143 return Ok(proton_path);
144 }
145
146 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 fn find_proton() -> Result<PathBuf> {
171 use directories::UserDirs;
172
173 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 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 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
235pub 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
283pub 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 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 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}