diff --git a/.dsa.emulator.toml b/.dsa.emulator.toml new file mode 100644 index 0000000..a67d247 --- /dev/null +++ b/.dsa.emulator.toml @@ -0,0 +1,5 @@ +# The configuration file for the emulator. Here is an example file with all of the options you may want to set. + +[misc] +# Defaults to false, here for testing purposes. +use_discord_rpc = true diff --git a/Cargo.lock b/Cargo.lock index 3d4ee0f..0a353ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1052,6 +1052,7 @@ dependencies = [ "egui", "egui_code_editor", "rfd", + "serde", "toml", ] diff --git a/emulator/Cargo.toml b/emulator/Cargo.toml index 4c1796a..55b18db 100644 --- a/emulator/Cargo.toml +++ b/emulator/Cargo.toml @@ -7,6 +7,10 @@ edition = "2024" name = "dsa_rs" path = "src/lib.rs" +[[bin]] +name = "emulator" +required-features = ["config"] + [dependencies] common = { path = "../common" } assembler = { path = "../assembler" } @@ -17,7 +21,8 @@ rfd = "0.15.3" dirs = "6.0.0" discord-presence = { version = "1.6.0", optional = true } toml = { version = "0.8.23", optional = true } +serde = { version = "1.0.219", features = ["derive"], optional = true } [features] discord-rpc = ["dep:discord-presence"] -config = ["dep:toml"] +config = ["dep:toml", "dep:serde"] diff --git a/emulator/src/emulator/config.rs b/emulator/src/emulator/config.rs index 534ce40..bf7d83a 100644 --- a/emulator/src/emulator/config.rs +++ b/emulator/src/emulator/config.rs @@ -1,2 +1,38 @@ //! Loads configuration information from a TOML file in the current working directory. //! Currently doesn't do much but this may be expanded. + +use std::path::Path; + +use serde::Deserialize; + +#[derive(Deserialize, Default)] +pub struct Config { + pub misc: MiscTable, +} + +/// For config options where you aren't sure what table it should go under. +#[derive(Deserialize, Default)] +pub struct MiscTable { + /// Whether or not we can enable Discord RPC for fun. + #[cfg(feature = "discord-rpc")] + pub use_discord_rpc: bool, +} + +impl Config { + pub fn load(path: &Path) -> Result { + let file_contents = match std::fs::read_to_string(path) { + Ok(file_contents) => file_contents, + Err(why) => { + eprintln!( + "WARN: Expected to read config file from '{}' with error '{}'. Using default settings.", + path.display(), + why + ); + + return Ok(Self::default()); + } + }; + + Self::deserialize(toml::Deserializer::new(&file_contents)) + } +} diff --git a/emulator/src/emulator/misc/rpc.rs b/emulator/src/emulator/misc/rpc.rs index 94d2f9c..c548874 100644 --- a/emulator/src/emulator/misc/rpc.rs +++ b/emulator/src/emulator/misc/rpc.rs @@ -16,14 +16,25 @@ //! //! Alternatively, you can hide this in your Discord settings. -use discord_presence::{Client, DiscordError}; +use std::{ + path::PathBuf, + sync::{ + Arc, + mpsc::{Receiver, Sender}, + }, + time::Duration, +}; + +use discord_presence::{Client, DiscordError, models::ActivityTimestamps}; + +use crate::emulator::config::Config; #[derive(Debug)] -pub enum DiscordRpcError { +pub enum RpcClientError { Client(DiscordError), } -impl std::fmt::Display for DiscordRpcError { +impl std::fmt::Display for RpcClientError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Client(why) => write!(f, "discord RPC error: {why}"), @@ -31,26 +42,156 @@ impl std::fmt::Display for DiscordRpcError { } } -impl std::error::Error for DiscordRpcError {} +impl std::error::Error for RpcClientError {} -impl From for DiscordRpcError { +impl From for RpcClientError { fn from(err: DiscordError) -> Self { Self::Client(err) } } -/// Sets up the Discord RPC client. -#[expect(clippy::unreadable_literal)] -pub fn start_rpc() -> Result { - let mut client = discord_presence::Client::new(1384303074088190042); - - _ = client.on_ready(|ctx| { - eprintln!("The discord RPC client is ready. Got event {:?}", ctx.event); - }); - - client.start(); - - client.set_activity(|act| act)?; - - Ok(client) +/// The type of activity the user is currently doing. +#[derive(Debug, Clone)] +pub enum Activity { + Idle, + EditingFile(PathBuf), +} + +/// Messages to send over the wire. +#[derive(Debug)] +pub enum Message { + /// Sent when we want to update the [`Context`]. + Update(Activity), + /// Sent when the main program wants to exit. + Stop, +} + +unsafe impl Send for Message {} + +#[derive(Debug, Clone)] +pub struct RpcClient { + /// Sends updates to [`Context`] (our state). + sender: Sender, + /// Stored for later cleanup on Drop. + thread_handle: Option>>, +} + +impl RpcClient { + #[expect(clippy::unreadable_literal)] + /// Sets up the [`RpcClient`]. + pub fn new( + sender: Sender, + reciever: Receiver, + ) -> Result { + // TODO: Put client id into a .env file. + let mut client = discord_presence::Client::new(1384303074088190042); + + let thread_handle = std::thread::spawn(move || { + client.start(); + + eprintln!("INFO: Started Discord RPC client."); + + std::thread::sleep(Duration::from_millis(1000)); + + // Recieve updates and do shit. + for message in &reciever { + match message { + Message::Update(activity) => { + Self::handle_activity(&mut client, &activity); + } + Message::Stop => { + eprintln!("INFO: Stopping discord RPC client."); + if let Err(why) = client.shutdown() { + eprintln!("ERROR: Stopping discord RPC client failed: {why}"); + } + + break; + } + } + } + }); + + Ok(Self { + sender, + thread_handle: Some(Arc::new(thread_handle)), + }) + } + + fn handle_activity(client: &mut Client, activity: &Activity) { + let current_time = std::time::SystemTime::now(); + let timestamps = ActivityTimestamps::new().start( + current_time + .duration_since(std::time::UNIX_EPOCH) + .expect("Failed to get UNIX timestamp for activity.") + .as_secs(), + ); + + match activity { + Activity::Idle => { + client + .set_activity(|act| act.details("Idle").timestamps(|_| timestamps)) + .expect("TODO: Exponential backoff."); + } + Activity::EditingFile(file_path) => { + client + .set_activity(|act| { + act.details(format!("Editing file: {}", file_path.display())) + .timestamps(|_| timestamps) + }) + .expect("TODO: Exponential backoff."); + } + } + + eprintln!("INFO: RPC sent: {activity:?}"); + } + + /// Stops the [`RpcClient`]. + /// + /// # Panics + /// + /// May panic if the reciever was deallocated. This should not happen. + fn stop(&self) { + self.sender + .send(Message::Stop) + .expect("Failed to send stop message to RPC client."); + } + + /// Send an update with a given [`Activity`] to the [`RpcClient`]. + /// + /// # Panics + /// + /// May panic if the reciever was deallocated. This should not happen. + pub fn update(&self, activity: Activity) { + self.sender + .send(Message::Update(activity)) + .expect("Failed to send update to RPC client. This should not happen."); + } +} + +// Possibly unneeded but good practice. +impl Drop for RpcClient { + fn drop(&mut self) { + self.stop(); + + if let Some(handle) = self.thread_handle.take() { + if let Some(handle) = Arc::into_inner(handle) { + let _ = handle.join(); + } + } + } +} + +/// Gets the discord [`RpcClient`] or returns None if this has been disabled in the config +/// options. +#[cfg(feature = "config")] +pub fn get_rpc_client_or_none( + config: &Config, + rpc_sender: Sender, + rpc_reciever: Receiver, +) -> Result, Box> { + if config.misc.use_discord_rpc { + Ok(Some(RpcClient::new(rpc_sender, rpc_reciever)?)) + } else { + Ok(None) + } } diff --git a/emulator/src/emulator/system/emulator.rs b/emulator/src/emulator/system/emulator.rs index 35baed4..f0d4012 100644 --- a/emulator/src/emulator/system/emulator.rs +++ b/emulator/src/emulator/system/emulator.rs @@ -1,22 +1,29 @@ +#[cfg(feature = "discord-rpc")] +use std::sync::Arc; use std::{ sync::mpsc::{self, Receiver, Sender}, thread, time::Duration, }; +#[cfg(feature = "discord-rpc")] +use crate::emulator::misc::rpc::{Activity, RpcClient}; + use crate::emulator::system::{ model::{Command, Running, State}, processor::Processor, }; -use common::instructions::{Instruction, Register}; +use common::prelude::*; +#[expect(clippy::too_many_lines)] pub fn run_emulator( cmd_rx: &Receiver, state_tx: &Sender, mut processor: Processor, + #[cfg(feature = "discord-rpc")] rpc_client: Option<&Arc>, ) { - println!("starting"); + println!("INFO: Starting emulator."); let mut running = Running::Paused; let mut addr = 0u32; @@ -29,7 +36,7 @@ pub fn run_emulator( let mut instruction_count = 0; loop { - println!("looping"); + println!("Looping"); let cmd = if running == Running::Running { match cmd_rx.try_recv() { @@ -50,6 +57,19 @@ pub fn run_emulator( match cmd { Command::Start => { running = Running::Running; + + // Update RPC with current state. TODO: Make this only occur on state + // changes. + #[cfg(feature = "discord-rpc")] + if let Some(rpc_client) = rpc_client { + use std::{path::PathBuf, str::FromStr}; + + rpc_client.update(Activity::EditingFile( + PathBuf::from_str("test") + .expect("This is a valid path, WTF."), + )); + } + println!("Emulator started"); } Command::Stop => { @@ -70,7 +90,9 @@ pub fn run_emulator( Ok(_) => {} Err(why) => { let pcx = processor.get(Register::Pcx); - eprintln!("Could not decode instruction at {pcx:x}. Reason: {why}"); + eprintln!( + "Could not decode instruction at {pcx:x}. Reason: {why}" + ); continue; } } @@ -110,7 +132,8 @@ pub fn run_emulator( } }; - // let instruction = match Instruction::decode(cpu_lock.get(Register::Cir)) {}; + // let instruction = match Instruction::decode(cpu_lock.get(Register::Cir)) + // {}; if matches!(instruction, Instruction::Halt) { running = Running::Halted; @@ -126,7 +149,8 @@ pub fn run_emulator( if update { let memory_view = processor.memory.read_range(addr, size); - let state = state(&mut processor, running, instruction_count, memory_view); + let state = + state(&mut processor, running, instruction_count, memory_view); let _ = state_tx.send(state); } } else { diff --git a/emulator/src/emulator/ui/control_unit.rs b/emulator/src/emulator/ui/control_unit.rs index 1dbaf81..8aade22 100644 --- a/emulator/src/emulator/ui/control_unit.rs +++ b/emulator/src/emulator/ui/control_unit.rs @@ -75,8 +75,12 @@ impl Component for ControlPanel { Running::Halted => "Halted", } )); - ui.label(format!("Instructions: {}", state.instructions)); - ui.label(format!("PC: 0x{:08X}", state.reg_file.get(Register::Pcx))); + + let pcx = state.reg_file.get(Register::Pcx); + let instructions = state.instructions; + + ui.label(format!("Instructions: {instructions}")); + ui.label(format!("PC: 0x{pcx:08X}")); let instruction = Instruction::decode(state.reg_file.get(Register::Cir)) .map_or_else( diff --git a/emulator/src/main.rs b/emulator/src/main.rs index b1189e9..67d89f7 100644 --- a/emulator/src/main.rs +++ b/emulator/src/main.rs @@ -1,42 +1,50 @@ -use std::thread; +#[cfg(feature = "discord-rpc")] +use std::sync::Arc; +use std::{ + path::Path, + sync::mpsc::{Receiver, Sender}, + thread, +}; + +#[cfg(feature = "discord-rpc")] +use dsa_rs::emulator::misc::rpc::{RpcClient, get_rpc_client_or_none}; use dsa_rs::emulator::{ - system::{emulator::run_emulator, memory::MainStore, processor::Processor}, + config::Config, + system::{ + emulator::run_emulator, + memory::MainStore, + model::{Command, State}, + processor::Processor, + }, ui::{ control_unit::ControlPanel, editor::Editor, interface::EmulatorUI, memory_inspector::MemoryInspector, stack_inspector::StackInspector, }, }; -fn main() -> Result<(), eframe::Error> { - // Initialize Channels +fn main() -> Result<(), Box> { + // Initialize channels and read in configuration. let (cmd_sender, cmd_receiver) = std::sync::mpsc::channel(); - let (state_sender, state_receiver) = std::sync::mpsc::channel(); + let (state_sender, state_reciever) = std::sync::mpsc::channel(); + let config = Config::load(Path::new(".dsa.emulator.toml"))?; - let mainstore = MainStore::new(); - let processor = Processor::new(Box::new(mainstore), vec![]); + // Setup RPC if enabled. + #[cfg(feature = "discord-rpc")] + let (rpc_sender, rpc_reciever) = std::sync::mpsc::channel(); - thread::spawn(move || { - run_emulator(&cmd_receiver, &state_sender, processor); - }); + #[cfg(feature = "discord-rpc")] + let rpc_client = + get_rpc_client_or_none(&config, rpc_sender, rpc_reciever)?.map(Arc::new); - // Create UI - let mut ui = EmulatorUI::new(cmd_sender.clone(), state_receiver); + #[cfg(feature = "discord-rpc")] + setup_emulator(cmd_receiver, state_sender, rpc_client); + #[cfg(not(feature = "discord-rpc"))] + setup_emulator(cmd_receiver, state_sender); - // Create UI modules - let control_unit = ControlPanel::new(cmd_sender.clone()); - ui.add_component(Box::new(control_unit)); + let ui = setup_ui(cmd_sender, state_reciever); - let mem_inspector = MemoryInspector::new(cmd_sender.clone()); - ui.add_component(Box::new(mem_inspector)); - - let stack_inspector = StackInspector::new(); - ui.add_component(Box::new(stack_inspector)); - - let editor = Editor::new(cmd_sender.clone()); - ui.add_component(Box::new(editor)); - - // Run UI + // Run UI. let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]), ..Default::default() @@ -49,5 +57,45 @@ fn main() -> Result<(), eframe::Error> { cc.egui_ctx.set_visuals(egui::Visuals::default()); Ok(Box::new(ui)) }), - ) + )?; + + Ok(()) +} + +fn setup_emulator( + cmd_receiver: Receiver, + state_sender: Sender, + #[cfg(feature = "discord-rpc")] rpc_client: Option>, +) { + let main_store = MainStore::new(); + let processor = Processor::new(Box::new(main_store), vec![]); + + thread::spawn(move || { + #[cfg(feature = "discord-rpc")] + run_emulator(&cmd_receiver, &state_sender, processor, rpc_client.as_ref()); + + #[cfg(not(feature = "discord-rpc"))] + run_emulator(&cmd_receiver, &state_sender, processor); + }); +} + +/// Creates the [`EmulatorUI`]. +fn setup_ui(cmd_sender: Sender, state_reciever: Receiver) -> EmulatorUI { + let mut ui = EmulatorUI::new(cmd_sender.clone(), state_reciever); + + // Create UI modules. + let control_unit = ControlPanel::new(cmd_sender.clone()); + + ui.add_component(Box::new(control_unit)); + + let mem_inspector = MemoryInspector::new(cmd_sender.clone()); + ui.add_component(Box::new(mem_inspector)); + + let stack_inspector = StackInspector::new(); + ui.add_component(Box::new(stack_inspector)); + + let editor = Editor::new(cmd_sender.clone()); + ui.add_component(Box::new(editor)); + + ui }