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 {
Use a specific config file path instead of the default
If set, the application will open this file immediately after starting up, and it will exit when the emulator window is closed (or if there is an error).
For platforms with multiple file inputs (e.g. Sega CD 32X), you can set this argument multiple times, e.g. '-f /path/to/cartridge.32x -f /path/to/disc.cue'
36 #[arg(long, default_value_t = false, action = clap::ArgAction::SetTrue)]
Run without opening the GUI window. Can only be used in combination with -f / --file-path
38 no_gui: bool,
In combination with -f / --file-path, specify what hardware to emulate. Will auto-detect based on file if not set
Attempt to load the specified save state when launching the game. Can only be used in combination with -f / --file-path
Override an individual config setting without modifying the settings file. Can only be used in combination with -f / --file-path
Parameter value should be in the form 'key=value', e.g. '--config-override genesis.remove_sprite_limits=true'. Can be set multiple times to override multiple settings
Print version string and immediately exit
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}