emu_runner/contexts/
fceux.rs

1use camino::Utf8PathBuf;
2use crate::{EmulatorContext, Error};
3use crate::includes::copy_if_different;
4
5#[derive(Debug, Clone, PartialEq)]
6pub struct FceuxContext {
7    pub config: Option<Utf8PathBuf>,
8    pub movie: Option<Utf8PathBuf>,
9    pub lua: Option<Utf8PathBuf>,
10    pub rom: Option<Utf8PathBuf>,
11    
12    /// If set, forces Old (false) or New (true) PPU mode.
13    /// 
14    /// **Note:** Only used when executable is `fceux` (linux binary) or `qfceux.exe`
15    pub ppu_mode: Option<bool>,
16    pub working_dir: Utf8PathBuf,
17}
18impl EmulatorContext for FceuxContext {
19    fn cmd_name(&self) -> String {
20        #[cfg(target_family = "unix")]
21        {
22            match self.determine_executable() {
23                Some(exe) if exe == "fceux" => "./fceux".into(),
24                Some(_) => "wine".into(),
25                None => "./fceux".into(),
26            }
27        }
28        
29        #[cfg(target_family = "windows")]
30        {
31            match self.determine_executable() {
32                Some(exe) => exe,
33                None => "fceux.exe".into()
34            }
35        }
36    }
37    
38    fn args(&self) -> Vec<String> {
39        let mut args = Vec::with_capacity(5);
40        
41        #[cfg(target_family = "unix")]
42        {
43            if self.cmd_name() == "wine" {
44                args.push(self.determine_executable().unwrap());
45            }
46        }
47        
48        match self.determine_executable() {
49            Some(exe) => match exe.as_str() {
50                "fceux.exe" | "fceux64.exe" => {
51                    if let Some(config) = self.config.as_ref() {
52                        args.push("-cfg".into());
53                        args.push(config.to_string());
54                    }
55                    if let Some(movie) = self.movie.as_ref() {
56                        args.push("-playmovie".into());
57                        args.push(movie.to_string());
58                    }
59                    if let Some(lua) = self.lua.as_ref() {
60                        args.push("-lua".into());
61                        args.push(lua.to_string());
62                    }
63                },
64                "fceux" | "qfceux.exe" => {
65                    if let Some(movie) = self.movie.as_ref() {
66                        args.push("--playmov".into());
67                        args.push(movie.to_string());
68                    }
69                    if let Some(lua) = self.lua.as_ref() {
70                        args.push("--loadlua".into());
71                        args.push(lua.to_string());
72                    }
73                    if let Some(ppu_mode) = self.ppu_mode.as_ref() {
74                        args.push("--newppu".into());
75                        args.push(if *ppu_mode { "1".into() } else { "0".into() });
76                    }
77                },
78                _ => ()
79            },
80            None => ()
81        }
82        
83        if let Some(rom) = self.rom.as_ref() {
84            args.push(rom.to_string());
85        }
86        
87        args
88    }
89    
90    fn env(&self) -> Vec<(String, String)> {
91        let mut vars = vec![];
92
93        #[cfg(target_family = "unix")]
94        {
95            if self.cmd_name() == "wine" {
96                let mut prefix = self.working_dir();
97                prefix.push(".wine/");
98                
99                vars.push(("WINEPREFIX".into(), prefix.to_string()));
100            }
101        }
102        
103        let mut home = self.working_dir();
104        home.push(".fceux/");
105        vars.push(("HOME".into(), home.to_string()));
106        
107        vars
108    }
109    
110    fn prepare(&mut self) -> Result<(), Error> {
111        // FCEUX accepts configs/movies/scripts/roms from anywhere,
112        // so we only need to verify they exist.
113        // However, since we change the working directory, and there's no
114        // easy way to test if file exists relative to a different dir,
115        // the paths _should_ be absolute, either originally or via the with_* functions.
116        
117        #[cfg(target_family = "windows")]
118        {
119            if cmd_name == "./fceux" {
120                return Err(Error::IncompatibleOSVersion);
121            }
122        }
123        
124        if let Some(config) = self.config.as_ref() {
125            // Preparing the config file is extremely messy.
126            // - win32/win64 provides a CLI argument that is used.
127            // - win64-QtSLD uses the fceux.cfg located beside the executable.
128            // - compiled linux builds use $HOME/.fceux/fceux.cfg.
129            //     (if $HOME isn't set, it's unclear what FCEUX does)
130            
131            if !config.is_file() {
132                return Err(Error::MissingConfig(config.clone()));
133            }
134            
135            if let Some(config) = self.config.as_ref() {
136                if !config.is_file() {
137                    return Err(Error::MissingConfig(config.clone()));
138                }
139                
140                if let Some(exe) = self.determine_executable() {
141                    let mut dest = self.working_dir();
142                    if exe == "fceux" {
143                        dest.push(".fceux/");
144                        if !dest.is_dir() {
145                            std::fs::create_dir_all(&dest)?;
146                        }
147                        dest.push("fceux.cfg");
148                        
149                        copy_if_different(&std::fs::read(config)?, dest)?;
150                    } else if exe == "qfceux.exe" {
151                        dest.push("fceux.cfg");
152                        
153                        copy_if_different(&std::fs::read(config)?, dest)?;
154                    } else if !config.is_absolute() {
155                        return Err(Error::AbsolutePathFailed);
156                    }
157                }
158            }
159        }
160        if let Some(movie) = self.movie.as_ref() {
161            if !movie.is_file() {
162                return Err(Error::MissingMovie(movie.clone()));
163            }
164            if !movie.is_absolute() {
165                return Err(Error::AbsolutePathFailed);
166            }
167        }
168        if let Some(lua) = self.lua.as_ref() {
169            if !lua.is_file() {
170                return Err(Error::MissingLua(lua.clone()));
171            }
172            if !lua.is_absolute() {
173                return Err(Error::AbsolutePathFailed);
174            }
175        }
176        if let Some(rom) = self.rom.as_ref() {
177            if !rom.is_file() {
178                return Err(Error::MissingRom(rom.clone()));
179            }
180            if !rom.is_absolute() {
181                return Err(Error::AbsolutePathFailed);
182            }
183        }
184        
185        Ok(())
186    }
187    
188    fn working_dir(&self) -> Utf8PathBuf {
189        self.working_dir.clone()
190    }
191}
192impl FceuxContext {
193    /// Creates a new Context with default options.
194    /// 
195    /// If the path does not point to a directory, or a file within a directory, which contains a valid FCEUX executable,
196    /// an error will be returned.
197    pub fn new<P: Into<Utf8PathBuf>>(working_dir: P) -> Result<Self, Error> {
198        let mut working_dir = working_dir.into();
199        if working_dir.is_file() {
200            working_dir.pop();
201        }
202        
203        working_dir = working_dir.canonicalize_utf8().unwrap_or(working_dir);
204        
205        if working_dir.is_file() || !working_dir.exists() {
206            return Err(Error::MissingExecutable(working_dir));
207        }
208        
209        let mut found = false;
210        for exe in ["fceux.exe", "fceux64.exe", "qfceux.exe", "fceux"] {
211            let mut path = working_dir.clone();
212            path.push(exe);
213            
214            if path.is_file() {
215                found = true;
216                break;
217            }
218        }
219        if !found {
220            let mut path = working_dir.clone();
221            path.push("fceux");
222            return Err(Error::MissingExecutable(path));
223        }
224        
225        Ok(Self {
226            config: None,
227            movie: None,
228            lua: None,
229            rom: None,
230            ppu_mode: None,
231            working_dir,
232        })
233    }
234    
235    pub fn with_config<P: Into<Utf8PathBuf>>(self, config: P) -> Self {
236        let config = config.into();
237        Self {
238            config: Some(config.canonicalize_utf8().unwrap_or_else(|_| config)),
239            ..self
240        }
241    }
242    
243    pub fn with_movie<P: Into<Utf8PathBuf>>(self, movie: P) -> Self {
244        let movie = movie.into();
245        Self {
246            movie: Some(movie.canonicalize_utf8().unwrap_or_else(|_| movie)),
247            ..self
248        }
249    }
250    
251    pub fn with_lua<P: Into<Utf8PathBuf>>(self, lua: P) -> Self {
252        let lua = lua.into();
253        Self {
254            lua: Some(lua.canonicalize_utf8().unwrap_or_else(|_| lua)),
255            ..self
256        }
257    }
258    
259    pub fn with_rom<P: Into<Utf8PathBuf>>(self, rom: P) -> Self {
260        let rom = rom.into();
261        Self {
262            rom: Some(rom.canonicalize_utf8().unwrap_or_else(|_| rom)),
263            ..self
264        }
265    }
266    
267    pub fn with_ppu_mode(self, ppu_mode: bool) -> Self {
268        Self {
269            ppu_mode: Some(ppu_mode),
270            ..self
271        }
272    }
273    
274    pub fn determine_executable(&self) -> Option<String> {
275        let mut path = self.working_dir();
276        path.push("fceux");
277        if path.is_file() {
278            return Some("fceux".into())
279        }
280        
281        path.pop();
282        path.push("fceux.exe");
283        if path.is_file() {
284            return Some("fceux.exe".into())
285        }
286        
287        path.pop();
288        path.push("fceux64.exe");
289        if path.is_file() {
290            return Some("fceux64.exe".into())
291        }
292        
293        path.pop();
294        path.push("qfceux.exe");
295        if path.is_file() {
296            return Some("qfceux.exe".into())
297        }
298        
299        None
300    }
301}