#![deny(warnings)] mod cheats; mod device; mod netplay; mod retroachievements; mod savestates; mod ui; use clap::Parser; use std::io::Error; /// N64 emulator #[derive(Parser, Debug)] #[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None, arg_required_else_help = if cfg!(feature = "gui") { false } else { true })] struct Args { game: Option, #[arg(short, long)] fullscreen: bool, #[arg(long)] overclock: Option, #[arg(long)] disable_expansion_pak: Option, #[arg(long, value_name = "CHEATS_FILE", hide = true)] cheats: Option, #[arg(long, value_name = "NETPLAY_PEER_ADDR", hide = true)] netplay_peer_addr: Option, #[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)] netplay_player_number: Option, #[arg( long, value_name = "PROFILE_NAME", help = "Create a new input profile (keyboard/gamepad mappings)" )] configure_input_profile: Option, #[arg(long, help = "Use DirectInput when configuring a new input profile")] use_dinput: bool, #[arg( long, value_name = "DEADZONE_PERCENTAGE", help = "Used along with --configure-input-profile to set the deadzone for analog sticks" )] deadzone: Option, #[arg( long, value_name = "PROFILE_NAME", help = "Must also specify --port. Used to bind a previously created profile to a port" )] bind_input_profile: Option, #[arg( long, help = "Lists connected controllers which can be used in --assign-controller" )] list_controllers: bool, #[arg( long, value_name = "CONTROLLER_NUMBER", help = "Must also specify --port. Used to assign a controller listed in --list-controllers to a port" )] assign_controller: Option, #[arg( long, value_name = "PORT", help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller" )] port: Option, #[arg( long, help = "Clear all input profile bindings and controller assignments" )] clear_input_bindings: bool, #[arg( long, value_name = "SLOT", help = "Load savestate from slot 0-9 when starting the game" )] load_state: Option, #[arg( long = "ra-username", value_name = "USERNAME", help = "Username for RetroAchievements" )] ra_username: Option, #[arg( long = "ra-token", value_name = "TOKEN", help = "Token for RetroAchievements", hide = true )] ra_token: Option, #[arg( long = "ra-password", value_name = "PASSWORD", help = "Password for RetroAchievements" )] ra_password: Option, #[arg( long = "ra-hardcore", help = "Enable Hardcore mode for RetroAchievements" )] ra_hardcore: bool, #[arg( long = "ra-challenge", help = "Enable Challenge Indicators for RetroAchievements" )] ra_challenge: bool, #[arg( long = "ra-leaderboard", help = "Enable Leaderboard Trackers for RetroAchievements" )] ra_leaderboard: bool, #[arg(long = "discord-rich-presence", help = "Enable Discord Rich Presence")] discord_rich_presence: bool, } pub async fn run() -> std::io::Result<()> { let dirs = ui::get_dirs(); std::fs::create_dir_all(dirs.config_dir)?; std::fs::create_dir_all(dirs.cache_dir)?; std::fs::create_dir_all(dirs.data_dir.join("saves"))?; std::fs::create_dir_all(dirs.data_dir.join("states"))?; ui::sdl_hints(); let args = Args::parse(); if let Some(game) = args.game { let file_path = std::path::Path::new(&game); let Some(rom_contents) = device::get_rom_contents(file_path) else { return Err(Error::other(format!( "Could not read ROM file: {}", file_path.display() ))); }; if let Some(slot) = args.load_state && slot > 9 { return Err(Error::other("Savestate slot must be between 0 and 9")); } let mut device = device::Device::new(); device.ui.config.recent_roms.retain(|x| *x != game); device.ui.config.recent_roms.insert(0, game); device.ui.config.recent_roms.truncate(5); if args.fullscreen { device.ui.video.fullscreen = true; } else { device.ui.video.fullscreen = device.ui.config.video.fullscreen; } let overclock = args .overclock .unwrap_or(device.ui.config.emulation.overclock); let disable_expansion_pak = args .disable_expansion_pak .unwrap_or(device.ui.config.emulation.disable_expansion_pak); let cheats = if let Some(cheats_file) = args.cheats && let Ok(data) = std::fs::read(cheats_file) && let Ok(cheats) = serde_json::from_slice(&data) { cheats } else { let game_crc = ui::storage::get_game_crc(&rom_contents); ui::config::Cheats::new() .cheats .get(&game_crc) .cloned() .unwrap_or_default() }; let mut shutdown_tx = None; let mut usb_handle = None; if let Some(peer_addr) = args.netplay_peer_addr && let Some(player_number) = args.netplay_player_number { device.netplay = Some(netplay::init(peer_addr.parse().unwrap(), player_number)); } else { for i in 0..4 { if device.ui.config.input.transfer_pak[i] && !device.ui.config.input.gb_rom_path[i].is_empty() && !device.ui.config.input.gb_ram_path[i].is_empty() && let Ok(rom) = std::fs::read(&device.ui.config.input.gb_rom_path[i]) && let Ok(ram) = std::fs::read(&device.ui.config.input.gb_ram_path[i]) { device::controller::gbcart::init(&mut device.transferpaks[i].cart, &rom, &ram); } } if device.ui.config.emulation.usb { (shutdown_tx, usb_handle, device.ui.usb) = ui::usb::init(); } if let Some(username) = args.ra_username { retroachievements::init_client( if cfg!(ra_hardcore_enabled) { args.ra_hardcore } else { false }, args.ra_challenge, args.ra_leaderboard, ); let (tx, rx) = tokio::sync::oneshot::channel::(); if let Some(password) = args.ra_password { retroachievements::login_user(username, password, tx); } else if let Some(token) = args.ra_token { retroachievements::login_token_user(username, token, tx); } else { tx.send(false).unwrap(); } rx.await.unwrap(); } } let (discord_watch_tx, discord_handle) = retroachievements::load_game( &rom_contents, rom_contents.len(), args.discord_rich_presence, ) .await; device::run_game( &mut device, &rom_contents, ui::GameSettings { overclock, disable_expansion_pak, cheats, load_savestate_slot: args.load_state, }, ); retroachievements::shutdown_client(discord_watch_tx, discord_handle).await; if device.netplay.is_some() { netplay::close(&mut device); } else { for i in 0..4 { if device.ui.config.input.transfer_pak[i] && !device.ui.config.input.gb_ram_path[i].is_empty() && !device.transferpaks[i].cart.ram.is_empty() { device::controller::gbcart::save( &mut device.transferpaks[i].cart, device.vi.elapsed_time as i64, &device.ui.config.input.gb_ram_path[i], ); } } } if let Some(shutdown_tx) = &shutdown_tx { ui::usb::close(shutdown_tx, usb_handle).await; } } else if std::env::args().count() > 1 { let mut config = ui::config::Config::new(); if let Some(profile) = args.configure_input_profile { ui::input::configure_input_profile( &mut config, profile, args.use_dinput, args.deadzone.unwrap_or(ui::input::DEADZONE_DEFAULT), ); ui::sdl_close(); return Ok(()); } else { if args.clear_input_bindings { ui::input::clear_bindings(&mut config); return Ok(()); } if let Some(port) = args.port && !(1..=4).contains(&port) { return Err(Error::other("Port must be between 1 and 4")); } ui::sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD); if args.list_controllers { let controllers = ui::input::get_controller_names(); for (i, controller) in controllers.iter().enumerate() { println!("Controller {i}: {controller}"); } } else { if let Some(assign_controller) = args.assign_controller { let Some(port) = args.port else { ui::sdl_close(); return Err(Error::other("Must specify port number")); }; ui::input::assign_controller(&mut config, assign_controller - 1, port); } if let Some(profile) = args.bind_input_profile { let Some(port) = args.port else { ui::sdl_close(); return Err(Error::other("Must specify port number")); }; ui::input::bind_input_profile(&mut config, profile, port); } } } } else { #[cfg(feature = "gui")] { retroachievements::init_client(false, false, false); ui::gui::app_window(); retroachievements::shutdown_client(None, None).await; } } ui::sdl_close(); Ok(()) }