emulator: get RPC working w/ Cargo features
This commit is contained in:
@@ -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
@@ -1052,6 +1052,7 @@ dependencies = [
|
||||
"egui",
|
||||
"egui_code_editor",
|
||||
"rfd",
|
||||
"serde",
|
||||
"toml",
|
||||
]
|
||||
|
||||
|
||||
+6
-1
@@ -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"]
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user