Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 2 additions & 0 deletions src/config/consts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub const CONFIG_PATH: &str = ".hachi";
pub const CONFIG_FILE: &str = "config.toml";
18 changes: 18 additions & 0 deletions src/config/errors.rs
Original file line number Diff line number Diff line change
@@ -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 {}
79 changes: 79 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -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<Self, ConfigError> {
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<Configuration, ConfigError> {
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(())
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod config;
mod ui;
mod vm;

Expand Down
8 changes: 2 additions & 6 deletions src/ui/debug.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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(
Expand All @@ -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) {
Expand Down
53 changes: 48 additions & 5 deletions src/ui/egui_plugin.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
Expand All @@ -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,
);
}
});
}

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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));
}
8 changes: 2 additions & 6 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod consts;
mod debug;
pub(super) mod egui_plugin;
mod options;
pub mod state;

use crate::vm::consts::DISPLAY_WIDTH;
Expand Down Expand Up @@ -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);
}
46 changes: 46 additions & 0 deletions src/ui/options.rs
Original file line number Diff line number Diff line change
@@ -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::<u32>()
{
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;
}
});
});
}
21 changes: 13 additions & 8 deletions src/ui/state.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<AsyncMutex<Option<PathBuf>>>,
pub cycles_per_frame: u32,
pub memory_debug_page: usize,
pub memory_address_search: String,
pub vm: VirtualMachine,
Expand All @@ -22,6 +22,8 @@ pub struct State {
pub vm_display: SizedTexture,
pub keypad_bindigs: HashMap<KeyCode, usize>,
pub timer: f32,
pub show_configuration_window: bool,
pub cycles_per_frame: u32,
}

#[derive(Parser)]
Expand All @@ -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);

Expand Down Expand Up @@ -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,
}
}

Expand Down