emu_runner/contexts/
bizhawk.rs

1use camino::Utf8PathBuf;
2use crate::{EmulatorContext, Error};
3use crate::includes::{BIZHAWK_BASH_DEFAULT, BIZHAWK_BASH_PRE290, copy_if_different};
4
5#[derive(Debug, Clone, PartialEq)]
6pub struct BizHawkContext {
7    pub config: Option<Utf8PathBuf>,
8    pub movie: Option<Utf8PathBuf>,
9    pub lua: Option<Utf8PathBuf>,
10    pub rom: Option<Utf8PathBuf>,
11    pub working_dir: Utf8PathBuf,
12}
13impl EmulatorContext for BizHawkContext {
14    fn cmd_name(&self) -> String {
15        #[cfg(target_family = "unix")]
16        { "bash".into() }
17        
18        #[cfg(target_family = "windows")]
19        { "EmuHawk.exe".into() }
20    }
21    
22    fn args(&self) -> Vec<String> {
23        let mut args = Vec::with_capacity(5);
24        
25        #[cfg(target_family = "unix")]
26        {
27            args.push("start-bizhawk.sh".into());
28        }
29        
30        if let Some(config) = self.config.as_ref() {
31            args.push(format!("--config={config}"));
32        }
33        if let Some(movie) = self.movie.as_ref() {
34            args.push(format!("--movie={movie}"));
35        }
36        if let Some(lua) = self.lua.as_ref() {
37            args.push(format!("--lua={lua}"));
38        }
39        if let Some(rom) = self.rom.as_ref() {
40            args.push(rom.to_string());
41        }
42        
43        args
44    }
45    
46    fn env(&self) -> Vec<(String, String)> {
47        vec![]
48    }
49
50    fn prepare(&mut self) -> Result<(), Error> {
51        // BizHawk accepts configs/movies/scripts/roms from anywhere,
52        // so we only need to verify they exist.
53        // However, since we change the working directory, and there's no
54        // easy way to test if file exists relative to a different dir,
55        // the paths _should_ be absolute, either originally or via the with_* functions.
56        
57        if let Some(config) = self.config.as_ref() {
58            if !config.is_file() {
59                return Err(Error::MissingConfig(config.clone()));
60            }
61            if !config.is_absolute() {
62                return Err(Error::AbsolutePathFailed);
63            }
64        }
65        if let Some(movie) = self.movie.as_ref() {
66            if !movie.is_file() {
67                return Err(Error::MissingMovie(movie.clone()));
68            }
69            if !movie.is_absolute() {
70                return Err(Error::AbsolutePathFailed);
71            }
72        }
73        if let Some(lua) = self.lua.as_ref() {
74            if !lua.is_file() {
75                return Err(Error::MissingLua(lua.clone()));
76            }
77            if !lua.is_absolute() {
78                return Err(Error::AbsolutePathFailed);
79            }
80        }
81        if let Some(rom) = self.rom.as_ref() {
82            if !rom.is_file() {
83                return Err(Error::MissingRom(rom.clone()));
84            }
85            if !rom.is_absolute() {
86                return Err(Error::AbsolutePathFailed);
87            }
88        }
89        
90        // If unix, copy bash script and check for incompatible versions
91        #[cfg(target_family = "unix")]
92        {
93            let bash = match self.detect_version() {
94                Some(ver) => match ver.as_str() {
95                    "2.8" | "2.8-rc1" | "2.7" | "2.6.3" | "2.6.2" | "2.6.1" | "2.6" => BIZHAWK_BASH_PRE290,
96                    
97                    "2.5.2" | "2.5.1" | "2.5.0" | "2.4.2" | "2.4.1" | "2.4" | "2.3.3"
98                        | "2.3.2" | "2.3.1" | "2.3" | "2.2.2" | "2.2.1" | "2.2" | "2.1.1"
99                        | "2.1.0" | "1.13.2" | "1.9.2" | "1.6.1" => return Err(Error::IncompatibleOSVersion),
100                    
101                    _ => BIZHAWK_BASH_DEFAULT,
102                },
103                None => BIZHAWK_BASH_DEFAULT
104            };
105            
106            let mut path = self.working_dir.clone();
107            path.push("start-bizhawk.sh");
108            copy_if_different(bash, path)?;
109        }
110        
111        Ok(())
112    }
113
114    fn working_dir(&self) -> Utf8PathBuf {
115        self.working_dir.clone()
116    }
117}
118impl BizHawkContext {
119    /// Creates a new Context with default options.
120    /// 
121    /// If the path does not point to a directory, or a file within a directory, which contains `EmuHawk.exe`,
122    /// an error will be returned.
123    pub fn new<P: Into<Utf8PathBuf>>(working_dir: P) -> Result<Self, Error> {
124        let mut working_dir = working_dir.into();
125        if working_dir.is_file() {
126            working_dir.pop();
127        }
128        
129        working_dir = working_dir.canonicalize_utf8().unwrap_or(working_dir);
130        
131        let mut detect_exe = working_dir.clone();
132        detect_exe.push("EmuHawk.exe");
133        if working_dir.is_file() || !working_dir.exists() || !detect_exe.is_file() {
134            return Err(Error::MissingExecutable(detect_exe));
135        }
136        
137        Ok(Self {
138            config: None,
139            movie: None,
140            lua: None,
141            rom: None,
142            working_dir,
143        })
144    }
145    
146    pub fn with_config<P: Into<Utf8PathBuf>>(self, config: P) -> Self {
147        let config = config.into();
148        Self {
149            config: Some(config.canonicalize_utf8().unwrap_or_else(|_| config)),
150            ..self
151        }
152    }
153    
154    pub fn with_movie<P: Into<Utf8PathBuf>>(self, movie: P) -> Self {
155        let movie = movie.into();
156        Self {
157            movie: Some(movie.canonicalize_utf8().unwrap_or_else(|_| movie)),
158            ..self
159        }
160    }
161    
162    pub fn with_lua<P: Into<Utf8PathBuf>>(self, lua: P) -> Self {
163        let lua = lua.into();
164        Self {
165            lua: Some(lua.canonicalize_utf8().unwrap_or_else(|_| lua)),
166            ..self
167        }
168    }
169    
170    pub fn with_rom<P: Into<Utf8PathBuf>>(self, rom: P) -> Self {
171        let rom = rom.into();
172        Self {
173            rom: Some(rom.canonicalize_utf8().unwrap_or_else(|_| rom)),
174            ..self
175        }
176    }
177    
178    /// Determines the emulator version by comparing the SHA1 checksum of `EmuHawk.exe`
179    pub fn detect_version(&self) -> Option<String> {
180        let mut exe = self.working_dir.clone();
181        exe.push("EmuHawk.exe");
182        let exe = std::fs::read(exe).unwrap();
183        let sha1 = sha1_smol::Sha1::from(exe).digest().to_string().to_lowercase();
184        
185        Some(match sha1.as_str() {
186            "ef7c4067cec01b60b89ff8c271f3c72a0b2d009f" => "2.9.1",
187            "c9be7f8e4a05122e60545e8988920b710bd50ea7" => "2.9",
188            "31a2fadd049957377358a0b7b1267f3d8ecebfd9" => "2.9-rc3",
189            "a6ff6e02a05a0ec52695a7ec757aedcdc16e0192" => "2.9-rc2",
190            "288e310c430cbcbc0881913efd82e91f16dc14dd" => "2.9-rc1",
191            "88e476295d004a80ea514847c0d590879e7b3d88" => "2.8",
192            "9d2738265a37e28813eeff08e41def697f58cbee" => "2.8-rc1",
193            "eac6aa28589372d120e23e5b2f69b56c2542273b" => "2.7",
194            "3261214b9991918c5224d27b6cf7d84f9acd3566" => "1.9.2",
195            "202a0d945cd20a1b2e5021d3499ac7b5c2f5ca46" => "1.6.1",
196            "7dd9dce90e16138ca38ef92cdb1270a378d21dad" => "2.6.3",
197            "3668613ed1fc61f1dafde9b678e6a637da23d882" => "2.6.2",
198            "115cb73156b4a288378fd00aa0fd982fb0c311c5" => "2.6.1",
199            "307526d8171fa9aa2dfbf735aa1eca23425b829a" => "2.6",
200            "7bcc6337005dba33fbc8a454cf7f669563f39e85" => "2.5.2",
201            "410c423feef9666955b2a0d66c3b64c3e432988a" => "2.5.1",
202            "d45a7348a8e5505b294df9add852787d04b569e4" => "2.5.0",
203            "6e169792aebef5942c9fabd276c7d3e07e2c3196" => "2.4.2",
204            "71d9bd1ae6d60b6fc7d3aebe474eae50995c29d7" => "2.4.1",
205            "2668ef81bad2459a9a14a09a3a8d5ee2c6e9cbac" => "2.4",
206            "1fbf1b672ddb4e98aef77a8edd5655149b4b4c72" => "2.3.3",
207            "d9365fd6f1f979a52689979e5709b26dfef7dc09" => "2.3.2",
208            "c2f4b95b86a11c472f7e8522598be644e9e05c6d" => "2.3.1",
209            "b2072e0bdf4944d060c83f44df17e88da4007c81" => "2.3",
210            "ab83cfd3ed5b9dc392d2b0d4aa1b99723c5bf4c9" => "1.13.2",
211            "4c1599c7ed7e5216477454ac7fac0719f2ee6e66" => "2.2.2",
212            "6095cb07bd79703527c01ad4f27ed4e907d2f030" => "2.2.1",
213            "4e2ec35bff8798494d3cc0e22276f2456939257d" => "2.2",
214            "fc372a78d03ca5229f3c125a9dff91a779a66b7a" => "2.1.1",
215            "c2e31867428e03ba2ef23911d605163a7008d6a5" => "2.1.0",
216            
217            _ => return None
218        }.to_string())
219    }
220}