main.rsannotatedmain.rssource379 lines · 12.2 KB · raw
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}