Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
2025-06-17 23:50:52 +01:00
8 changed files with 318 additions and 54 deletions
+5
View File
@@ -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
Generated
+1
View File
@@ -1052,6 +1052,7 @@ dependencies = [
"egui",
"egui_code_editor",
"rfd",
"serde",
"toml",
]
+6 -1
View File
@@ -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"]
+36
View File
@@ -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<Self, toml::de::Error> {
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))
}
}
+160 -19
View File
@@ -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<DiscordError> for DiscordRpcError {
impl From<DiscordError> 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<Client, DiscordRpcError> {
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<Message>,
/// Stored for later cleanup on Drop.
thread_handle: Option<Arc<std::thread::JoinHandle<()>>>,
}
impl RpcClient {
#[expect(clippy::unreadable_literal)]
/// Sets up the [`RpcClient`].
pub fn new(
sender: Sender<Message>,
reciever: Receiver<Message>,
) -> Result<Self, RpcClientError> {
// 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<Message>,
rpc_reciever: Receiver<Message>,
) -> Result<Option<RpcClient>, Box<dyn std::error::Error + 'static>> {
if config.misc.use_discord_rpc {
Ok(Some(RpcClient::new(rpc_sender, rpc_reciever)?))
} else {
Ok(None)
}
}
+30 -6
View File
@@ -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<Command>,
state_tx: &Sender<State>,
mut processor: Processor,
#[cfg(feature = "discord-rpc")] rpc_client: Option<&Arc<RpcClient>>,
) {
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 {
+6 -2
View File
@@ -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(
+74 -26
View File
@@ -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<dyn std::error::Error>> {
// 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<Command>,
state_sender: Sender<State>,
#[cfg(feature = "discord-rpc")] rpc_client: Option<Arc<RpcClient>>,
) {
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<Command>, state_reciever: Receiver<State>) -> 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
}