1use clap::Parser; 2use egui_sdl3_wgpu::{FrameOptions, FrameRunEffect}; 3use env_logger::Env; 4use image::{DynamicImage, ImageFormat}; 5use jgenesis_common::frontend::EmulatorTrait; 6use jgenesis_gui::GuiEmulatorRunner; 7use jgenesis_gui::app::{App, ConfigInfo, LoadAtStartup}; 8use jgenesis_native_config::AppConfig; 9use jgenesis_native_config::paths::{ConfigDirType, ConfigDirs, ConfigWithPath}; 10use jgenesis_native_driver::config::AppConfigExt; 11use jgenesis_native_driver::extensions::Console; 12use jgenesis_native_driver::{ 13 NativeEmulator, NativeGameBoyEmulator, NativeGbaEmulator, NativeGenesisEmulator, 14 NativeNesEmulator, NativePcEngineEmulator, NativeSmsGgEmulator, NativeSnesEmulator, 15 NativeTickEffect, SdlSubsystems, 16}; 17use std::path::{Path, PathBuf}; 18use std::thread; 19use std::time::Duration; 20 21#[allow(clippy::doc_markdown)] 22#[derive(Debug, Parser)] 23struct Args { 24 /// Use a specific config file path instead of the default 25 #[arg(long = "config")] 26 config_path: Option<PathBuf>, 27 28 /// If set, the application will open this file immediately after starting up, and it will exit 29 /// when the emulator window is closed (or if there is an error). 30 /// 31 /// For platforms with multiple file inputs (e.g. Sega CD 32X), you can set this argument 32 /// multiple times, e.g. '-f /path/to/cartridge.32x -f /path/to/disc.cue' 33 #[arg(long = "file-path", short = 'f')] 34 startup_file_paths: Vec<PathBuf>, 35 36 #[arg(long, default_value_t = false, action = clap::ArgAction::SetTrue)] 37 /// Run without opening the GUI window. Can only be used in combination with -f / --file-path 38 no_gui: bool, 39 40 /// In combination with -f / --file-path, specify what hardware to emulate. Will auto-detect based on file if 41 /// not set 42 #[arg(long = "hardware")] 43 console: Option<Console>, 44 45 /// Attempt to load the specified save state when launching the game. Can only be used in 46 /// combination with -f / --file-path 47 #[arg(long, value_name = "SLOT")] 48 load_save_state: Option<usize>, 49 50 /// Override an individual config setting without modifying the settings file. Can only be used 51 /// in combination with -f / --file-path 52 /// 53 /// Parameter value should be in the form 'key=value', e.g. '--config-override genesis.remove_sprite_limits=true'. 54 /// Can be set multiple times to override multiple settings 55 #[arg(long = "config-override")] 56 config_overrides: Vec<String>, 57 58 /// Print version string and immediately exit 59 #[arg(short = 'v', long, default_value_t = false, action = clap::ArgAction::SetTrue)] 60 version: bool, 61} 62 63impl Args { 64 fn fix_appimage_relative_paths(mut self) -> Self { 65 if let Some(config_path) = self.config_path { 66 self.config_path = 67 Some(jgenesis_common::fix_appimage_relative_path(&config_path).into_owned()); 68 } 69 70 self.startup_file_paths = self 71 .startup_file_paths 72 .into_iter() 73 .map(|path| jgenesis_common::fix_appimage_relative_path(&path).into_owned()) 74 .collect(); 75 76 self 77 } 78 79 fn load_at_startup(&self) -> Option<LoadAtStartup> { 80 if self.startup_file_paths.is_empty() { 81 return None; 82 } 83 84 Some(LoadAtStartup { 85 file_paths: self.startup_file_paths.clone(), 86 console: self.console, 87 load_state_slot: self.load_save_state, 88 config_overrides: self.config_overrides.clone(), 89 }) 90 } 91 92 fn validate_or_exit(&self) { 93 fn exit_with_message(message: &str) -> ! { 94 <Args as clap::Args>::augment_args(clap::Command::new(env!("CARGO_BIN_NAME"))) 95 .error(clap::error::ErrorKind::ValueValidation, message) 96 .exit() 97 } 98 99 let file_path_present = !self.startup_file_paths.is_empty(); 100 101 if self.console.is_some() && !file_path_present { 102 exit_with_message("--hardware can only be used with the -f / --file-path arg"); 103 } 104 105 if self.load_save_state.is_some() && !file_path_present { 106 exit_with_message("--load-save-state can only be used with the -f / --file-path arg"); 107 } 108 109 if self.no_gui && !file_path_present { 110 exit_with_message("--no-gui can only be used with the -f / --file-path arg"); 111 } 112 113 if !self.config_overrides.is_empty() && !file_path_present { 114 exit_with_message("--config-override can only be used with the -f / --file-path arg"); 115 } 116 } 117} 118 119fn initial_gui_size(config: &AppConfig) -> (f32, f32) { 120 ( 121 f32_max(jgenesis_native_config::DEFAULT_GUI_WIDTH, config.gui_window_width), 122 f32_max(jgenesis_native_config::DEFAULT_GUI_HEIGHT, config.gui_window_height), 123 ) 124} 125 126fn f32_max(value: f32, max: f32) -> f32 { 127 if value < max { max } else { value } 128} 129 130fn load_icon() -> DynamicImage { 131 const ICON: &[u8] = include_bytes!("../../256x256.png"); 132 133 image::load_from_memory_with_format(ICON, ImageFormat::Png).expect("Failed to load GUI icon") 134} 135 136fn main() -> anyhow::Result<()> { 137 env_logger::Builder::from_env( 138 Env::default().default_filter_or(jgenesis_common::DEFAULT_LOG_FILTER), 139 ) 140 .init(); 141 142 let args = Args::parse().fix_appimage_relative_paths(); 143 144 if args.version { 145 println!("{}", env!("CARGO_PKG_VERSION")); 146 return Ok(()); 147 } 148 149 args.validate_or_exit(); 150 151 let config_dirs = ConfigDirs::new(); 152 let config_dir_type = config_dirs.default_dir_type(args.config_path.clone()); 153 let config_with_path = ConfigWithPath::load_from_dir_or_default( 154 &config_dirs, 155 &config_dir_type, 156 AppConfig::default, 157 ); 158 159 if let Some(file_path) = args.startup_file_paths.first() { 160 log::info!("Will open file '{}' after starting", file_path.display()); 161 } 162 163 let sdl = SdlSubsystems::init()?; 164 165 let load_at_startup = args.load_at_startup(); 166 167 if args.no_gui 168 && let Some(load_at_startup) = &load_at_startup 169 { 170 run_no_gui(sdl, config_with_path, load_at_startup) 171 } else { 172 run_with_gui(sdl, config_with_path, config_dirs, config_dir_type, load_at_startup) 173 } 174} 175 176fn run_with_gui( 177 sdl: SdlSubsystems, 178 config_with_path: ConfigWithPath, 179 config_dirs: ConfigDirs, 180 config_dir_type: ConfigDirType, 181 load_at_startup: Option<LoadAtStartup>, 182) -> anyhow::Result<()> { 183 let (gui_width, gui_height) = initial_gui_size(&config_with_path.config); 184 185 let icon = load_icon(); 186 187 let config_info = ConfigInfo { 188 initial_config: config_with_path.config, 189 config_path: config_with_path.path, 190 config_dirs, 191 config_dir_type, 192 }; 193 194 let options = FrameOptions { 195 window_width: gui_width.round() as u32, 196 window_height: gui_height.round() as u32, 197 icon: Some(icon), 198 ..FrameOptions::default() 199 }; 200 201 let mut frame = egui_sdl3_wgpu::Frame::new("jgenesis", &sdl.video.borrow(), options)?; 202 203 let (mut emu_runner, emu_runner_handle) = 204 GuiEmulatorRunner::new(sdl.clone(), frame.egui_ctx().clone()); 205 206 let mut app = 207 App::new(config_info, load_at_startup, frame.egui_ctx().clone(), &sdl, emu_runner_handle); 208 209 loop { 210 emu_runner.run(&mut |event| { 211 frame.handle_sdl_event(event); 212 app.handle_sdl_event(event, frame.egui_ctx(), frame.window_id()); 213 }); 214 215 if frame.run(&mut |ui, _ctx| app.ui(ui))? == FrameRunEffect::Closed { 216 return Ok(()); 217 } 218 219 thread::sleep(Duration::from_millis(1)); 220 } 221} 222 223struct RunEmulatorArgs<'a> { 224 sdl: SdlSubsystems, 225 config: AppConfig, 226 config_path: &'a Path, 227 console: Console, 228 primary_path: PathBuf, 229 secondary_paths: &'a [PathBuf], 230 load_state_slot: Option<usize>, 231} 232 233fn run_no_gui( 234 sdl: SdlSubsystems, 235 ConfigWithPath { mut config, path: config_path }: ConfigWithPath, 236 load_at_startup: &LoadAtStartup, 237) -> anyhow::Result<()> { 238 if let Err(err) = config.apply_overrides(&load_at_startup.config_overrides) { 239 log::error!("Error applying config overrides: {err}"); 240 } 241 242 let primary_path = load_at_startup.file_paths.first().unwrap(); 243 let secondary_paths = &load_at_startup.file_paths[1..]; 244 245 let console = load_at_startup 246 .console 247 .or_else(|| Console::from_file(primary_path).map(|console| console.console)) 248 .unwrap_or_else(|| { 249 log::error!( 250 "Unable to determine console for file '{}'; defaulting to Genesis", 251 primary_path.display() 252 ); 253 Console::Genesis 254 }); 255 256 let run_args = RunEmulatorArgs { 257 sdl, 258 config, 259 config_path: config_path.as_ref(), 260 console, 261 primary_path: primary_path.clone(), 262 secondary_paths, 263 load_state_slot: load_at_startup.load_state_slot, 264 }; 265 266 match console { 267 Console::MasterSystem | Console::GameGear | Console::Sg1000 => run_smsgg(run_args), 268 Console::Genesis | Console::SegaCd | Console::Sega32X | Console::SegaCd32X => { 269 run_genesis(run_args) 270 } 271 Console::Nes => run_nes(run_args), 272 Console::Snes => run_snes(run_args), 273 Console::GameBoy | Console::GameBoyColor => run_gb(run_args), 274 Console::GameBoyAdvance => run_gba(run_args), 275 Console::PcEngine => run_pce(run_args), 276 } 277} 278 279fn run_smsgg( 280 RunEmulatorArgs { sdl, config, config_path, console, primary_path, load_state_slot, .. }: RunEmulatorArgs<'_>, 281) -> anyhow::Result<()> { 282 let cheats = config 283 .try_load_cheats_if_enabled(config_path, &primary_path, console.standard_extension()) 284 .unwrap_or_default(); 285 let emulator = NativeSmsGgEmulator::create( 286 sdl, 287 config.smsgg_config(primary_path, console.to_smsgg_hardware(), &cheats), 288 )?; 289 run_emulator_no_gui(emulator, load_state_slot) 290} 291 292fn run_genesis( 293 RunEmulatorArgs { 294 sdl, 295 config, 296 config_path, 297 console, 298 primary_path, 299 secondary_paths, 300 load_state_slot, 301 }: RunEmulatorArgs<'_>, 302) -> anyhow::Result<()> { 303 let cheats = config 304 .try_load_cheats_if_enabled(config_path, &primary_path, console.standard_extension()) 305 .unwrap_or_default(); 306 let emulator = NativeGenesisEmulator::create( 307 sdl, 308 config.genesis_config( 309 primary_path, 310 secondary_paths.first().cloned(), 311 console.to_genesis_hardware(), 312 &cheats, 313 ), 314 )?; 315 run_emulator_no_gui(emulator, load_state_slot) 316} 317 318fn run_nes( 319 RunEmulatorArgs { sdl, config, primary_path, load_state_slot, .. }: RunEmulatorArgs<'_>, 320) -> anyhow::Result<()> { 321 let emulator = NativeNesEmulator::create(sdl, config.nes_config(primary_path))?; 322 run_emulator_no_gui(emulator, load_state_slot) 323} 324 325fn run_snes( 326 RunEmulatorArgs { sdl, config, primary_path, load_state_slot, .. }: RunEmulatorArgs<'_>, 327) -> anyhow::Result<()> { 328 let emulator = NativeSnesEmulator::create(sdl, config.snes_config(primary_path))?; 329 run_emulator_no_gui(emulator, load_state_slot) 330} 331 332fn run_gb( 333 RunEmulatorArgs { sdl, config, primary_path, load_state_slot, .. }: RunEmulatorArgs<'_>, 334) -> anyhow::Result<()> { 335 let emulator = NativeGameBoyEmulator::create(sdl, config.gb_config(primary_path.clone()))?; 336 run_emulator_no_gui(emulator, load_state_slot) 337} 338 339fn run_gba( 340 RunEmulatorArgs { sdl, config, primary_path, load_state_slot, .. }: RunEmulatorArgs<'_>, 341) -> anyhow::Result<()> { 342 let emulator = NativeGbaEmulator::create(sdl, config.gba_config(primary_path))?; 343 run_emulator_no_gui(emulator, load_state_slot) 344} 345 346fn run_pce( 347 RunEmulatorArgs { sdl, config, primary_path, load_state_slot, .. }: RunEmulatorArgs<'_>, 348) -> anyhow::Result<()> { 349 let emulator = NativePcEngineEmulator::create(sdl, config.pce_config(primary_path))?; 350 run_emulator_no_gui(emulator, load_state_slot) 351} 352 353fn run_emulator_no_gui<Emulator: EmulatorTrait>( 354 mut emulator: NativeEmulator<Emulator>, 355 load_state_slot: Option<usize>, 356) -> anyhow::Result<()> { 357 if let Some(slot) = load_state_slot { 358 emulator.load_state(slot)?; 359 } 360 361 loop { 362 match emulator.run(&mut |_event| {})? { 363 Some(NativeTickEffect::PowerOff | NativeTickEffect::Exit) => return Ok(()), 364 None => {} 365 } 366 367 thread::sleep(Duration::from_millis(1)); 368 } 369} 370 371#[cfg(test)] 372mod tests { 373 use super::*; 374 375 #[test] 376 fn load_icon_does_not_panic() { 377 let _ = load_icon(); 378 } 379}