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 {

Use a specific config file path instead of the default

25    #[arg(long = "config")]
26    config_path: Option<PathBuf>,

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'

33    #[arg(long = "file-path", short = 'f')]
34    startup_file_paths: Vec<PathBuf>,
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

42    #[arg(long = "hardware")]
43    console: Option<Console>,

Attempt to load the specified save state when launching the game. Can only be used in combination with -f / --file-path

47    #[arg(long, value_name = "SLOT")]
48    load_save_state: Option<usize>,

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

55    #[arg(long = "config-override")]
56    config_overrides: Vec<String>,

Print version string and immediately exit

59    #[arg(short = 'v', long, default_value_t = false, action = clap::ArgAction::SetTrue)]
60    version: bool,
61}
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}