diff --git a/Cargo.toml b/Cargo.toml index 04d0dca..361f544 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,11 @@ sevenz-rust = "0.5" chrono = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -egui = "0.23" +eframe = "0.23" +rfd = {version = "0.12", default-features = false, features = ["xdg-portal"] } sha2 = "0.10" rusttype = "0.9" +futures = "0.3" sdl2-sys = { git = "https://github.com/Rust-SDL2/rust-sdl2.git" } clap = { version = "4.4", features = ["derive"] } diff --git a/src/device.rs b/src/device.rs index b3a7f7a..6834082 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,4 +1,6 @@ use crate::ui; +use std::fs; +use std::io::Read; pub mod ai; pub mod cache; @@ -32,6 +34,109 @@ pub mod tlb; pub mod unmapped; pub mod vi; +pub fn run_game(file_path: &std::path::Path, device: &mut Device, fullscreen: bool) { + let rom_contents = get_rom_contents(file_path); + + cart_rom::init(device, rom_contents); // cart needs to come before rdram + + // rdram pointer is shared with parallel-rdp + let (rdram_ptr, rdram_size) = rdram::init(device); + + ui::audio::init(&mut device.ui, 33600); + ui::video::init(&mut device.ui, rdram_ptr, rdram_size, fullscreen); + ui::input::init(&mut device.ui); + + mi::init(device); + pif::init(device); + memory::init(device); + rsp_interface::init(device); + rdp::init(device); + vi::init(device); + cpu::init(device); + + ui::storage::init(&mut device.ui); + ui::storage::load_saves(&mut device.ui); + cart_rom::load_rom_save(device); + + cpu::run(device); +} + +fn swap_rom(contents: Vec) -> Vec { + let test = u32::from_be_bytes(contents[0..4].try_into().unwrap()); + if test == 0x80371240 { + // z64 + contents + } else if test == 0x37804012 { + // v64 + let mut data: Vec = vec![0; contents.len()]; + for i in (0..contents.len()).step_by(2) { + let temp = u16::from_ne_bytes(contents[i..i + 2].try_into().unwrap()); + data[i..i + 2].copy_from_slice(&temp.to_be_bytes()); + } + return data; + } else if test == 0x40123780 { + // n64 + let mut data: Vec = vec![0; contents.len()]; + for i in (0..contents.len()).step_by(4) { + let temp = u32::from_ne_bytes(contents[i..i + 4].try_into().unwrap()); + data[i..i + 4].copy_from_slice(&temp.to_be_bytes()); + } + return data; + } else { + panic!("unknown rom format") + } +} + +fn get_rom_contents(file_path: &std::path::Path) -> Vec { + let mut contents = vec![]; + if file_path.extension().unwrap().to_ascii_lowercase() == "zip" { + let zip_file = fs::File::open(file_path).unwrap(); + let mut archive = zip::ZipArchive::new(zip_file).unwrap(); + for i in 0..archive.len() { + let mut file = archive.by_index(i).unwrap(); + let extension = file + .enclosed_name() + .unwrap() + .extension() + .unwrap() + .to_ascii_lowercase(); + if extension == "z64" || extension == "n64" || extension == "v64" { + file.read_to_end(&mut contents) + .expect("could not read zip file"); + break; + } + } + } else if file_path.extension().unwrap().to_ascii_lowercase() == "7z" { + let mut archive = + sevenz_rust::SevenZReader::open(file_path, sevenz_rust::Password::empty()).unwrap(); + + let mut found = false; + archive + .for_each_entries( + &mut |entry: &sevenz_rust::SevenZArchiveEntry, reader: &mut dyn std::io::Read| { + let name = entry.name().to_ascii_lowercase(); + if !found + && (name.ends_with("z64") || name.ends_with("n64") || name.ends_with("v64")) + { + reader + .read_to_end(&mut contents) + .expect("could not read zip file"); + found = true; + } else { + //skip other files + std::io::copy(reader, &mut std::io::sink())?; + } + Ok(true) + }, + ) + .expect("ok"); + } else { + contents = fs::read(file_path).expect("Should have been able to read the file"); + } + + swap_rom(contents) +} + pub struct Device { pub ui: ui::Ui, byte_swap: usize, diff --git a/src/main.rs b/src/main.rs index a1ade90..44c754b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ #![feature(round_ties_even)] #![feature(string_remove_matches)] -use std::fs; -use std::io::Read; +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release + mod device; mod ui; use clap::Parser; @@ -55,158 +55,79 @@ struct Args { clear_input_bindings: bool, } -fn swap_rom(contents: Vec) -> Vec { - let test = u32::from_be_bytes(contents[0..4].try_into().unwrap()); - if test == 0x80371240 { - // z64 - contents - } else if test == 0x37804012 { - // v64 - let mut data: Vec = vec![0; contents.len()]; - for i in (0..contents.len()).step_by(2) { - let temp = u16::from_ne_bytes(contents[i..i + 2].try_into().unwrap()); - data[i..i + 2].copy_from_slice(&temp.to_be_bytes()); - } - return data; - } else if test == 0x40123780 { - // n64 - let mut data: Vec = vec![0; contents.len()]; - for i in (0..contents.len()).step_by(4) { - let temp = u32::from_ne_bytes(contents[i..i + 4].try_into().unwrap()); - data[i..i + 4].copy_from_slice(&temp.to_be_bytes()); - } - return data; - } else { - panic!("unknown rom format") - } -} +fn main() { + let cache_dir = dirs::cache_dir().unwrap().join("gopher64"); -fn get_rom_contents(file_path: &std::path::Path) -> Vec { - let mut contents = vec![]; - if file_path.extension().unwrap().to_ascii_lowercase() == "zip" { - let zip_file = fs::File::open(file_path).unwrap(); - let mut archive = zip::ZipArchive::new(zip_file).unwrap(); - for i in 0..archive.len() { - let mut file = archive.by_index(i).unwrap(); - let extension = file - .enclosed_name() - .unwrap() - .extension() - .unwrap() - .to_ascii_lowercase(); - if extension == "z64" || extension == "n64" || extension == "v64" { - file.read_to_end(&mut contents) - .expect("could not read zip file"); - break; + let _ = std::fs::create_dir_all(cache_dir.clone()); + let _ = std::fs::remove_file(cache_dir.clone().join("game_running")); + + let args = Args::parse(); + let args_as_strings: Vec = std::env::args().collect(); + let args_count = args_as_strings.len(); + if args_count > 1 { + let mut device = device::Device::new(); + + if args.clear_input_bindings { + ui::input::clear_bindings(&mut device.ui); + return; + } + if args.port.is_some() { + let port = args.port.unwrap(); + if !(1..=4).contains(&port) { + println!("Port must be betwen 1 and 4"); + return; } } - } else if file_path.extension().unwrap().to_ascii_lowercase() == "7z" { - let mut archive = - sevenz_rust::SevenZReader::open(file_path, sevenz_rust::Password::empty()).unwrap(); + if args.list_controllers { + ui::input::list_controllers(&device.ui); + return; + } + if args.assign_controller.is_some() { + if args.port.is_none() { + println!("Must specify port number"); + return; + } + ui::input::assign_controller( + &mut device.ui, + args.assign_controller.unwrap(), + args.port.unwrap(), + ); + return; + } + if args.bind_input_profile.is_some() { + if args.port.is_none() { + println!("Must specify port number"); + return; + } + ui::input::bind_input_profile( + &mut device.ui, + args.bind_input_profile.unwrap(), + args.port.unwrap(), + ); + return; + } + if args.configure_input_profile.is_some() { + ui::input::configure_input_profile( + &mut device.ui, + args.configure_input_profile.unwrap(), + ); + return; + } - let mut found = false; - archive - .for_each_entries( - &mut |entry: &sevenz_rust::SevenZArchiveEntry, reader: &mut dyn std::io::Read| { - let name = entry.name().to_ascii_lowercase(); - if !found - && (name.ends_with("z64") || name.ends_with("n64") || name.ends_with("v64")) - { - reader - .read_to_end(&mut contents) - .expect("could not read zip file"); - found = true; - } else { - //skip other files - std::io::copy(reader, &mut std::io::sink())?; - } - Ok(true) - }, - ) - .expect("ok"); + if args.game.is_some() { + let file_path = std::path::Path::new(args.game.as_ref().unwrap()); + device::run_game(file_path, &mut device, args.fullscreen); + } } else { - contents = fs::read(file_path).expect("Should have been able to read the file"); - } - - swap_rom(contents) -} - -fn run_game(file_path: &std::path::Path, device: &mut device::Device, fullscreen: bool) { - let rom_contents = get_rom_contents(file_path); - - device::cart_rom::init(device, rom_contents); // cart needs to come before rdram - - // rdram pointer is shared with parallel-rdp - let (rdram_ptr, rdram_size) = device::rdram::init(device); - - ui::audio::init(&mut device.ui, 33600); - ui::video::init(&mut device.ui, rdram_ptr, rdram_size, fullscreen); - ui::input::init(&mut device.ui); - - device::mi::init(device); - device::pif::init(device); - device::memory::init(device); - device::rsp_interface::init(device); - device::rdp::init(device); - device::vi::init(device); - device::cpu::init(device); - - ui::storage::init(&mut device.ui); - ui::storage::load_saves(&mut device.ui); - device::cart_rom::load_rom_save(device); - - device::cpu::run(device); -} - -fn main() { - let args = Args::parse(); - let mut device = device::Device::new(); - - if args.clear_input_bindings { - ui::input::clear_bindings(&mut device.ui); - return; - } - if args.port.is_some() { - let port = args.port.unwrap(); - if !(1..=4).contains(&port) { - println!("Port must be betwen 1 and 4"); - return; - } - } - if args.list_controllers { - ui::input::list_controllers(&device.ui); - return; - } - if args.assign_controller.is_some() { - if args.port.is_none() { - println!("Must specify port number"); - return; - } - ui::input::assign_controller( - &mut device.ui, - args.assign_controller.unwrap(), - args.port.unwrap(), - ); - return; - } - if args.bind_input_profile.is_some() { - if args.port.is_none() { - println!("Must specify port number"); - return; - } - ui::input::bind_input_profile( - &mut device.ui, - args.bind_input_profile.unwrap(), - args.port.unwrap(), - ); - return; - } - if args.configure_input_profile.is_some() { - ui::input::configure_input_profile(&mut device.ui, args.configure_input_profile.unwrap()); - return; - } - if args.game.is_some() { - let file_path = std::path::Path::new(args.game.as_ref().unwrap()); - run_game(file_path, &mut device, args.fullscreen); + let options = eframe::NativeOptions { + initial_window_size: Some(eframe::egui::vec2(640.0, 480.0)), + ..Default::default() + }; + eframe::run_native( + "gopher64", + options, + Box::new(|_cc| Box::new(ui::gui::GopherEguiApp::new())), + ) + .unwrap(); } } diff --git a/src/ui.rs b/src/ui.rs index b65433e..9ac67dd 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,5 +1,6 @@ pub mod audio; pub mod config; +pub mod gui; pub mod input; pub mod storage; pub mod video; diff --git a/src/ui/gui.rs b/src/ui/gui.rs new file mode 100644 index 0000000..e75f73d --- /dev/null +++ b/src/ui/gui.rs @@ -0,0 +1,220 @@ +use crate::device; +use crate::ui; +use eframe::egui; + +pub struct GopherEguiApp { + configure_profile: bool, + profile_name: String, + controllers: Vec, + selected_controller: [i32; 4], + selected_profile: [String; 4], + input_profiles: Vec, +} + +fn get_input_profiles(game_ui: &ui::Ui) -> Vec { + let mut profiles = vec![]; + for key in game_ui.config.input.input_profiles.keys() { + profiles.push((*key).clone()) + } + profiles +} + +fn get_controllers(game_ui: &ui::Ui) -> Vec { + let mut controllers: Vec = vec![]; + + let joystick_subsystem = game_ui.joystick_subsystem.as_ref().unwrap(); + let num_joysticks = joystick_subsystem.num_joysticks().unwrap(); + for i in 0..num_joysticks { + controllers.push(joystick_subsystem.name_for_index(i).unwrap()); + } + controllers +} + +impl GopherEguiApp { + pub fn new() -> GopherEguiApp { + let game_ui = ui::Ui::new(); + let joystick_subsystem = game_ui.joystick_subsystem.as_ref().unwrap(); + let num_joysticks = joystick_subsystem.num_joysticks().unwrap(); + let mut guids: Vec = vec![]; + for i in 0..num_joysticks { + guids.push(joystick_subsystem.device_guid(i).unwrap().to_string()); + } + let mut selected_controller = [-1, -1, -1, -1]; + for (pos, item) in game_ui + .config + .input + .controller_assignment + .iter() + .enumerate() + { + if item.is_some() { + for (guid_pos, guid) in guids.iter().enumerate() { + if item.as_deref().unwrap() == *guid { + selected_controller[pos] = guid_pos as i32; + break; + } + } + } + } + GopherEguiApp { + configure_profile: false, + profile_name: "".to_string(), + selected_profile: game_ui.config.input.input_profile_binding.clone(), + selected_controller, + controllers: get_controllers(&game_ui), + input_profiles: get_input_profiles(&game_ui), + } + } +} + +fn save_config(game_ui: &mut ui::Ui, selected_controller: [i32; 4], selected_profile: [String; 4]) { + let joystick_subsystem = game_ui.joystick_subsystem.as_ref().unwrap(); + for (pos, item) in selected_controller.iter().enumerate() { + if *item != -1 { + game_ui.config.input.controller_assignment[pos] = Some( + joystick_subsystem + .device_guid(*item as u32) + .unwrap() + .to_string(), + ); + } else { + game_ui.config.input.controller_assignment[pos] = None + } + } + + game_ui.config.input.input_profile_binding = selected_profile; +} + +impl Drop for GopherEguiApp { + fn drop(&mut self) { + let mut game_ui = ui::Ui::new(); + save_config( + &mut game_ui, + self.selected_controller, + self.selected_profile.clone(), + ); + } +} + +impl eframe::App for GopherEguiApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + if self.configure_profile { + egui::Window::new("Configure Input Profile") + // .open(&mut self.configure_profile) + .show(ctx, |ui| { + ui.horizontal(|ui| { + let name_label = ui.label("Profile Name: "); + ui.text_edit_singleline(&mut self.profile_name) + .labelled_by(name_label.id); + }); + ui.horizontal(|ui| { + if ui.button("Configure Profile").clicked() { + let profile_name = self.profile_name.clone(); + execute(async { + let mut game_ui = ui::Ui::new(); + ui::input::configure_input_profile(&mut game_ui, profile_name); + }); + self.configure_profile = false; + if !self.profile_name.is_empty() && self.profile_name != "default" { + self.input_profiles.push(self.profile_name.clone()) + } + }; + if ui.button("Close").clicked() { + self.configure_profile = false + }; + }); + }); + } + + egui::CentralPanel::default().show(ctx, |ui| { + ui.set_enabled(!self.configure_profile); + + if ui.button("Open ROM").clicked() { + // Spawn dialog on main thread + let task = rfd::AsyncFileDialog::new().pick_file(); + let selected_controller = self.selected_controller; + let selected_profile = self.selected_profile.clone(); + execute(async move { + let file = task.await; + + if let Some(file) = file { + let running_file = dirs::cache_dir() + .unwrap() + .join("gopher64") + .join("game_running"); + if running_file.exists() { + return; + } + let _ = std::fs::File::create(running_file.clone()); + let mut device = device::Device::new(); + save_config(&mut device.ui, selected_controller, selected_profile); + device::run_game(std::path::Path::new(file.path()), &mut device, false); + let _ = std::fs::remove_file(running_file.clone()); + } + }); + } + + if ui.button("Configure Input Profile").clicked() { + let running_file = dirs::cache_dir() + .unwrap() + .join("gopher64") + .join("game_running"); + if running_file.exists() { + return; + } + self.configure_profile = true; + } + + ui.add_space(32.0); + ui.label("Controller Config:"); + egui::Grid::new("some_unique_id").show(ui, |ui| { + ui.label("Port"); + ui.label("Profile"); + ui.label("Controller"); + ui.end_row(); + for i in 0..4 { + ui.label(format!("{}", i + 1)); + + egui::ComboBox::from_id_source(format!("profile-combo-{}", i)) + .selected_text(self.selected_profile[i].clone()) + .show_ui(ui, |ui| { + for j in 0..self.input_profiles.len() { + ui.selectable_value( + &mut self.selected_profile[i], + self.input_profiles[j].clone(), + self.input_profiles[j].clone(), + ); + } + }); + + let controller_text = if self.selected_controller[i] == -1 { + "None".to_string() + } else { + self.controllers[self.selected_controller[i] as usize].clone() + }; + egui::ComboBox::from_id_source(format!("controller-combo-{}", i)) + .selected_text(controller_text) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.selected_controller[i], + -1, + "None".to_string(), + ); + for j in 0..self.controllers.len() { + ui.selectable_value( + &mut self.selected_controller[i], + j as i32, + self.controllers[j].clone(), + ); + } + }); + ui.end_row(); + } + }); + }); + } +} + +fn execute + Send + 'static>(f: F) { + std::thread::spawn(move || futures::executor::block_on(f)); +} diff --git a/src/ui/input.rs b/src/ui/input.rs index d91adc2..30a3fc2 100644 --- a/src/ui/input.rs +++ b/src/ui/input.rs @@ -298,6 +298,10 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String) { println!("Profile name cannot be default"); return; } + if profile.is_empty() { + println!("Profile name cannot be empty"); + return; + } let joystick_subsystem = ui.sdl_context.as_ref().unwrap().joystick().unwrap(); let mut joysticks = vec![]; for i in 0..joystick_subsystem.num_joysticks().unwrap() {