diff --git a/Cargo.toml b/Cargo.toml index f5a428b..57129c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,6 @@ rfd = "0.15.0" pollster = "0.4.0" async-mutex = "1.4.0" clap = { version = "4.5.21", features = ["derive"] } +serde = { version = "1.0.218", features = ["derive"] } +toml = "0.8.20" +dirs = "6.0.0" diff --git a/src/config/consts.rs b/src/config/consts.rs new file mode 100644 index 0000000..05aa4df --- /dev/null +++ b/src/config/consts.rs @@ -0,0 +1,2 @@ +pub const CONFIG_PATH: &str = ".hachi"; +pub const CONFIG_FILE: &str = "config.toml"; diff --git a/src/config/errors.rs b/src/config/errors.rs new file mode 100644 index 0000000..97fd351 --- /dev/null +++ b/src/config/errors.rs @@ -0,0 +1,18 @@ +use std::error::Error; +use std::fmt::{Display, Formatter}; + +#[derive(Debug)] +pub enum ConfigError { + InvalidFileFormat, + NotReadable, + NotWritable, + CannotCreateDirectory, +} + +impl Display for ConfigError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl Error for ConfigError {} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..fd9eabb --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,79 @@ +mod consts; +mod errors; + +use crate::config::consts::{CONFIG_FILE, CONFIG_PATH}; +use crate::config::errors::ConfigError; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; + +#[derive(Clone, Deserialize, Serialize)] +pub struct Configuration { + pub vm: VmOptions, + pub debug: DebugOptions, +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct VmOptions { + pub cycles_per_frame: u32, +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct DebugOptions { + pub enable_debug_menu: bool, +} + +impl Default for Configuration { + fn default() -> Self { + Self { + vm: VmOptions { cycles_per_frame: 10 }, + debug: DebugOptions { enable_debug_menu: false }, + } + } +} + +impl Configuration { + fn get_file_path() -> PathBuf { + dirs::home_dir().unwrap().join(CONFIG_PATH).join(CONFIG_FILE) + } + + pub fn load() -> Result { + let file_path = Self::get_file_path(); + + if !file_path.exists() { + let config = Configuration::default(); + config.update()?; + + return Ok(config); + } + + let configuration = Self::read_from_file(&file_path)?; + + Ok(configuration) + } + + fn read_from_file(path: &PathBuf) -> Result { + let file_contents = + fs::read_to_string(path).map_err(|_| ConfigError::NotReadable)?; + + let configuration = toml::from_str(&file_contents) + .map_err(|_| ConfigError::InvalidFileFormat)?; + + Ok(configuration) + } + + pub fn update(&self) -> Result<(), ConfigError> { + let config_path = Self::get_file_path(); + + if !config_path.exists() { + fs::create_dir_all(config_path.parent().unwrap()) + .map_err(|_| ConfigError::CannotCreateDirectory)?; + } + + let config_to_str = toml::to_string(self).unwrap(); + fs::write(config_path, config_to_str) + .map_err(|_| ConfigError::NotWritable)?; + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 812257d..beef8d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod config; mod ui; mod vm; diff --git a/src/ui/debug.rs b/src/ui/debug.rs index 3cb68fd..8d5a8ae 100644 --- a/src/ui/debug.rs +++ b/src/ui/debug.rs @@ -1,6 +1,5 @@ use crate::ui::consts::MEMORY_MAX_DISPLAY_ITEMS; use crate::ui::state::State; -use crate::vm::consts::{DEBUG_DISPLAY_HEIGHT, DEBUG_DISPLAY_WIDTH}; use notan::egui; use notan::egui::{ vec2, Align, Button, Color32, Frame, Grid, Layout, Mesh, Rect, ScrollArea, @@ -18,11 +17,10 @@ fn filter_by_memory_address(index: &usize, state: &State) -> bool { *index == search_address } -pub fn vm_display(state: &mut State, ui: &mut Ui) { +pub fn vm_display(state: &mut State, ui: &mut Ui, height: f32, width: f32) { Frame::canvas(ui.style()).show(ui, |ui| { let rect = ui.max_rect(); - let desired_size = - Vec2::new(DEBUG_DISPLAY_WIDTH as f32, DEBUG_DISPLAY_HEIGHT as f32); + let desired_size = Vec2::new(width, height); let mut vm_display_mesh = Mesh::with_texture(state.vm_display.id); vm_display_mesh.add_rect_with_uv( @@ -33,8 +31,6 @@ pub fn vm_display(state: &mut State, ui: &mut Ui) { ui.painter().add(Shape::mesh(vm_display_mesh)); }); - - ui.add_space(270.0); } pub fn display_memory_contents(ui: &mut Ui, state: &mut State) { diff --git a/src/ui/egui_plugin.rs b/src/ui/egui_plugin.rs index d298913..7ea3ca4 100644 --- a/src/ui/egui_plugin.rs +++ b/src/ui/egui_plugin.rs @@ -1,8 +1,11 @@ use super::state::State; use crate::ui::debug::{display_memory_contents, vm_display}; +use crate::ui::options::option_dialog; +use crate::vm::consts::{DEBUG_DISPLAY_HEIGHT, DEBUG_DISPLAY_WIDTH}; use notan::app::App; use notan::egui::{ - self, CentralPanel, Context, Grid, Id, Modal, SidePanel, TopBottomPanel, Ui, + self, CentralPanel, Color32, Context, Grid, Id, Mesh, Modal, Rect, Shape, + SidePanel, TopBottomPanel, Ui, Vec2, }; use pollster::FutureExt; use rfd::AsyncFileDialog; @@ -21,6 +24,10 @@ pub fn init<'a>( }); }); + if state.show_configuration_window { + option_dialog(state, ctx); + } + CentralPanel::default().show(ctx, |ui| { if let Err(e) = state.vm.last_cycle_result.clone() { state.vm.pause(); @@ -47,9 +54,18 @@ pub fn init<'a>( } }); - if !state.vm.is_running && !state.debug_mode_enabled { + if !state.debug_mode_enabled { CentralPanel::default().show(ctx, |ui| { - ui.label("Welcome to Hachi!"); + if !state.vm.is_running { + ui.label("Welcome to Hachi!"); + } else { + vm_display_fullscreen( + state, + ui, + _app.window().width() as f32, + _app.window().width() as f32, + ); + } }); } @@ -80,7 +96,14 @@ pub fn init<'a>( }); ui.vertical(|ui| { - vm_display(state, ui); + vm_display( + state, + ui, + DEBUG_DISPLAY_HEIGHT as f32, + DEBUG_DISPLAY_WIDTH as f32, + ); + + ui.add_space(270.0); ui.separator(); @@ -122,11 +145,31 @@ fn file_menu_handler(state: &mut State) -> impl FnOnce(&mut Ui) + '_ { }); } } + + if ui.button("Options").clicked() { + ui.close_menu(); + + state.show_configuration_window = true; + } } } fn view_menu_handler(state: &mut State) -> impl FnOnce(&mut Ui) + '_ { |ui| { - ui.checkbox(&mut state.debug_mode_enabled, "Debug mode"); + ui.checkbox(&mut state.debug_mode_enabled, "Debug mode").clicked(); } } + +fn vm_display_fullscreen(state: &State, ui: &mut Ui, height: f32, width: f32) { + let rect = ui.available_rect_before_wrap(); + let desired_size = Vec2::new(width, height / 1.5); + + let mut vm_display_mesh = Mesh::with_texture(state.vm_display.id); + vm_display_mesh.add_rect_with_uv( + Rect::from_min_size(egui::pos2(0.0, rect.min.y), desired_size), + Rect::from_min_max(egui::pos2(0.0, 1.0), egui::pos2(1.0, 0.0)), + Color32::WHITE, + ); + + ui.painter().add(Shape::mesh(vm_display_mesh)); +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index ddb8247..2baa735 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,6 +1,7 @@ mod consts; mod debug; pub(super) mod egui_plugin; +mod options; pub mod state; use crate::vm::consts::DISPLAY_WIDTH; @@ -52,10 +53,5 @@ pub fn draw( } gfx.render(&ui_renderer); - - if state.debug_mode_enabled { - gfx.render_to(&state.display_renderer, &vm_display_renderer); - } else { - gfx.render(&vm_display_renderer); - } + gfx.render_to(&state.display_renderer, &vm_display_renderer); } diff --git a/src/ui/options.rs b/src/ui/options.rs new file mode 100644 index 0000000..b672da5 --- /dev/null +++ b/src/ui/options.rs @@ -0,0 +1,46 @@ +use crate::ui::state::State; +use notan::egui; +use notan::egui::Window; + +pub fn option_dialog(state: &mut State, ctx: &egui::Context) { + Window::new("Options") + .max_height(100.0) + .max_width(200.0) + .resizable(false) + .show(ctx, |ui| { + let mut cycles_per_frame_raw_text = + state.configuration.vm.cycles_per_frame.to_string(); + + ui.horizontal(|ui| { + ui.label("Cycles per frame"); + if ui + .text_edit_singleline(&mut cycles_per_frame_raw_text) + .changed() + { + if let Ok(value) = cycles_per_frame_raw_text.parse::() + { + state.configuration.vm.cycles_per_frame = value; + } + } + }); + + ui.horizontal(|ui| { + ui.checkbox( + &mut state.configuration.debug.enable_debug_menu, + "Run \"Debug mode\" on start", + ); + }); + + ui.add_space(20.0); + + ui.centered_and_justified(|ui| { + if ui.button("Close").clicked() { + state.configuration.update().unwrap(); + + state.cycles_per_frame = + state.configuration.vm.cycles_per_frame; + state.show_configuration_window = false; + } + }); + }); +} diff --git a/src/ui/state.rs b/src/ui/state.rs index aa58af9..1578e34 100644 --- a/src/ui/state.rs +++ b/src/ui/state.rs @@ -1,4 +1,4 @@ -use crate::vm::consts::{DEBUG_DISPLAY_HEIGHT, DEBUG_DISPLAY_WIDTH}; +use crate::config::Configuration; use crate::vm::VirtualMachine; use async_mutex::Mutex as AsyncMutex; use clap::Parser; @@ -12,8 +12,8 @@ use std::sync::Arc; #[derive(AppState)] pub struct State { pub last_dir: PathBuf, + pub configuration: Configuration, pub file_path_option: Arc>>, - pub cycles_per_frame: u32, pub memory_debug_page: usize, pub memory_address_search: String, pub vm: VirtualMachine, @@ -22,6 +22,8 @@ pub struct State { pub vm_display: SizedTexture, pub keypad_bindigs: HashMap, pub timer: f32, + pub show_configuration_window: bool, + pub cycles_per_frame: u32, } #[derive(Parser)] @@ -32,21 +34,20 @@ struct Args { pub fn setup(gfx: &mut Graphics) -> State { let args = Args::parse(); + let file_path = if args.file.is_empty() { None } else { Some(PathBuf::from(args.file).canonicalize().unwrap()) }; + let last_dir = if let Some(path) = &file_path { path.parent().unwrap().to_path_buf() } else { std::env::current_dir().unwrap() }; - let display_renderer = gfx - .create_render_texture(DEBUG_DISPLAY_WIDTH, DEBUG_DISPLAY_HEIGHT) - .build() - .unwrap(); + let display_renderer = gfx.create_render_texture(800, 600).build().unwrap(); let vm_display_texture = gfx.egui_register_texture(&display_renderer); @@ -75,18 +76,22 @@ pub fn setup(gfx: &mut Graphics) -> State { (KeyCode::V, 0xF), ]; + let config = Configuration::load().unwrap(); + State { last_dir, + configuration: config.clone(), file_path_option: Arc::new(AsyncMutex::new(file_path)), - cycles_per_frame: 10, memory_debug_page: 0, memory_address_search: String::new(), vm: VirtualMachine::new(), - debug_mode_enabled: false, + debug_mode_enabled: config.debug.enable_debug_menu, display_renderer, vm_display: vm_display_texture, keypad_bindigs: default_bindings.into(), timer: 0.0, + show_configuration_window: false, + cycles_per_frame: config.vm.cycles_per_frame, } }