From 62811b3d15a77147132d73d785e2c8e3d96dca95 Mon Sep 17 00:00:00 2001 From: Anders Madsen Date: Mon, 29 Jan 2024 21:36:04 +0100 Subject: [PATCH 01/13] Init of Discord Rich Presence for Slippi - Currently not working --- discord-rpc/Cargo.toml | 29 +++ discord-rpc/src/config.rs | 126 ++++++++++ discord-rpc/src/discord.rs | 293 +++++++++++++++++++++++ discord-rpc/src/lib.rs | 88 +++++++ discord-rpc/src/melee.rs | 327 ++++++++++++++++++++++++++ discord-rpc/src/melee/character.rs | 122 ++++++++++ discord-rpc/src/melee/dolphin_mem.rs | 233 ++++++++++++++++++ discord-rpc/src/melee/dolphin_user.rs | 34 +++ discord-rpc/src/melee/msrb.rs | 59 +++++ discord-rpc/src/melee/multiman.rs | 9 + discord-rpc/src/melee/stage.rs | 173 ++++++++++++++ discord-rpc/src/rank.rs | 14 ++ discord-rpc/src/tray.rs | 286 ++++++++++++++++++++++ discord-rpc/src/util.rs | 23 ++ exi/Cargo.toml | 1 + exi/src/lib.rs | 25 ++ 16 files changed, 1842 insertions(+) create mode 100644 discord-rpc/Cargo.toml create mode 100644 discord-rpc/src/config.rs create mode 100644 discord-rpc/src/discord.rs create mode 100644 discord-rpc/src/lib.rs create mode 100644 discord-rpc/src/melee.rs create mode 100644 discord-rpc/src/melee/character.rs create mode 100644 discord-rpc/src/melee/dolphin_mem.rs create mode 100644 discord-rpc/src/melee/dolphin_user.rs create mode 100644 discord-rpc/src/melee/msrb.rs create mode 100644 discord-rpc/src/melee/multiman.rs create mode 100644 discord-rpc/src/melee/stage.rs create mode 100644 discord-rpc/src/rank.rs create mode 100644 discord-rpc/src/tray.rs create mode 100644 discord-rpc/src/util.rs diff --git a/discord-rpc/Cargo.toml b/discord-rpc/Cargo.toml new file mode 100644 index 0000000..ec467e3 --- /dev/null +++ b/discord-rpc/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "discord-rpc" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +directories = "5.0.0" +discord-rich-presence = "0.2.3" +encoding_rs = "0.8.32" +lazy_static = "1.4.0" +num_enum = "0.6.1" +open = "4.1.0" +preferences = { git = "https://github.com/andybarron/preferences-rs" } +regex = "1.8.1" +reqwest = { version = "0.11.16", features = ["json"] } +ruspiro-singleton = "0.4.3" +serde = { version = "1.0.160", features = ["derive"] } +serde_derive = "1.0.160" +serde_json = "1.0.96" +single-instance = "0.3.3" +structstruck = "0.4.1" +strum = "0.24.1" +strum_macros = "0.24.3" +tokio = { version = "1.27.0", features = ["full"] } +tokio-util = "0.7.7" +trayicon = "0.1.3" +windows = { version = "0.48.0", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_System_Memory", "Win32_System_Diagnostics_ToolHelp", "Win32_System_Diagnostics_Debug", "Win32_System_ProcessStatus", "Win32_UI_WindowsAndMessaging"] } diff --git a/discord-rpc/src/config.rs b/discord-rpc/src/config.rs new file mode 100644 index 0000000..dd7c7d2 --- /dev/null +++ b/discord-rpc/src/config.rs @@ -0,0 +1,126 @@ +use preferences::{AppInfo, Preferences}; +use ruspiro_singleton::Singleton; + +use crate::melee::SlippiMenuScene; + +pub const APP_INFO: AppInfo = AppInfo { + name: "conf", + author: "Slippi Discord Integration", +}; +const PREFS_KEY: &str = "app_config"; + +pub static CONFIG: Singleton = Singleton::lazy(&|| { + match AppConfig::load(&APP_INFO, PREFS_KEY) { + Ok(cfg) => cfg, + Err(_) => AppConfig::default() + } +}); + +structstruck::strike! { + #[strikethrough[derive(Serialize, Deserialize, PartialEq, Debug)]] + pub struct AppConfig { + pub global: struct { + pub show_in_game_character: bool, + pub show_in_game_time: bool + }, + pub slippi: struct { + pub enabled: bool, + pub show_queueing: bool, + pub show_opponent_name: bool, + pub ranked: struct { + pub enabled: bool, + pub show_rank: bool, + pub show_view_ranked_profile_button: bool, + pub show_score: bool + }, + pub unranked: struct { + pub enabled: bool + }, + pub direct: struct { + pub enabled: bool + }, + pub teams: struct { + pub enabled: bool + } + }, + pub uncle_punch: struct { + pub enabled: bool + }, + pub vs_mode: struct { + pub enabled: bool + }, + pub training_mode: struct { + pub enabled: bool + }, + pub stadium: struct { + pub enabled: bool, + pub hrc: struct { + pub enabled: bool + }, + pub btt: struct { + pub enabled: bool, + pub show_stage_name: bool + }, + pub mmm: struct { + pub enabled: bool + } + } + } +} + +impl Default for AppConfig { + fn default() -> Self { + AppConfig { + global: Global { + show_in_game_character: true, + show_in_game_time: true + }, + slippi: Slippi { + enabled: true, + show_queueing: true, + show_opponent_name: true, + ranked: Ranked { + enabled: true, + show_rank: true, + show_view_ranked_profile_button: true, + show_score: true + }, + unranked: Unranked { enabled: true }, + direct: Direct { enabled: true }, + teams: Teams { enabled: true } + }, + uncle_punch: UnclePunch { enabled: true }, + vs_mode: VsMode { enabled: true }, + training_mode: TrainingMode { enabled: true }, + stadium: Stadium { + enabled: true, + hrc: Hrc { + enabled: true + }, + btt: Btt { + enabled: true, + show_stage_name: true + }, + mmm: Mmm { + enabled: true + } + } + } + } +} + +pub fn write_config(val: &AppConfig) { + let _ = val.save(&APP_INFO, PREFS_KEY); +} + +// Utility implementations +impl SlippiMenuScene { + pub fn is_enabled(&self, c: &AppConfig) -> bool { + match *self { + SlippiMenuScene::Ranked => c.slippi.ranked.enabled, + SlippiMenuScene::Unranked => c.slippi.unranked.enabled, + SlippiMenuScene::Direct => c.slippi.direct.enabled, + SlippiMenuScene::Teams => c.slippi.teams.enabled + } + } +} \ No newline at end of file diff --git a/discord-rpc/src/discord.rs b/discord-rpc/src/discord.rs new file mode 100644 index 0000000..f0e29a7 --- /dev/null +++ b/discord-rpc/src/discord.rs @@ -0,0 +1,293 @@ +use discord_rich_presence::{activity::{self, Timestamps, Button}, DiscordIpc, DiscordIpcClient}; + +use crate::{util::current_unix_time, melee::{stage::{MeleeStage, OptionalMeleeStage}, character::{MeleeCharacter, OptionalMeleeCharacter}, MeleeScene, SlippiMenuScene, dolphin_user::get_connect_code}, rank, config::CONFIG}; +use crate::util; + +#[derive(Debug, PartialEq, Clone)] +pub enum DiscordClientRequestType { + Clear, + Queue, + Game, + Mainmenu, + Idle, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum DiscordClientRequestTimestampMode { + None, + Start, + Static, // like Start, but we never update even if the timestamp changes. Used for non-ingame actions. + End +} + +#[derive(Debug, Clone)] +pub struct DiscordClientRequestTimestamp { + pub mode: DiscordClientRequestTimestampMode, + pub timestamp: i64 +} + +impl DiscordClientRequestTimestamp { + pub fn none() -> Self { Self { mode: DiscordClientRequestTimestampMode::None, timestamp: 0 } } +} + +// we ignore this field +impl PartialEq for DiscordClientRequestTimestamp { + fn eq(&self, o: &Self) -> bool { + // if the game was in pause for too long, resynchronize by saying that this payload is not the same as the other. + // To respect the rate limit, we choose a relatively high amount of seconds + self.mode == DiscordClientRequestTimestampMode::Static || self.timestamp.abs_diff(o.timestamp) < 15 + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct DiscordClientRequest { + pub req_type: DiscordClientRequestType, + pub scene: Option, + pub stage: OptionalMeleeStage, + pub character: OptionalMeleeCharacter, + pub mode: String, + pub timestamp: DiscordClientRequestTimestamp, + pub opp_name: Option +} + +impl Default for DiscordClientRequest { + fn default() -> Self { + DiscordClientRequest { + req_type: DiscordClientRequestType::Clear, + scene: None, + stage: OptionalMeleeStage(None), + character: OptionalMeleeCharacter(None), + mode: "".into(), + timestamp: DiscordClientRequestTimestamp { + mode: DiscordClientRequestTimestampMode::Static, + timestamp: current_unix_time(), + }, + opp_name: None + } + } +} + +impl DiscordClientRequest { + pub fn clear() -> Self { Default::default() } + pub fn queue(scene: Option, character: Option) -> Self { + Self { + req_type: DiscordClientRequestType::Queue, + scene, + character: OptionalMeleeCharacter(character), + ..Default::default() + } + } + pub fn idle(scene: Option, character: Option) -> Self { + Self { + req_type: DiscordClientRequestType::Idle, + scene, + character: OptionalMeleeCharacter(character), + ..Default::default() + } + } + pub fn main_menu() -> Self { + Self { + req_type: DiscordClientRequestType::Mainmenu, + ..Default::default() + } + } + pub fn game(stage: Option, character: Option, mode: MeleeScene, timestamp: DiscordClientRequestTimestamp, opp_name: Option) -> Self { + Self { + req_type: DiscordClientRequestType::Game, + stage: OptionalMeleeStage(stage), + character: OptionalMeleeCharacter(character), + mode: mode.to_string(), + timestamp, + opp_name, + ..Default::default() + } + } +} + +pub struct DiscordClient { + client: DiscordIpcClient +} + +impl DiscordClient { + pub fn clear(&mut self) { + self.client.clear_activity().unwrap(); + } + pub async fn queue(&mut self, scene: Option, character: OptionalMeleeCharacter) { + let mut large_image = "slippi".into(); + let mut large_text = "Searching".into(); + let mut buttons = Vec::with_capacity(1); + let mut _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = "".into(); + if CONFIG.with_ref(|c| c.slippi.ranked.show_rank) { + let connect_code_opt = get_connect_code(); + if connect_code_opt.is_some() { + let connect_code = connect_code_opt.unwrap(); + if connect_code.is_valid() { + let fmt_code = connect_code.as_url(); + + let rank_info = rank::get_rank_info(fmt_code.as_str()).await.unwrap(); + large_image = rank_info.name.to_lowercase().replace(" ", "_"); + large_text = format!("{} | {} ELO", rank_info.name, util::round(rank_info.elo, 2)); + if CONFIG.with_ref(|c| c.slippi.ranked.show_view_ranked_profile_button) { + _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = format!("https://slippi.gg/user/{}", fmt_code.as_str()); + buttons.push(Button::new("Get Slippi", "https://slippi.gg/")); + buttons.push(Button::new("View Ranked Profile", _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it.as_str())); + } + } + } + } + + self.client.set_activity( + activity::Activity::new() + .assets({ + let mut activity = activity::Assets::new(); + if !large_image.is_empty() { activity = activity.large_image(large_image.as_str()); } + if !large_text.is_empty() { activity = activity.large_text(large_text.as_str()); } + activity.small_image(character.as_discord_resource().as_str()) + .small_text(character.to_string().as_str()) + }) + .buttons(buttons) + .timestamps(self.current_timestamp()) + .details(scene.and_then(|v| Some(v.to_string())).unwrap_or("".into()).as_str()) + .state("In Queue") + ).unwrap() + + } + pub async fn main_menu(&mut self) { + let mut large_image = "slippi".into(); + let mut large_text = "Idle".into(); + let mut buttons = Vec::with_capacity(1); + let mut _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = "".into(); + if CONFIG.with_ref(|c| c.slippi.ranked.show_rank) { + let connect_code_opt = get_connect_code(); + if connect_code_opt.is_some() { + let connect_code = connect_code_opt.unwrap(); + if connect_code.is_valid() { + let fmt_code = connect_code.as_url(); + + let rank_info = rank::get_rank_info(fmt_code.as_str()).await.unwrap(); + large_image = "slippi"; + large_text = format!("{} | {} ELO", rank_info.name, util::round(rank_info.elo, 2)); + if CONFIG.with_ref(|c| c.slippi.ranked.show_view_ranked_profile_button) { + _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = format!("https://slippi.gg/user/{}", fmt_code.as_str()); + buttons.push(Button::new("Get Slippi", "https://slippi.gg/")); + buttons.push(Button::new("View Ranked Profile", _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it.as_str())); + + } + } + } + } + + self.client.set_activity( + activity::Activity::new() + .assets({ + let mut activity = activity::Assets::new(); + if !large_image.is_empty() { activity = activity.large_image(large_image); } + if !large_text.is_empty() { activity = activity.large_text(large_text.as_str()); } + activity + }) + .buttons(buttons) + .timestamps(self.current_timestamp()) + .details("Super Smash Bros. Melee") + .state("Main Menu") + ).unwrap() + } + + pub async fn idle(&mut self, scene: Option, character: OptionalMeleeCharacter) { + let mut large_image = "slippi".into(); + let mut large_text = "Idle".into(); + let mut buttons = Vec::with_capacity(1); + let mut _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = "".into(); + if CONFIG.with_ref(|c| c.slippi.ranked.show_rank) { + let connect_code_opt = get_connect_code(); + if connect_code_opt.is_some() { + let connect_code = connect_code_opt.unwrap(); + if connect_code.is_valid() { + let fmt_code = connect_code.as_url(); + + let rank_info = rank::get_rank_info(fmt_code.as_str()).await.unwrap(); + large_image = rank_info.name.to_lowercase().replace(" ", "_"); + large_text = format!("{} | {} ELO", rank_info.name, util::round(rank_info.elo, 2)); + if CONFIG.with_ref(|c| c.slippi.ranked.show_view_ranked_profile_button) { + _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = format!("https://slippi.gg/user/{}", fmt_code.as_str()); + buttons.push(Button::new("Get Slippi", "https://slippi.gg/")); + buttons.push(Button::new("View Ranked Profile", _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it.as_str())); + } + } + } + } + + self.client.set_activity( + activity::Activity::new() + .assets({ + let mut activity = activity::Assets::new(); + if !large_image.is_empty() { activity = activity.large_image(large_image.as_str()); } + if !large_text.is_empty() { activity = activity.large_text(large_text.as_str()); } + activity.small_image(character.as_discord_resource().as_str()) + .small_text(character.to_string().as_str()) + }) + .buttons(buttons) + .timestamps(self.current_timestamp()) + .details(scene.and_then(|v| Some(v.to_string())).unwrap_or("".into()).as_str()) + .state("Character Selection Screen") + ).unwrap() + + } + + pub async fn game(&mut self, stage: OptionalMeleeStage, character: OptionalMeleeCharacter, mode: String, timestamp: DiscordClientRequestTimestamp, opp_name: Option) { + let mut large_image = "slippi".into(); + let mut large_text = "Idle".into(); + let mut buttons = Vec::with_capacity(1); + let mut _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = "".into(); + if CONFIG.with_ref(|c| c.slippi.ranked.show_rank) { + let connect_code_opt = get_connect_code(); + if connect_code_opt.is_some() { + let connect_code = connect_code_opt.unwrap(); + if connect_code.is_valid() { + let fmt_code = connect_code.as_url(); + + let rank_info = rank::get_rank_info(fmt_code.as_str()).await.unwrap(); + large_image = rank_info.name.to_lowercase().replace(" ", "_"); + large_text = format!("{} | {} ELO", rank_info.name, util::round(rank_info.elo, 2)); + if CONFIG.with_ref(|c| c.slippi.ranked.show_view_ranked_profile_button) { + _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = format!("https://slippi.gg/user/{}", fmt_code.as_str()); + buttons.push(Button::new("Get Slippi", "https://slippi.gg/")); + buttons.push(Button::new("View Ranked Profile", _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it.as_str())); + } + } + } + } + self.client.set_activity( + activity::Activity::new() + .assets( + activity::Assets::new() + .large_image(stage.as_discord_resource().as_str()) + .large_text(stage.to_string().as_str()) + .small_image(character.as_discord_resource().as_str()) + .small_text(character.to_string().as_str()) + ) + .timestamps( + if timestamp.mode == DiscordClientRequestTimestampMode::None { Timestamps::new() } + else if (timestamp.mode as u8) < (DiscordClientRequestTimestampMode::End as u8) { Timestamps::new().start(timestamp.timestamp) } + else { Timestamps::new().end(timestamp.timestamp) }) + .buttons(buttons) + .details(mode.as_str()) + .state(opp_name.and_then(|n| Some(format!("Playing against {}", n))).unwrap_or("In Game".into()).as_str()) + ).unwrap() + + } + + pub fn close(&mut self) { + self.client.close().unwrap(); + } + + fn current_timestamp(&self) -> Timestamps { + Timestamps::new().start(util::current_unix_time()) + } +} + +pub fn start_client() -> Result> { + let mut client = DiscordIpcClient::new("1096595344600604772")?; + client.connect()?; + + Ok(DiscordClient { client }) +} \ No newline at end of file diff --git a/discord-rpc/src/lib.rs b/discord-rpc/src/lib.rs new file mode 100644 index 0000000..9a7ccbd --- /dev/null +++ b/discord-rpc/src/lib.rs @@ -0,0 +1,88 @@ +// TODO Sessions - each scene has a minor 0 which is the css. if you leave the major scene, the session ends, otherwise when not in-game we show when the session started +// ^ option name "Show overall game session when not in-game" +// TODO HRC & BTT Records in discord +// TODO Ranked match score, button "Viw opponent ranked profile", show details in stage striking already (in discord rich presence, signalize that you are in stage striking as well) +// TODO clean up melee.rs, move structs/enums away in coherent bundles + +//#![windows_subsystem = "windows"] +#![feature(generic_const_exprs)] + +#[macro_use] +extern crate serde_derive; + +use discord::{DiscordClientRequest, DiscordClientRequestType}; +use single_instance::SingleInstance; +use tokio_util::sync::CancellationToken; +use tokio::sync::mpsc; +use util::sleep; + +use crate::tray::MeleeTrayEvent; + +mod config; +mod discord; +mod tray; +mod rank; +mod util; +mod melee; + +#[tokio::main] +async fn main() { + let instance = SingleInstance::new("SLIPPI_DISCORD_RICH_PRESENCE_MTX").unwrap(); + assert!(instance.is_single()); + let (tx, mut rx) = mpsc::channel::(32); + let (mtx, mrx) = std::sync::mpsc::channel::(); + + let cancel_token = CancellationToken::new(); + let melee_cancel_token = cancel_token.child_token(); + tokio::spawn(async move { + loop { + let discord_tx = tx.clone(); + let tray_tx = mtx.clone(); + let c_token = melee_cancel_token.clone(); + let res = tokio::task::spawn_blocking(move || { + let mut client = melee::MeleeClient::new(); + client.run(c_token, discord_tx, tray_tx); + }).await; + match res { + Ok(_) => { /* handle successfull exit */ }, + Err(err) if err.is_panic() => { + // panic + let _ = tx.send(DiscordClientRequest::clear()).await; + println!("[ERROR] Melee Client crashed. Restarting..."); + sleep(500); + }, + Err(_) => { } + } + } + }); + + let discord_cancel_token = cancel_token.clone(); + tokio::spawn(async move { + let mut discord_client = discord::start_client().unwrap(); + + loop { + if discord_cancel_token.is_cancelled() { + break + } + let poll_res = rx.try_recv(); + if poll_res.is_ok() { + let msg = poll_res.unwrap(); + println!("{:?}", msg); + match msg.req_type { + DiscordClientRequestType::Queue => discord_client.queue(msg.scene, msg.character).await, + DiscordClientRequestType::Idle => discord_client.idle(msg.scene, msg.character).await, + DiscordClientRequestType::Game => discord_client.game(msg.stage, msg.character, msg.mode, msg.timestamp, msg.opp_name).await, + DiscordClientRequestType::Mainmenu => discord_client.main_menu().await, + DiscordClientRequestType::Clear => discord_client.clear() + } + } + + } + discord_client.close(); + }); + + tray::run_tray(mrx); // synchronous + + // cleanup + cancel_token.cancel(); +} \ No newline at end of file diff --git a/discord-rpc/src/melee.rs b/discord-rpc/src/melee.rs new file mode 100644 index 0000000..bc03c9c --- /dev/null +++ b/discord-rpc/src/melee.rs @@ -0,0 +1,327 @@ +use std::{fmt::Display}; + +use num_enum::TryFromPrimitive; +use strum::{IntoEnumIterator}; +use strum_macros::{Display, EnumIter}; +use tokio_util::sync::CancellationToken; + +use crate::{discord::{DiscordClientRequest, DiscordClientRequestType, DiscordClientRequestTimestamp, DiscordClientRequestTimestampMode}, util::{current_unix_time, sleep}, melee::{stage::MeleeStage, character::MeleeCharacter}, config::{CONFIG}, tray::MeleeTrayEvent}; + +use self::{dolphin_mem::{DolphinMemory, util::R13}, msrb::MSRBOffset, multiman::MultiManVariant}; + +mod dolphin_mem; +mod msrb; +mod multiman; +pub mod stage; +pub mod character; +pub mod dolphin_user; + +// reference: https://github.com/akaneia/m-ex/blob/master/MexTK/include/match.h#L11-L14 +#[derive(PartialEq, EnumIter, Clone, Copy)] +enum TimerMode { + Countup = 3, + Countdown = 2, + Hidden = 1, + Frozen = 0, +} + +#[derive(TryFromPrimitive, Display, Debug)] +#[repr(u8)] +enum MatchmakingMode { + Idle = 0, + Initializing = 1, + Matchmaking = 2, + OpponentConnecting = 3, + ConnectionSuccess = 4, + ErrorEncountered = 5 +} + +#[derive(Debug, TryFromPrimitive, Display, PartialEq, Clone, Copy)] +#[repr(u8)] +pub enum SlippiMenuScene { + Ranked = 0, + Unranked = 1, + Direct = 2, + Teams = 3 +} + +pub struct MeleeClient { + mem: DolphinMemory, + last_payload: DiscordClientRequest, + last_tray_event: MeleeTrayEvent +} + +#[derive(PartialEq, Clone, Copy,Debug)] +pub enum MeleeScene { + MainMenu, + VsMode, + UnclePunch, + TrainingMode, + SlippiOnline(Option), + SlippiCss(Option), + HomeRunContest, + TargetTest(Option), + MultiManMelee(MultiManVariant) +} + +impl Display for MeleeScene { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + Self::MainMenu => write!(f, "Main Menu"), + Self::VsMode => write!(f, "Vs. Mode"), + Self::UnclePunch => write!(f, "UnclePunch Training Mode"), + Self::TrainingMode => write!(f, "Training Mode"), + Self::SlippiOnline(Some(scene)) => write!(f, "{}", scene), + Self::SlippiOnline(None) => write!(f, "Slippi Online"), + Self::HomeRunContest => write!(f, "Home-Run Contest"), + Self::TargetTest(stage_opt) => { + if stage_opt.is_some() && CONFIG.with_ref(|c| c.stadium.btt.show_stage_name) { + write!(f, "{}", stage_opt.unwrap()) + } else { + write!(f, "Target Test") + } + }, + Self::MultiManMelee(variant) => write!(f, "Multi-Man Melee ({})", match variant { + MultiManVariant::TenMan => "10 man", + MultiManVariant::HundredMan => "100 man", + MultiManVariant::ThreeMinute => "3 min", + MultiManVariant::FifteenMinute => "15 min", + MultiManVariant::Endless => "Endless", + MultiManVariant::Cruel => "Cruel", + }), + Self::SlippiCss(_) => unimplemented!(), + } + } +} + +impl MeleeClient { + pub fn new() -> Self { + MeleeClient { mem: DolphinMemory::new(), last_payload: DiscordClientRequest::clear(), last_tray_event: MeleeTrayEvent::Disconnected } + } + + fn get_player_port(&mut self) -> Option { self.mem.read::(R13!(0x5108)) } + fn get_slippi_player_port(&mut self) -> Option { self.mem.read_msrb(MSRBOffset::MsrbLocalPlayerIndex) } + fn get_opp_name(&mut self) -> Option { self.mem.read_msrb_string::<31>(MSRBOffset::MsrbOppName) } + fn get_player_connect_code(&mut self, port: u8) -> Option { + const PLAYER_CONNECTCODE_OFFSETS: [MSRBOffset; 4] = [MSRBOffset::MsrbP1ConnectCode, MSRBOffset::MsrbP2ConnectCode, MSRBOffset::MsrbP3ConnectCode, MSRBOffset::MsrbP4ConnectCode]; + self.mem.read_msrb_string_shift_jis::<10>(PLAYER_CONNECTCODE_OFFSETS[port as usize]) + } + fn get_character_selection(&mut self, port: u8) -> Option { + // 0x04 = character, 0x05 = skin (reference: https://github.com/bkacjios/m-overlay/blob/master/source/modules/games/GALE01-2.lua#L199-L202) + const PLAYER_SELECTION_BLOCKS: [u32; 4] = [0x8043208B, 0x80432093, 0x8043209B, 0x804320A3]; + self.mem.read::(PLAYER_SELECTION_BLOCKS[port as usize] + 0x04).and_then(|v| MeleeCharacter::try_from(v).ok()) + } + fn timer_mode(&mut self) -> TimerMode { + const MATCH_INIT: u32 = 0x8046DB68; // first byte, reference: https://github.com/akaneia/m-ex/blob/master/MexTK/include/match.h#L136 + self.mem.read::(MATCH_INIT).and_then(|v| { + for timer_mode in TimerMode::iter() { + let val = timer_mode as u8; + if v & val == val { + return Some(timer_mode); + } + } + None + }).unwrap_or(TimerMode::Countup) + } + fn game_time(&mut self) -> i64 { self.mem.read::(0x8046B6C8).and_then(|v| Some(v)).unwrap_or(0) as i64 } + fn matchmaking_type(&mut self) -> Option { + self.mem.read_msrb::(MSRBOffset::MsrbConnectionState).and_then(|v| MatchmakingMode::try_from(v).ok()) + } + fn slippi_online_scene(&mut self) -> Option { self.mem.read::(R13!(0x5060)).and_then(|v| SlippiMenuScene::try_from(v).ok()) } + /*fn game_variant(&mut self) -> Option { + const GAME_ID_ADDR: u32 = 0x80000000; + const GAME_ID_LEN: usize = 0x06; + + let game_id = self.mem.read_string::(GAME_ID_ADDR); + if game_id.is_none() { + return None; + } + return match game_id.unwrap().as_str() { + "GALE01" => Some(MeleeGameVariant::Vanilla), + "GTME01" => Some(MeleeGameVariant::UnclePunch), + _ => None + } + }*/ + + + fn get_melee_scene(&mut self) -> Option { + const MAJOR_SCENE: u32 = 0x80479D30; + const MINOR_SCENE: u32 = 0x80479D33; + let scene_tuple = (self.mem.read::(MAJOR_SCENE).unwrap_or(0), self.mem.read::(MINOR_SCENE).unwrap_or(0)); + + // Print the scene_tuple to the console + println!("Major Scene: {:?}", self.mem.read::(MAJOR_SCENE).unwrap_or(0)); + println!("Minor Scene: {:?}", self.mem.read::(MAJOR_SCENE).unwrap_or(0)); + match scene_tuple { + (0, 0) => Some(MeleeScene::MainMenu), + (1, 0) => Some(MeleeScene::MainMenu), + (1, 1) => Some(MeleeScene::MainMenu), + (2, 2) => Some(MeleeScene::VsMode), + (43, 1) => Some(MeleeScene::UnclePunch), + (28, 2) => Some(MeleeScene::TrainingMode), + (28, 28) => Some(MeleeScene::TrainingMode), + (8, 2) => Some(MeleeScene::SlippiOnline(self.slippi_online_scene())), + (8, 0) => Some(MeleeScene::SlippiCss(self.slippi_online_scene())), + (8, 8) => Some(MeleeScene::SlippiCss(self.slippi_online_scene())), + (32, 1) => Some(MeleeScene::HomeRunContest), + (15, 1) => Some(MeleeScene::TargetTest(self.get_stage())), + (33, 1) => Some(MeleeScene::MultiManMelee(MultiManVariant::TenMan)), + (34, 1) => Some(MeleeScene::MultiManMelee(MultiManVariant::HundredMan)), + (35, 1) => Some(MeleeScene::MultiManMelee(MultiManVariant::ThreeMinute)), + (36, 1) => Some(MeleeScene::MultiManMelee(MultiManVariant::FifteenMinute)), + (37, 1) => Some(MeleeScene::MultiManMelee(MultiManVariant::Endless)), + (38, 1) => Some(MeleeScene::MultiManMelee(MultiManVariant::Cruel)), + _ => None + } + } + fn get_stage(&mut self) -> Option { + self.mem.read::(0x8049E6C8 + 0x88 + 0x03).and_then(|v| MeleeStage::try_from(v).ok()) + } + fn get_character(&mut self, player_id: u8) -> Option { + const PLAYER_BLOCKS: [u32; 4] = [0x80453080, 0x80453F10, 0x80454DA0, 0x80455C30]; + self.mem.read::(PLAYER_BLOCKS[player_id as usize] + 0x07).and_then(|v| MeleeCharacter::try_from(v).ok()) + } + + pub fn run(&mut self, stop_signal: CancellationToken, discord_send: tokio::sync::mpsc::Sender, tray_send: std::sync::mpsc::Sender) { + const RUN_INTERVAL: u64 = 1000; + macro_rules! send_discord_msg { + ($req:expr) => { + if self.last_payload != $req { + let _ = discord_send.blocking_send($req); + self.last_payload = $req; + } + }; + } + + loop { + if stop_signal.is_cancelled() { + return; + } + if !self.mem.has_process() { + println!("{}", if self.mem.find_process() { "Found" } else { "Searching process..." }); + } else { + self.mem.check_process_running(); + } + + { + let has_process = self.mem.has_process(); + if has_process == (self.last_tray_event == MeleeTrayEvent::Disconnected) { + let tray_ev = if has_process { MeleeTrayEvent::Connected } else { MeleeTrayEvent::Disconnected }; + self.last_tray_event = tray_ev; + let _ = tray_send.send(tray_ev); + } + } + + CONFIG.with_ref(|c| { + // self.get_game_variant(); + let gamemode_opt: Option = self.get_melee_scene(); + + if gamemode_opt.is_some() { + let gamemode: MeleeScene = gamemode_opt.unwrap(); + + // Check if we are queueing a game + if c.slippi.enabled && c.slippi.show_queueing && match gamemode { + MeleeScene::SlippiCss(scene) => + scene.and_then(|s| Some(s.is_enabled(c))).unwrap_or(true), + _ => false + } { + match self.matchmaking_type() { + Some(MatchmakingMode::Initializing) | Some(MatchmakingMode::Matchmaking) => { + let port_op = self.get_player_port(); + if !port_op.is_none() { + let port = port_op.unwrap(); + let character = if c.global.show_in_game_character { self.get_character_selection(port) } else { Some(MeleeCharacter::Hidden) }; + match gamemode { + MeleeScene::SlippiCss(scene) => { + let request = DiscordClientRequest::queue( + scene, + character + ); + send_discord_msg!(request.clone()); + }, + _ => {/* shouldn't happen */} + } + } + } + Some(MatchmakingMode::Idle) => { + let port_op = self.get_player_port(); + if !port_op.is_none() { + let port = port_op.unwrap(); + let character = if c.global.show_in_game_character { self.get_character_selection(port) } else { Some(MeleeCharacter::Hidden) }; + match gamemode { + MeleeScene::SlippiCss(scene) => { + let request = DiscordClientRequest::idle( + scene, + character + ); + send_discord_msg!(request.clone()); + }, + _ => {/* shouldn't happen */} + } + } + } + Some(_) => { + send_discord_msg!(DiscordClientRequest::clear()); + }, // sometimes it's none, probably because the pointer indirection changes during the asynchronous memory requests + _ => {} + } + // Else, we want to see if the current game mode is enabled in the config (we're in-game) + } else if match gamemode { + + MeleeScene::MainMenu => true, + MeleeScene::SlippiCss(_) => false, // if we are in css, ignore + MeleeScene::SlippiOnline(scene) => c.slippi.enabled && + scene.and_then(|s| Some(s.is_enabled(c))).unwrap_or(true), + MeleeScene::UnclePunch => c.uncle_punch.enabled, + MeleeScene::TrainingMode => c.training_mode.enabled, + MeleeScene::VsMode => c.vs_mode.enabled, + MeleeScene::HomeRunContest => c.stadium.enabled && c.stadium.hrc.enabled, + MeleeScene::TargetTest(_) => c.stadium.enabled && c.stadium.btt.enabled, + MeleeScene::MultiManMelee(_) => c.stadium.enabled && c.stadium.mmm.enabled + } { + let game_time = self.game_time(); + let timestamp = if c.global.show_in_game_time { + DiscordClientRequestTimestamp { + mode: match self.timer_mode() { + TimerMode::Countdown => DiscordClientRequestTimestampMode::End, + TimerMode::Frozen => DiscordClientRequestTimestampMode::Static, + _ => DiscordClientRequestTimestampMode::Start + }, + timestamp: if self.timer_mode() == TimerMode::Countdown { current_unix_time() + game_time } else { current_unix_time() - game_time } + } + } else { + DiscordClientRequestTimestamp::none() + }; + let player_index = match gamemode { + MeleeScene::VsMode => self.get_player_port(), + MeleeScene::SlippiOnline(_) => self.get_slippi_player_port(), + _ => Some(0u8) // default to port 1, mostly the case in single player modes like training mode/unclepunch + }.unwrap_or(0u8); + + let request = if let MeleeScene::MainMenu = gamemode { + // For main menu, do not show character or stage + DiscordClientRequest::main_menu() + } else { + // For other game modes, construct the request normally + DiscordClientRequest::game( + match gamemode { MeleeScene::TargetTest(scene) => scene, _ => self.get_stage() }, + if c.global.show_in_game_character { self.get_character(player_index) } else { Some(MeleeCharacter::Hidden) }, + gamemode, + timestamp, + if match gamemode { MeleeScene::SlippiOnline(_) => true, _ => false } && c.slippi.show_opponent_name { self.get_opp_name() } else { None } + ) + }; + + send_discord_msg!(request.clone()); + } else { + send_discord_msg!(DiscordClientRequest::clear()); + } + } else if self.last_payload.req_type != DiscordClientRequestType::Clear { + send_discord_msg!(DiscordClientRequest::clear()); + } + }); + + sleep(RUN_INTERVAL); + } + } +} \ No newline at end of file diff --git a/discord-rpc/src/melee/character.rs b/discord-rpc/src/melee/character.rs new file mode 100644 index 0000000..a7689df --- /dev/null +++ b/discord-rpc/src/melee/character.rs @@ -0,0 +1,122 @@ +use std::fmt::Display; + +use num_enum::TryFromPrimitive; + +#[derive(Debug, Clone, Copy, PartialEq, TryFromPrimitive)] +#[repr(u8)] +pub enum MeleeCharacter { + DrMario = 0x16, + Mario = 0x08, + Luigi = 0x07, + Bowser = 0x05, + Peach = 0x0C, + Yoshi = 0x11, + DonkeyKong = 0x01, + CaptainFalcon = 0x00, + Ganondorf = 0x19, + Falco = 0x14, + Fox = 0x02, + Ness = 0x0B, + IceClimbers = 0x0E, + Kirby = 0x04, + Samus = 0x10, + Zelda = 0x12, + Sheik = 0x13, + Link = 0x06, + YoungLink = 0x15, + Pichu = 0x18, + Pikachu = 0x0D, + Jigglypuff = 0x0F, + Mewtwo = 0x0A, + MrGameAndWatch = 0x03, + Marth = 0x09, + Roy = 0x17, + Hidden = 0xFF +} + +impl MeleeCharacter { + // useful when fetching from player card character address, however remains unused for now + pub fn from_css(css_index: u8) -> Option { + match css_index { + 0 => Some(Self::DrMario), + 1 => Some(Self::Mario), + 2 => Some(Self::Luigi), + 3 => Some(Self::Bowser), + 4 => Some(Self::Peach), + 5 => Some(Self::Yoshi), + 6 => Some(Self::DonkeyKong), + 7 => Some(Self::CaptainFalcon), + 8 => Some(Self::Ganondorf), + 9 => Some(Self::Falco), + 10 => Some(Self::Fox), + 11 => Some(Self::Ness), + 12 => Some(Self::IceClimbers), + 13 => Some(Self::Kirby), + 14 => Some(Self::Samus), + 15 => Some(Self::Zelda), + 16 => Some(Self::Link), + 17 => Some(Self::YoungLink), + 18 => Some(Self::Pichu), + 19 => Some(Self::Pikachu), + 20 => Some(Self::Jigglypuff), + 21 => Some(Self::Mewtwo), + 22 => Some(Self::MrGameAndWatch), + 23 => Some(Self::Marth), + 24 => Some(Self::Roy), + _ => None + } + } +} + +impl Display for MeleeCharacter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + Self::DrMario => write!(f, "Dr. Mario"), + Self::Mario => write!(f, "Mario"), + Self::Luigi => write!(f, "Luigi"), + Self::Bowser => write!(f, "Bowser"), + Self::Peach => write!(f, "Peach"), + Self::Yoshi => write!(f, "Yoshi"), + Self::DonkeyKong => write!(f, "Donkey Kong"), + Self::CaptainFalcon => write!(f, "Captain Falcon"), + Self::Ganondorf => write!(f, "Ganondorf"), + Self::Falco => write!(f, "Falco"), + Self::Fox => write!(f, "Fox"), + Self::Ness => write!(f, "Ness"), + Self::IceClimbers => write!(f, "Ice Climbers"), + Self::Kirby => write!(f, "Kirby"), + Self::Samus => write!(f, "Samus"), + Self::Zelda => write!(f, "Zelda"), + Self::Sheik => write!(f, "Sheik"), + Self::Link => write!(f, "Link"), + Self::YoungLink => write!(f, "Young Link"), + Self::Pichu => write!(f, "Pichu"), + Self::Pikachu => write!(f, "Pikachu"), + Self::Jigglypuff => write!(f, "Jigglypuff"), + Self::Mewtwo => write!(f, "Mewtwo"), + Self::MrGameAndWatch => write!(f, "Mr. Game & Watch"), + Self::Marth => write!(f, "Marth"), + Self::Roy => write!(f, "Roy"), + Self::Hidden => write!(f, "Hidden") + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct OptionalMeleeCharacter(pub Option); +impl OptionalMeleeCharacter { + pub fn as_discord_resource(&self) -> String { + self.0.as_ref().and_then(|c| + if *c == MeleeCharacter::Hidden { Some("transparent".into()) } + else { Some(format!("char{}", (*c) as u8) ) } + ).unwrap_or("questionmark".into()) + } +} +impl Display for OptionalMeleeCharacter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &(*self).0 { + Some(v) => write!(f, "{}", v), + _ => write!(f, "Unknown character") + } + } +} diff --git a/discord-rpc/src/melee/dolphin_mem.rs b/discord-rpc/src/melee/dolphin_mem.rs new file mode 100644 index 0000000..a790b2d --- /dev/null +++ b/discord-rpc/src/melee/dolphin_mem.rs @@ -0,0 +1,233 @@ +use std::ffi::c_void; +use std::mem; +use std::str::from_utf8_unchecked; + +use encoding_rs::SHIFT_JIS; +use windows::Win32::Foundation::ERROR_PARTIAL_COPY; +use windows::Win32::Foundation::GetLastError; +use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory; +use windows::Win32::System::Memory::MEMORY_BASIC_INFORMATION; +use windows::Win32::System::Memory::VirtualQueryEx; +use windows::Win32::System::ProcessStatus::PSAPI_WORKING_SET_EX_BLOCK; +use windows::Win32::System::ProcessStatus::PSAPI_WORKING_SET_EX_INFORMATION; +use windows::Win32::System::ProcessStatus::QueryWorkingSetEx; +use windows::Win32::{System::{Diagnostics::ToolHelp::{CreateToolhelp32Snapshot, PROCESSENTRY32, TH32CS_SNAPPROCESS, Process32Next}, Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, GetExitCodeProcess}}, Foundation::{STILL_ACTIVE, HANDLE, CloseHandle}}; + +const VALID_PROCESS_NAMES: &'static [&'static str] = &["Dolphin.exe", "Slippi Dolphin.exe", "Slippi_Dolphin.exe", "DolphinWx.exe", "DolphinQt2.exe"]; +const GC_RAM_START: u32 = 0x80000000; +const GC_RAM_END: u32 = 0x81800000; +const GC_RAM_SIZE: usize = 0x2000000; +const MEM_MAPPED: u32 = 0x40000; + +pub struct DolphinMemory { + process_handle: Option, + dolphin_base_addr: Option<*mut c_void>, + dolphin_addr_size: Option +} + +impl DolphinMemory { + pub fn new() -> Self { + DolphinMemory { process_handle: None, dolphin_base_addr: None, dolphin_addr_size: None } + } + + pub fn find_process(&mut self) -> bool { + unsafe { + let mut status: u32 = 0; + let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).unwrap(); + let mut pe32 = PROCESSENTRY32 { + dwSize: mem::size_of::() as u32, + cntUsage: 0, + th32ProcessID: 0, + th32DefaultHeapID: 0, + th32ModuleID: 0, + cntThreads: 0, + th32ParentProcessID: 0, + pcPriClassBase: 0, + dwFlags: 0, + szExeFile: [0; 260] + }; + + loop { + if !Process32Next(snapshot, &mut pe32 as *mut _).as_bool() { + break; + } + let name = from_utf8_unchecked(&pe32.szExeFile); + if VALID_PROCESS_NAMES.iter().any(|&e| name.starts_with(e)) { + println!("{}", name); + let handle_res = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pe32.th32ProcessID); + if handle_res.is_ok() { + let handle = handle_res.unwrap(); + if GetExitCodeProcess(handle, &mut status as *mut _).as_bool() && status as i32 == STILL_ACTIVE.0 { + self.process_handle = Some(handle); + break; + } + } else { + // ? handle is supposed to be null so what will be closed... ported from m-overlay, see reference on the top + CloseHandle(handle_res.unwrap()); + self.process_handle = None; + } + } else { + self.process_handle = None; + } + } + CloseHandle(snapshot); + return self.has_process(); + } + } + + pub fn has_process(&self) -> bool { + self.process_handle.is_some() + } + + pub fn check_process_running(&mut self) -> bool { + if self.process_handle.is_none() { + return false; + } + + let mut status: u32 = 0; + unsafe { + if GetExitCodeProcess(self.process_handle.unwrap(), &mut status as *mut _).as_bool() && status as i32 != STILL_ACTIVE.0 { + self.reset(); + return false; + } + } + return true; + } + + pub fn read(&mut self, addr: u32) -> Option where [u8; mem::size_of::()]:{ + if !self.has_process() || (!self.has_gamecube_ram_offset() && !self.find_gamecube_ram_offset()) { + return None; + } + + let mut addr = addr; + if addr >= GC_RAM_START && addr <= GC_RAM_END { + addr -= GC_RAM_START; + } else { + println!("[MEMORY] Attempt to read from invalid address {:#08x}", addr); + return None; + } + + let raddr = self.dolphin_base_addr.unwrap() as usize + addr as usize; + let mut output = [0u8; mem::size_of::()]; + let size = mem::size_of::(); + let mut memread: usize = 0; + + unsafe { + let success = ReadProcessMemory(self.process_handle.unwrap(), raddr as *const c_void, &mut output as *mut _ as *mut c_void, size, Some(&mut memread as *mut _)); + if success.as_bool() && memread == size { + // because win32 decides to give me the output in the wrong endianness, we'll reverse it + output.reverse(); // TODO figure out if we really have to do this, i would like to avoid it if possible + return Some(mem::transmute_copy(&output)); + } else { + let err = GetLastError().0; + println!("[MEMORY] Failed reading from address {:#08X} ERROR {}", addr, err); + if err == ERROR_PARTIAL_COPY.0 { // game probably closed, reset the dolphin ram offset + self.dolphin_addr_size = None; + self.dolphin_base_addr = None; + } + return None; + } + } + } + + pub fn read_string(&mut self, addr: u32) -> Option where [(); mem::size_of::<[u8; LEN]>()]:{ + let res = self.read::<[u8; LEN]>(addr); + if res.is_none() { + return None; + } + + let mut raw = res.unwrap(); + raw.reverse(); // we apparently have to reverse it again due to how the string is gathered + + return match std::str::from_utf8(&raw) { + Ok(v) => Some(v.trim_end_matches(char::from(0)).into()), + Err(e) => { + println!("Invalid utf-8 string => {:?} | {}", res.unwrap(), e.to_string()); + None + } + }; + } + + pub fn read_string_shift_jis(&mut self, addr: u32) -> Option where [(); mem::size_of::<[u8; LEN]>()]:{ + let res = self.read::<[u8; LEN]>(addr); + if res.is_none() { + return None; + } + + let mut raw = res.unwrap(); + raw.reverse(); // we apparently have to reverse it again due to how the string is gathered + + let (dec_res, _enc, errors) = SHIFT_JIS.decode(&raw); + if errors { + println!("Invalid shift-jis string => {:?}", res.unwrap()) + } + return Some(dec_res.as_ref().trim_end_matches(char::from(0)).to_string()); + } + + pub fn pointer_indirection(&mut self, addr: u32, amount: u32) -> Option { + let mut curr = self.read::(addr); + for n in 2..=amount { + if curr.is_none() { + return None; + } + curr = self.read::(curr.unwrap()); + } + curr + } + + /*pub fn write(&self) { + + }*/ + + fn find_gamecube_ram_offset(&mut self) -> bool { + if !self.has_process() { + return false; + } + + unsafe { + let mut info: MEMORY_BASIC_INFORMATION = Default::default(); + let mut address: usize = 0; + + while VirtualQueryEx(self.process_handle.unwrap(), Some(address as *const c_void), &mut info as *mut _, mem::size_of::()) == mem::size_of::() { + address = address + info.RegionSize / mem::size_of::(); + // Dolphin stores the GameCube RAM address space in 32MB chunks. + // Extended memory override can allow up to 64MB. + if info.RegionSize >= GC_RAM_SIZE && info.RegionSize % GC_RAM_SIZE == 0 && info.Type.0 == MEM_MAPPED { + let mut wsinfo = PSAPI_WORKING_SET_EX_INFORMATION { + VirtualAddress: 0 as *mut c_void, + VirtualAttributes: PSAPI_WORKING_SET_EX_BLOCK { Flags: 0 } + }; + wsinfo.VirtualAddress = info.BaseAddress; + + if QueryWorkingSetEx(self.process_handle.unwrap(), &mut wsinfo as *mut _ as *mut c_void, mem::size_of::().try_into().unwrap()).as_bool() { + if (wsinfo.VirtualAttributes.Flags & 1) == 1 && info.BaseAddress != 0 as *mut c_void { + self.dolphin_base_addr = Some(info.BaseAddress); + self.dolphin_addr_size = Some(info.RegionSize); + + println!("Dolphin Base Address: {:?}", self.dolphin_base_addr); + println!("Dolphin Address Size: {:?}", self.dolphin_addr_size); + return true; + } + } + } + } + } + + return false; + } + + fn has_gamecube_ram_offset(&self) -> bool { + self.dolphin_base_addr.is_some() + } + + fn reset(&mut self) { + self.process_handle = None; + self.dolphin_base_addr = None; + self.dolphin_addr_size = None; + } +} + +pub mod util { + macro_rules! R13 {($offset:expr) => { 0x804db6a0 - $offset }} + pub(crate) use R13; +} diff --git a/discord-rpc/src/melee/dolphin_user.rs b/discord-rpc/src/melee/dolphin_user.rs new file mode 100644 index 0000000..a9ca5f9 --- /dev/null +++ b/discord-rpc/src/melee/dolphin_user.rs @@ -0,0 +1,34 @@ +use std::fs; + +use lazy_static::lazy_static; +use regex::Regex; +use serde_json::Value; +use crate::util::get_appdata_file; + +pub struct ConnectCode(String); +impl ConnectCode { + pub fn is_valid(&self) -> bool { + lazy_static! { + static ref RE: Regex = Regex::new("^([A-Za-z0-9])+#[0-9]{1,6}$").unwrap(); + } + RE.is_match(self.0.as_str()) + } + + pub fn as_url(&self) -> String { + self.0.to_lowercase().replace("#", "-") + } +} + +pub fn get_connect_code() -> Option { + if let Some(user_json_path) = get_appdata_file("Slippi Launcher/netplay/User/Slippi/user.json") { + if user_json_path.is_file() && user_json_path.exists() { + return fs::read_to_string(user_json_path).ok().and_then(|data| { + match serde_json::from_str::(data.as_str()) { + Ok(data) => data["connectCode"].as_str().and_then(|v| Some(ConnectCode(v.into()))), + _ => None + } + }); + } + } + None +} \ No newline at end of file diff --git a/discord-rpc/src/melee/msrb.rs b/discord-rpc/src/melee/msrb.rs new file mode 100644 index 0000000..2af7924 --- /dev/null +++ b/discord-rpc/src/melee/msrb.rs @@ -0,0 +1,59 @@ +use std::mem; + +use super::dolphin_mem::DolphinMemory; + +const MATCH_STRUCT_LEN: isize = 0x138; + +// reference: https://github.com/project-slippi/slippi-ssbm-asm/blob/0be644aff85986eae17e96f4c98b3342ab087d05/Online/Online.s#L311-L344 +#[derive(Clone, Copy)] +pub enum MSRBOffset { + MsrbConnectionState = 0, // u8, matchmaking state defined above + MsrbIsLocalPlayerReady = Self::MsrbConnectionState as isize + 1, // bool + MsrbIsRemotePlayerReady = Self::MsrbIsLocalPlayerReady as isize + 1, // bool + MsrbLocalPlayerIndex = Self::MsrbIsRemotePlayerReady as isize + 1, // u8 + MsrbRemotePlayerIndex = Self::MsrbLocalPlayerIndex as isize + 1, // u8s + MsrbRngOffset = Self::MsrbRemotePlayerIndex as isize + 1, // u32 + MsrbDelayFrames = Self::MsrbRngOffset as isize + 4, // u8 + MsrbUserChatmsgId = Self::MsrbDelayFrames as isize + 1, // u8 + MsrbOppChatmsgId = Self::MsrbUserChatmsgId as isize + 1, // u8 + MsrbChatmsgPlayerIndex = Self::MsrbOppChatmsgId as isize + 1, // u8 + MsrbVsLeftPlayers = Self::MsrbChatmsgPlayerIndex as isize + 1, // u32 player ports 0xP1P2P3PN + MsrbVsRightPlayers = Self::MsrbVsLeftPlayers as isize + 4, // u32 player ports 0xP1P2P3PN + MsrbLocalName = Self::MsrbVsRightPlayers as isize + 4, // char[31] + MsrbP1Name = Self::MsrbLocalName as isize + 31, // char[31] + MsrbP2Name = Self::MsrbP1Name as isize + 31, // char[31] + MsrbP3Name = Self::MsrbP2Name as isize + 31, // char[31] + MsrbP4Name = Self::MsrbP3Name as isize + 31, // char[31] + MsrbOppName = Self::MsrbP4Name as isize + 31, // char[31] + MsrbP1ConnectCode = Self::MsrbOppName as isize + 31, // char[10] hashtag is shift-jis + MsrbP2ConnectCode = Self::MsrbP1ConnectCode as isize + 10, // char[10] hashtag is shift-jis + MsrbP3ConnectCode = Self::MsrbP2ConnectCode as isize + 10, // char[10] hashtag is shift-jis + MsrbP4ConnectCode = Self::MsrbP3ConnectCode as isize + 10, // char[10] hashtag is shift-jis + MsrbP1SlippiUid = Self::MsrbP4ConnectCode as isize + 10, // char[29] + MsrbP2SlippiUid = Self::MsrbP1SlippiUid as isize + 29, // char[29] + MsrbP3SlippiUid = Self::MsrbP2SlippiUid as isize + 29, // char[29] + MsrbP4SlippiUid = Self::MsrbP3SlippiUid as isize + 29, // char[29] + MsrbErrorMsg = Self::MsrbP4SlippiUid as isize + 29, // char[241] + ErrorMessageLen = 241, + MsrbGameInfoBlock = Self::MsrbErrorMsg as isize + Self::ErrorMessageLen as isize, // MATCH_STRUCT_LEN + MsrbMatchId = Self::MsrbGameInfoBlock as isize + MATCH_STRUCT_LEN, // char[51] + MsrbSize = Self::MsrbMatchId as isize + 51, +} + +impl DolphinMemory { + fn msrb_ptr(&mut self) -> Option { + const CSSDT_BUF_ADDR: u32 = 0x80005614; // reference: https://github.com/project-slippi/slippi-ssbm-asm/blob/0be644aff85986eae17e96f4c98b3342ab087d05/Online/Online.s#L31 + self.pointer_indirection(CSSDT_BUF_ADDR, 2) + } + pub fn read_msrb(&mut self, offset: MSRBOffset) -> Option where [u8; mem::size_of::()]: { + self.msrb_ptr().and_then(|ptr| self.read::(ptr + offset as u32)) + } + + pub fn read_msrb_string(&mut self, offset: MSRBOffset) -> Option where [u8; mem::size_of::<[u8; LEN]>()]: { + self.msrb_ptr().and_then(|ptr| self.read_string::(ptr + offset as u32)) + } + + pub fn read_msrb_string_shift_jis(&mut self, offset: MSRBOffset) -> Option where [u8; mem::size_of::<[u8; LEN]>()]: { + self.msrb_ptr().and_then(|ptr| self.read_string_shift_jis::(ptr + offset as u32)) + } +} diff --git a/discord-rpc/src/melee/multiman.rs b/discord-rpc/src/melee/multiman.rs new file mode 100644 index 0000000..0aa6999 --- /dev/null +++ b/discord-rpc/src/melee/multiman.rs @@ -0,0 +1,9 @@ +#[derive(PartialEq, Clone, Copy, Debug)] +pub enum MultiManVariant { + TenMan, + HundredMan, + ThreeMinute, + FifteenMinute, + Endless, + Cruel +} \ No newline at end of file diff --git a/discord-rpc/src/melee/stage.rs b/discord-rpc/src/melee/stage.rs new file mode 100644 index 0000000..fdb4c4e --- /dev/null +++ b/discord-rpc/src/melee/stage.rs @@ -0,0 +1,173 @@ +use std::fmt::Display; + +use num_enum::TryFromPrimitive; + +use super::character::MeleeCharacter; + +#[derive(Debug, PartialEq, Copy, Clone, TryFromPrimitive)] +#[repr(u8)] +pub enum MeleeStage { + // Dummy, (unused) + // Test, (unused) + Castle = 2, + Rcruise, + Kongo, + Garden, + Greatbay, + Shrine, + Zebes, + Kraid, + Story, + Yoster, + Izumi, + Greens, + Corneria, + Venom, + PStad, + Pura, + MuteCity, + BigBlue, + Onett, + Fourside, + IceMt, + // IceTop, (unused) + Mk1 = 24, + Mk2, + Akaneia, + FlatZone, + OldPu, + OldStory, + OldKongo, + // AdvKraid, (unused) + // AdvShrine, (unused) + // AdvZr, (unused) + // AdvBr, (unused) + // AdvTe, (unused) + Battle = 36, + FD, + + HomeRunStadium = 67, + + MarioTargetTest = 40, + CaptainFalconTargetTest, + YoungLinkTargetTest, + DonkeyKongTargetTest, + DrMarioTargetTest, + FalcoTargetTest, + FoxTargetTest, + IceClimbersTargetTest, + KirbyTargetTest, + BowserTargetTest, + LinkTargetTest, + LuigiTargetTest, + MarthTargetTest, + MewtwoTargetTest, + NessTargetTest, + PeachTargetTest, + PichuTargetTest, + PikachuTargetTest, + JigglypuffTargetTest, + SamusTargetTest, + SheikTargetTest, + YoshiTargetTest, + ZeldaTargetTest, + MrGameAndWatchTargetTest, + RoyTargetTest, + GanondorfTargetTest, +} + +impl MeleeStage { + fn is_target_test(&self) -> bool { + *self as u8 >= MeleeStage::MarioTargetTest as u8 && *self as u8 <= MeleeStage::GanondorfTargetTest as u8 + } +} + +impl Display for MeleeStage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + // Self::Dummy => write!(f, "Dummy"), + // Self::Test => write!(f, "Test"), + Self::Castle => write!(f, "Princess Peach's Castle"), + Self::Rcruise => write!(f, "Rainbow Cruise"), + Self::Kongo => write!(f, "Kongo Jungle"), + Self::Garden => write!(f, "Jungle Japes"), + Self::Greatbay => write!(f, "Great Bay"), + Self::Shrine => write!(f, "Temple"), + Self::Zebes => write!(f, "Brinstar"), + Self::Kraid => write!(f, "Brinstar Depths"), + Self::Story => write!(f, "Yoshi's Story"), + Self::Yoster => write!(f, "Yoshi's Island"), + Self::Izumi => write!(f, "Fountain of Dreams"), + Self::Greens => write!(f, "Green Greens"), + Self::Corneria => write!(f, "Corneria"), + Self::Venom => write!(f, "Venom"), + Self::PStad => write!(f, "Pokemon Stadium"), + Self::Pura => write!(f, "Poke Floats"), + Self::MuteCity => write!(f, "Mute City"), + Self::BigBlue => write!(f, "Big Blue"), + Self::Onett => write!(f, "Onett"), + Self::Fourside => write!(f, "Fourside"), + Self::IceMt => write!(f, "IcicleMountain"), + // Self::IceTop => write!(f, "Icetop"), + Self::Mk1 => write!(f, "Mushroom Kingdom"), + Self::Mk2 => write!(f, "Mushroom Kingdom II"), + Self::Akaneia => write!(f, "Akaneia"), + Self::FlatZone => write!(f, "Flat Zone"), + Self::OldPu => write!(f, "Dream Land"), + Self::OldStory => write!(f, "Yoshi's Island (N64)"), + Self::OldKongo => write!(f, "Kongo Jungle (N64)"), + Self::Battle => write!(f, "Battlefield"), + Self::FD => write!(f, "Final Destination"), + Self::HomeRunStadium => write!(f, "Home-Run Stadium"), + + Self::DrMarioTargetTest => write!(f, "Target Test ({})", MeleeCharacter::DrMario), + Self::MarioTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Mario), + Self::LuigiTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Luigi), + Self::BowserTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Bowser), + Self::PeachTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Peach), + Self::YoshiTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Yoshi), + Self::DonkeyKongTargetTest => write!(f, "Target Test ({})", MeleeCharacter::DonkeyKong), + Self::CaptainFalconTargetTest => write!(f, "Target Test ({})", MeleeCharacter::CaptainFalcon), + Self::GanondorfTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Ganondorf), + Self::FalcoTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Falco), + Self::FoxTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Fox), + Self::NessTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Ness), + Self::IceClimbersTargetTest => write!(f, "Target Test ({})", MeleeCharacter::IceClimbers), + Self::KirbyTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Kirby), + Self::SamusTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Samus), + Self::SheikTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Sheik), + Self::ZeldaTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Zelda), + Self::LinkTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Link), + Self::YoungLinkTargetTest => write!(f, "Target Test ({})", MeleeCharacter::YoungLink), + Self::PichuTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Pichu), + Self::PikachuTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Pikachu), + Self::JigglypuffTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Jigglypuff), + Self::MewtwoTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Mewtwo), + Self::MrGameAndWatchTargetTest => write!(f, "Target Test ({})", MeleeCharacter::MrGameAndWatch), + Self::MarthTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Marth), + Self::RoyTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Roy), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct OptionalMeleeStage(pub Option); +impl OptionalMeleeStage { + pub fn as_discord_resource(&self) -> String { + self.0.as_ref().and_then(|c| { + if c.is_target_test() { + Some("stagebtt".into()) + } else { + Some(format!("stage{}", (*c) as u8)) + } + }).unwrap_or("questionmark".into()) + } +} +impl Display for OptionalMeleeStage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &(*self).0 { + Some(v) => write!(f, "{}", v), + _ => write!(f, "Unknown stage") + } + } +} \ No newline at end of file diff --git a/discord-rpc/src/rank.rs b/discord-rpc/src/rank.rs new file mode 100644 index 0000000..9e57cf3 --- /dev/null +++ b/discord-rpc/src/rank.rs @@ -0,0 +1,14 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct RankInfo { + #[serde(alias = "rank")] + pub name: String, + #[serde(alias = "rating")] + pub elo: f32 +} + +pub async fn get_rank_info(code: &str) -> Result> { + let res = reqwest::get(format!("http://slprank.com/rank/{}?raw", code)).await?; + Ok(res.json::().await?) +} \ No newline at end of file diff --git a/discord-rpc/src/tray.rs b/discord-rpc/src/tray.rs new file mode 100644 index 0000000..f693111 --- /dev/null +++ b/discord-rpc/src/tray.rs @@ -0,0 +1,286 @@ +use std::{mem::MaybeUninit, sync::{atomic::{AtomicBool, self}, Arc, mpsc::Receiver}}; + +use trayicon::{TrayIconBuilder, MenuBuilder}; +use windows::Win32::UI::WindowsAndMessaging::{TranslateMessage, DispatchMessageA, PeekMessageA, PM_REMOVE}; + +use crate::{config::{CONFIG, AppConfig, write_config, APP_INFO}, util::get_appdata_file}; + +use {std::sync::mpsc}; + +struct ExtendedMenuBuilder(MenuBuilder); +impl ExtendedMenuBuilder { + fn new() -> ExtendedMenuBuilder { + ExtendedMenuBuilder(MenuBuilder::::new()) + } + fn checkable(self, name: &str, is_checked: bool, id: TrayEvents) -> Self { + ExtendedMenuBuilder(self.0.checkable(name, is_checked, id)) + } + // checkable with enabled check + fn cwec(self, name: &str, is_checked: bool, id: TrayEvents, enable: &[bool]) -> Self { + ExtendedMenuBuilder(self.0.with(trayicon::MenuItem::Checkable { + id, + name: name.into(), + disabled: enable.iter().any(|v| !v), + is_checked, + icon: None + })) + } + fn submenu(self, name: &str, menu: MenuBuilder) -> Self { + ExtendedMenuBuilder(self.0.submenu(name, menu)) + } +} +impl From for MenuBuilder { + fn from(value: ExtendedMenuBuilder) -> Self { + value.0 + } +} + +#[derive(PartialEq, Clone, Copy)] +pub enum MeleeTrayEvent { + Connected, + Disconnected +} + +#[derive(Clone, Eq, PartialEq, Debug)] +enum TrayEvents { + _Unused, + + // Global + ShowInGameCharacter, + ShowInGameTime, + + // Slippi + EnableSlippi, + SlippiShowQueueing, + SlippiShowOpponentName, + + SlippiEnableRanked, + SlippiRankedShowRank, + SlippiRankedShowViewRankedProfileButton, + SlippiRankedShowScore, + + SlippiEnableUnranked, + + SlippiEnableDirect, + + SlippiEnableTeams, + + // Unclepunch + EnableUnclePunch, + + // Training Mode + EnableTrainingMode, + + // Vs. Mode + EnableVsMode, + + // Stadium + EnableStadium, + + StadiumEnableHRC, + + StadiumEnableBTT, + StadiumBTTShowStageName, + + StadiumEnableMMM, + + // Miscallaneous + OpenConfig, + Quit, +} + +fn build_menu(melee_connected: &Arc) -> MenuBuilder { + CONFIG.with_ref(|c| { + MenuBuilder::new() + .with(trayicon::MenuItem::Item { + id: TrayEvents::_Unused, + name: if melee_connected.load(atomic::Ordering::Relaxed) { "✔️ Connected to Dolphin process" } else { "❌ Searching for Dolphin process..." }.into(), + disabled: true, + icon: None + }) + .separator() + .submenu( + "Global", + MenuBuilder::new() + .checkable("Show Character", c.global.show_in_game_character, TrayEvents::ShowInGameCharacter) + .checkable("Show In-Game Time", c.global.show_in_game_time, TrayEvents::ShowInGameTime) + ) + .submenu( + "Slippi Online", + ExtendedMenuBuilder::new() + .checkable("Enabled", c.slippi.enabled, TrayEvents::EnableSlippi) + .cwec("Show activity when searching", c.slippi.show_queueing, TrayEvents::SlippiShowQueueing, &[c.slippi.enabled]) + .cwec("Show opponent name", c.slippi.show_opponent_name, TrayEvents::SlippiShowOpponentName, &[c.slippi.enabled]) + .submenu( + "Ranked", + ExtendedMenuBuilder::new() + .cwec("Enabled", c.slippi.ranked.enabled, TrayEvents::SlippiEnableRanked, &[c.slippi.enabled]) + .cwec("Show rank", c.slippi.ranked.show_rank, TrayEvents::SlippiRankedShowRank, &[c.slippi.enabled, c.slippi.ranked.enabled]) + .cwec("Show \"View Ranked Profile\" button", c.slippi.ranked.show_view_ranked_profile_button, TrayEvents::SlippiRankedShowViewRankedProfileButton, &[c.slippi.enabled, c.slippi.ranked.enabled]) + .cwec("Show match score", c.slippi.ranked.show_score, TrayEvents::SlippiRankedShowScore, &[c.slippi.enabled, c.slippi.ranked.enabled]) + .into() + ) + .submenu( + "Unranked", + ExtendedMenuBuilder::new() + .cwec("Enabled", c.slippi.unranked.enabled, TrayEvents::SlippiEnableUnranked, &[c.slippi.enabled]) + .into() + ) + .submenu( + "Direct", + ExtendedMenuBuilder::new() + .cwec("Enabled", c.slippi.direct.enabled, TrayEvents::SlippiEnableDirect, &[c.slippi.enabled]) + .into() + ) + .submenu( + "Teams", + ExtendedMenuBuilder::new() + .cwec("Enabled", c.slippi.teams.enabled, TrayEvents::SlippiEnableTeams, &[c.slippi.enabled]) + .into() + ) + .into() + ) + .submenu( + "UnclePunch", + MenuBuilder::new() + .checkable("Enabled", c.uncle_punch.enabled, TrayEvents::EnableUnclePunch) + ) + .submenu( + "Training Mode", + MenuBuilder::new() + .checkable("Enabled", c.training_mode.enabled, TrayEvents::EnableTrainingMode) + ) + .submenu( + "Vs. Mode", + MenuBuilder::new() + .checkable("Enabled", c.vs_mode.enabled, TrayEvents::EnableVsMode) + ) + .submenu( + "Stadium", + ExtendedMenuBuilder::new() + .checkable("Enabled", c.stadium.enabled, TrayEvents::EnableStadium) + .submenu( + "Home-Run Contest", + ExtendedMenuBuilder::new() + .cwec("Enabled", c.stadium.hrc.enabled, TrayEvents::StadiumEnableHRC, &[c.stadium.enabled]) + .into() + ) + .submenu( + "Target Test", + ExtendedMenuBuilder::new() + .cwec("Enabled", c.stadium.btt.enabled, TrayEvents::StadiumEnableBTT, &[c.stadium.enabled]) + .cwec("Show stage name", c.stadium.btt.show_stage_name, TrayEvents::StadiumBTTShowStageName, &[c.stadium.enabled]) + .into() + ) + .submenu( + "Multi-Man Melee", + ExtendedMenuBuilder::new() + .cwec("Enabled", c.stadium.mmm.enabled, TrayEvents::StadiumEnableMMM, &[c.stadium.enabled]) + .into() + ) + .into() + ) + .separator() + .item("Open Configuration File", TrayEvents::OpenConfig) + .item("Quit", TrayEvents::Quit) + }) +} + +pub fn run_tray(mrx: Receiver) { + let melee_connected = Arc::new(AtomicBool::new(false)); + + let (s, r) = mpsc::channel::(); + let icon_raw = include_bytes!("../assets/icon.ico"); + + let mut tray_icon = TrayIconBuilder::new() + .sender(s) + .icon_from_buffer(icon_raw) + .tooltip("Slippi Discord Integration") + .menu( + build_menu(&melee_connected) + ) + .build() + .unwrap(); + + let should_end = Arc::new(AtomicBool::new(false)); + let shared_should_end = should_end.clone(); + std::thread::spawn(move || { + let mut update_menu = || { + tray_icon.set_menu(&build_menu(&melee_connected)).unwrap(); + }; + let mut toggle_handler = |modifier: fn(&mut AppConfig)| { + CONFIG.with_mut(|c| { modifier(c); write_config(c); }); + update_menu(); + }; + + loop { + if let Ok(melee_ev) = mrx.try_recv() { + melee_connected.store(melee_ev == MeleeTrayEvent::Connected, atomic::Ordering::Relaxed); + toggle_handler(|_|{}); + } + if let Ok(tray_ev) = r.try_recv() { + match tray_ev { + TrayEvents::ShowInGameCharacter => toggle_handler(|f| f.global.show_in_game_character = !f.global.show_in_game_character), + TrayEvents::ShowInGameTime => toggle_handler(|f| f.global.show_in_game_time = !f.global.show_in_game_time), + + TrayEvents::EnableSlippi => toggle_handler(|f| f.slippi.enabled = !f.slippi.enabled), + TrayEvents::SlippiShowQueueing => toggle_handler(|f| f.slippi.show_queueing = !f.slippi.show_queueing), + TrayEvents::SlippiShowOpponentName => toggle_handler(|f| f.slippi.show_opponent_name = !f.slippi.show_opponent_name), + + TrayEvents::SlippiEnableRanked => toggle_handler(|f| f.slippi.ranked.enabled = !f.slippi.ranked.enabled), + TrayEvents::SlippiRankedShowRank => toggle_handler(|f| f.slippi.ranked.show_rank = !f.slippi.ranked.show_rank), + TrayEvents::SlippiRankedShowViewRankedProfileButton => toggle_handler(|f| f.slippi.ranked.show_view_ranked_profile_button = !f.slippi.ranked.show_view_ranked_profile_button), + TrayEvents::SlippiRankedShowScore => toggle_handler(|f| f.slippi.ranked.show_score = !f.slippi.ranked.show_score), + + TrayEvents::SlippiEnableUnranked => toggle_handler(|f| f.slippi.unranked.enabled = !f.slippi.unranked.enabled), + + TrayEvents::SlippiEnableDirect => toggle_handler(|f| f.slippi.direct.enabled = !f.slippi.direct.enabled), + + TrayEvents::SlippiEnableTeams => toggle_handler(|f| f.slippi.teams.enabled = !f.slippi.teams.enabled), + + TrayEvents::EnableUnclePunch => toggle_handler(|f| f.uncle_punch.enabled = !f.uncle_punch.enabled), + + TrayEvents::EnableVsMode => toggle_handler(|f| f.vs_mode.enabled = !f.vs_mode.enabled), + + TrayEvents::EnableTrainingMode => toggle_handler(|f| f.training_mode.enabled = !f.training_mode.enabled), + + TrayEvents::EnableStadium => toggle_handler(|f| f.stadium.enabled = !f.stadium.enabled), + + TrayEvents::StadiumEnableHRC => toggle_handler(|f| f.stadium.hrc.enabled = !f.stadium.hrc.enabled), + + TrayEvents::StadiumEnableBTT => toggle_handler(|f| f.stadium.btt.enabled = !f.stadium.btt.enabled), + TrayEvents::StadiumBTTShowStageName => toggle_handler(|f| f.stadium.btt.show_stage_name = !f.stadium.btt.show_stage_name), + + TrayEvents::StadiumEnableMMM => toggle_handler(|f| f.stadium.mmm.enabled = !f.stadium.mmm.enabled), + + TrayEvents::OpenConfig => { + if let Some(conf_file) = get_appdata_file(format!("{}/{}/app_config.prefs.json", APP_INFO.author, APP_INFO.name).as_str()) { + if conf_file.is_file() && conf_file.exists() { + let _ = open::that(conf_file); + } + } + } + TrayEvents::Quit => { + should_end.store(true, atomic::Ordering::Relaxed); + break; + }, + TrayEvents::_Unused => {} + } + } + } + }); + // Application message loop + loop { + if shared_should_end.load(atomic::Ordering::Relaxed) { + break; + } + unsafe { + let mut msg = MaybeUninit::uninit(); + let bret = PeekMessageA(msg.as_mut_ptr(), None, 0, 0, PM_REMOVE); + if bret.as_bool() { + TranslateMessage(msg.as_ptr()); + DispatchMessageA(msg.as_ptr()); + } + } + } +} \ No newline at end of file diff --git a/discord-rpc/src/util.rs b/discord-rpc/src/util.rs new file mode 100644 index 0000000..3c3f142 --- /dev/null +++ b/discord-rpc/src/util.rs @@ -0,0 +1,23 @@ +use std::{time::{SystemTime, UNIX_EPOCH, Duration}, thread, path::PathBuf}; + +use directories::BaseDirs; + +pub fn current_unix_time() -> i64 { + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs().try_into().unwrap() +} + +pub fn sleep(millis: u64) { + thread::sleep(Duration::from_millis(millis)); +} + +pub fn round(x: f32, decimals: u32) -> f32 { + let y = 10i32.pow(decimals) as f32; + (x * y).round() / y +} + +pub fn get_appdata_file(suffix: &str) -> Option { + if let Some(base_dirs) = BaseDirs::new() { + return Some(base_dirs.config_dir().join(suffix)); + } + None +} \ No newline at end of file diff --git a/exi/Cargo.toml b/exi/Cargo.toml index 9710da9..5d1bc49 100644 --- a/exi/Cargo.toml +++ b/exi/Cargo.toml @@ -19,5 +19,6 @@ dolphin-integrations = { path = "../dolphin" } slippi-game-reporter = { path = "../game-reporter" } slippi-jukebox = { path = "../jukebox" } slippi-user = { path = "../user" } +slippi-discord-rpc = { path = "../discord-rpc" } tracing = { workspace = true } ureq = { workspace = true } diff --git a/exi/src/lib.rs b/exi/src/lib.rs index 5f1885c..5e51ee5 100644 --- a/exi/src/lib.rs +++ b/exi/src/lib.rs @@ -13,6 +13,9 @@ use dolphin_integrations::Log; use slippi_game_reporter::GameReporter; use slippi_jukebox::Jukebox; use slippi_user::UserManager; +// Addition to existing imports +use slippi_discord_rpc::DiscordClientRequest; // This assumes slippi-discord-rpc has this type or function. +use slippi_discord_rpc::DiscordClientRequestType; // Adjust this as necessary based on actual module contents. mod config; pub use config::{Config, FilePathsConfig, SCMConfig}; @@ -24,6 +27,7 @@ pub struct SlippiEXIDevice { pub game_reporter: GameReporter, pub user_manager: UserManager, pub jukebox: Option, + discord_client: Option, } pub enum JukeboxConfiguration { @@ -70,7 +74,28 @@ impl SlippiEXIDevice { jukebox: None, } } + pub fn start_discord_client(&mut self) { + if self.discord_client.is_some() { + tracing::info!(target: Log::SlippiOnline, "Discord client is already running."); + return; + } + match DiscordClient::new() { // Replace with the actual API call to create a new Discord client instance. + Ok(client) => { + self.discord_client = Some(client); + tracing::info!(target: Log::SlippiOnline, "Discord client started successfully."); + + // If needed, run the Discord client in the background, for example with a Tokio task. + // This will require the Discord client to be 'Send' if it is moved to an async block. + tokio::spawn(async move { + self.discord_client.as_mut().unwrap().run().await; + }); + } + Err(e) => { + tracing::error!(target: Log::SlippiOnline, "Failed to start Discord client due to {:?}", e); + } + } + } /// Stubbed for now, but this would get called by the C++ EXI device on DMAWrite. pub fn dma_write(&mut self, _address: usize, _size: usize) {} From 5b84e802d8870cd709807ee2212d3de14cd56b87 Mon Sep 17 00:00:00 2001 From: Anders Madsen Date: Mon, 29 Jan 2024 22:07:55 +0100 Subject: [PATCH 02/13] Updated lib.rs for discord-rpc Removing tokio (partly done) (untested) --- discord-rpc/src/lib.rs | 91 +++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/discord-rpc/src/lib.rs b/discord-rpc/src/lib.rs index 9a7ccbd..03ff2c1 100644 --- a/discord-rpc/src/lib.rs +++ b/discord-rpc/src/lib.rs @@ -3,7 +3,6 @@ // TODO HRC & BTT Records in discord // TODO Ranked match score, button "Viw opponent ranked profile", show details in stage striking already (in discord rich presence, signalize that you are in stage striking as well) // TODO clean up melee.rs, move structs/enums away in coherent bundles - //#![windows_subsystem = "windows"] #![feature(generic_const_exprs)] @@ -12,9 +11,10 @@ extern crate serde_derive; use discord::{DiscordClientRequest, DiscordClientRequestType}; use single_instance::SingleInstance; -use tokio_util::sync::CancellationToken; -use tokio::sync::mpsc; +use std::sync::{mpsc, Arc, Mutex}; use util::sleep; +use std::thread; +use std::sync::mpsc::TryRecvError; use crate::tray::MeleeTrayEvent; @@ -25,58 +25,57 @@ mod rank; mod util; mod melee; -#[tokio::main] -async fn main() { +fn main() { let instance = SingleInstance::new("SLIPPI_DISCORD_RICH_PRESENCE_MTX").unwrap(); assert!(instance.is_single()); - let (tx, mut rx) = mpsc::channel::(32); - let (mtx, mrx) = std::sync::mpsc::channel::(); + let (tx, rx) = mpsc::channel::(); + let (mtx, mrx) = mpsc::channel::(); - let cancel_token = CancellationToken::new(); - let melee_cancel_token = cancel_token.child_token(); - tokio::spawn(async move { - loop { - let discord_tx = tx.clone(); - let tray_tx = mtx.clone(); - let c_token = melee_cancel_token.clone(); - let res = tokio::task::spawn_blocking(move || { - let mut client = melee::MeleeClient::new(); - client.run(c_token, discord_tx, tray_tx); - }).await; - match res { - Ok(_) => { /* handle successfull exit */ }, - Err(err) if err.is_panic() => { - // panic - let _ = tx.send(DiscordClientRequest::clear()).await; - println!("[ERROR] Melee Client crashed. Restarting..."); - sleep(500); - }, - Err(_) => { } + let cancel_token = Arc::new(Mutex::new(false)); + + { + let cancel_token = cancel_token.clone(); + thread::spawn(move || { + while !*cancel_token.lock().unwrap() { + let discord_tx = tx.clone(); + let tray_tx = mtx.clone(); + // The loop is now managed by a simple spawning of a new thread after a crash + match thread::spawn(move || { + let mut client = melee::MeleeClient::new(); + client.run(discord_tx, tray_tx); + }).join() { + Ok(_) => { /* handle successful exit */ }, + Err(_) => { + // panic + let _ = tx.send(DiscordClientRequest::clear()); + println!("[ERROR] Melee Client crashed. Restarting..."); + sleep(500); + } + } } - } - }); + }); + } let discord_cancel_token = cancel_token.clone(); - tokio::spawn(async move { + thread::spawn(move || { let mut discord_client = discord::start_client().unwrap(); - loop { - if discord_cancel_token.is_cancelled() { - break - } + while !*discord_cancel_token.lock().unwrap() { let poll_res = rx.try_recv(); - if poll_res.is_ok() { - let msg = poll_res.unwrap(); - println!("{:?}", msg); - match msg.req_type { - DiscordClientRequestType::Queue => discord_client.queue(msg.scene, msg.character).await, - DiscordClientRequestType::Idle => discord_client.idle(msg.scene, msg.character).await, - DiscordClientRequestType::Game => discord_client.game(msg.stage, msg.character, msg.mode, msg.timestamp, msg.opp_name).await, - DiscordClientRequestType::Mainmenu => discord_client.main_menu().await, - DiscordClientRequestType::Clear => discord_client.clear() - } + match poll_res { + Ok(msg) => { + println!("{:?}", msg); + match msg.req_type { + DiscordClientRequestType::Queue => discord_client.queue(msg.scene, msg.character), + DiscordClientRequestType::Idle => discord_client.idle(msg.scene, msg.character), + DiscordClientRequestType::Game => discord_client.game(msg.stage, msg.character, msg.mode, msg.timestamp, msg.opp_name), + DiscordClientRequestType::Mainmenu => discord_client.main_menu(), + DiscordClientRequestType::Clear => discord_client.clear() + } + }, + Err(TryRecvError::Disconnected) => break, + Err(TryRecvError::Empty) => {} } - } discord_client.close(); }); @@ -84,5 +83,5 @@ async fn main() { tray::run_tray(mrx); // synchronous // cleanup - cancel_token.cancel(); + *cancel_token.lock().unwrap() = true; } \ No newline at end of file From 2453dfe4cfe4861ca1af209ee30f6535fac00ef0 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 29 Jan 2024 16:55:39 -0800 Subject: [PATCH 03/13] Stub out a `DiscordHandler` integration. The core code was temporarily moved into `discord-rpc/src/bin.rs`, and a new `discord-rpc/src/lib.rs` now exists which stubs out the general flow of how the "handle" should work. This will compile cleanly and "run", albeit do nothing - but now the Discord work that Anders has done can be layered in. This removed a few dependencies from `discord-rpc` that caused compilation issues on non-Windows-x86-64 machines (e.g, windows, tray, ruspiro-singleton). The approach this lib has to take to integrate with Dolphin also likely means we won't need these, but I wanted to make a note to be safe. No actual core logic of the feature has been touched. --- Cargo.toml | 1 + discord-rpc/Cargo.toml | 15 ++-- discord-rpc/src/bin.rs | 87 +++++++++++++++++++++++ discord-rpc/src/error.rs | 22 ++++++ discord-rpc/src/lib.rs | 141 ++++++++++++++++++-------------------- discord-rpc/src/util.rs | 2 +- dolphin/src/logger/mod.rs | 3 + exi/Cargo.toml | 2 +- exi/src/lib.rs | 90 +++++++++++++++--------- 9 files changed, 246 insertions(+), 117 deletions(-) create mode 100644 discord-rpc/src/bin.rs create mode 100644 discord-rpc/src/error.rs diff --git a/Cargo.toml b/Cargo.toml index 9198c24..67aa678 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ debug = true panic = "abort" [workspace.dependencies] +thiserror = "1.0.44" time = { version = "0.3.20", default-features = false, features = ["std", "local-offset"] } serde = { version = "1", features = ["derive"] } serde_json = { version = "1" } diff --git a/discord-rpc/Cargo.toml b/discord-rpc/Cargo.toml index ec467e3..51cf60c 100644 --- a/discord-rpc/Cargo.toml +++ b/discord-rpc/Cargo.toml @@ -1,11 +1,18 @@ [package] -name = "discord-rpc" +name = "slippi-discord-rpc" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +dolphin-integrations = { path = "../dolphin" } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +ureq = { workspace = true } + directories = "5.0.0" discord-rich-presence = "0.2.3" encoding_rs = "0.8.32" @@ -15,15 +22,9 @@ open = "4.1.0" preferences = { git = "https://github.com/andybarron/preferences-rs" } regex = "1.8.1" reqwest = { version = "0.11.16", features = ["json"] } -ruspiro-singleton = "0.4.3" -serde = { version = "1.0.160", features = ["derive"] } -serde_derive = "1.0.160" -serde_json = "1.0.96" single-instance = "0.3.3" structstruck = "0.4.1" strum = "0.24.1" strum_macros = "0.24.3" tokio = { version = "1.27.0", features = ["full"] } tokio-util = "0.7.7" -trayicon = "0.1.3" -windows = { version = "0.48.0", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_System_Memory", "Win32_System_Diagnostics_ToolHelp", "Win32_System_Diagnostics_Debug", "Win32_System_ProcessStatus", "Win32_UI_WindowsAndMessaging"] } diff --git a/discord-rpc/src/bin.rs b/discord-rpc/src/bin.rs new file mode 100644 index 0000000..03ff2c1 --- /dev/null +++ b/discord-rpc/src/bin.rs @@ -0,0 +1,87 @@ +// TODO Sessions - each scene has a minor 0 which is the css. if you leave the major scene, the session ends, otherwise when not in-game we show when the session started +// ^ option name "Show overall game session when not in-game" +// TODO HRC & BTT Records in discord +// TODO Ranked match score, button "Viw opponent ranked profile", show details in stage striking already (in discord rich presence, signalize that you are in stage striking as well) +// TODO clean up melee.rs, move structs/enums away in coherent bundles +//#![windows_subsystem = "windows"] +#![feature(generic_const_exprs)] + +#[macro_use] +extern crate serde_derive; + +use discord::{DiscordClientRequest, DiscordClientRequestType}; +use single_instance::SingleInstance; +use std::sync::{mpsc, Arc, Mutex}; +use util::sleep; +use std::thread; +use std::sync::mpsc::TryRecvError; + +use crate::tray::MeleeTrayEvent; + +mod config; +mod discord; +mod tray; +mod rank; +mod util; +mod melee; + +fn main() { + let instance = SingleInstance::new("SLIPPI_DISCORD_RICH_PRESENCE_MTX").unwrap(); + assert!(instance.is_single()); + let (tx, rx) = mpsc::channel::(); + let (mtx, mrx) = mpsc::channel::(); + + let cancel_token = Arc::new(Mutex::new(false)); + + { + let cancel_token = cancel_token.clone(); + thread::spawn(move || { + while !*cancel_token.lock().unwrap() { + let discord_tx = tx.clone(); + let tray_tx = mtx.clone(); + // The loop is now managed by a simple spawning of a new thread after a crash + match thread::spawn(move || { + let mut client = melee::MeleeClient::new(); + client.run(discord_tx, tray_tx); + }).join() { + Ok(_) => { /* handle successful exit */ }, + Err(_) => { + // panic + let _ = tx.send(DiscordClientRequest::clear()); + println!("[ERROR] Melee Client crashed. Restarting..."); + sleep(500); + } + } + } + }); + } + + let discord_cancel_token = cancel_token.clone(); + thread::spawn(move || { + let mut discord_client = discord::start_client().unwrap(); + + while !*discord_cancel_token.lock().unwrap() { + let poll_res = rx.try_recv(); + match poll_res { + Ok(msg) => { + println!("{:?}", msg); + match msg.req_type { + DiscordClientRequestType::Queue => discord_client.queue(msg.scene, msg.character), + DiscordClientRequestType::Idle => discord_client.idle(msg.scene, msg.character), + DiscordClientRequestType::Game => discord_client.game(msg.stage, msg.character, msg.mode, msg.timestamp, msg.opp_name), + DiscordClientRequestType::Mainmenu => discord_client.main_menu(), + DiscordClientRequestType::Clear => discord_client.clear() + } + }, + Err(TryRecvError::Disconnected) => break, + Err(TryRecvError::Empty) => {} + } + } + discord_client.close(); + }); + + tray::run_tray(mrx); // synchronous + + // cleanup + *cancel_token.lock().unwrap() = true; +} \ No newline at end of file diff --git a/discord-rpc/src/error.rs b/discord-rpc/src/error.rs new file mode 100644 index 0000000..aff2670 --- /dev/null +++ b/discord-rpc/src/error.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +use crate::Message; + +/// Any error type that can be raised by this library. +#[derive(Error, Debug)] +pub enum DiscordRPCError { + #[error("{0}")] + GenericIO(#[from] std::io::Error), + + #[error("Failed to spawn thread: {0}")] + ThreadSpawn(std::io::Error), + + #[error("The channel receiver has disconnected, implying that the data could never be received.")] + ChannelReceiverDisconnected(#[from] std::sync::mpsc::SendError), + + #[error("The channel sender has disconnected, implying no further messages will be received.")] + ChannelSenderDisconnected(#[from] std::sync::mpsc::RecvError), + + #[error("Unknown DiscordRPC Error")] + Unknown +} diff --git a/discord-rpc/src/lib.rs b/discord-rpc/src/lib.rs index 03ff2c1..9a4aeb9 100644 --- a/discord-rpc/src/lib.rs +++ b/discord-rpc/src/lib.rs @@ -1,87 +1,78 @@ -// TODO Sessions - each scene has a minor 0 which is the css. if you leave the major scene, the session ends, otherwise when not in-game we show when the session started -// ^ option name "Show overall game session when not in-game" -// TODO HRC & BTT Records in discord -// TODO Ranked match score, button "Viw opponent ranked profile", show details in stage striking already (in discord rich presence, signalize that you are in stage striking as well) -// TODO clean up melee.rs, move structs/enums away in coherent bundles -//#![windows_subsystem = "windows"] -#![feature(generic_const_exprs)] +//! This module implements native Discord integration for Slippi. +//! +//! The core of it runs in a background thread, listening for new +//! events on each pass of its own loop. -#[macro_use] -extern crate serde_derive; - -use discord::{DiscordClientRequest, DiscordClientRequestType}; -use single_instance::SingleInstance; -use std::sync::{mpsc, Arc, Mutex}; -use util::sleep; use std::thread; -use std::sync::mpsc::TryRecvError; +use std::sync::mpsc::{channel, Receiver, Sender}; + +use dolphin_integrations::Log; + +mod error; +pub use error::DiscordRPCError; -use crate::tray::MeleeTrayEvent; +pub(crate) type Result = std::result::Result; -mod config; -mod discord; -mod tray; -mod rank; -mod util; -mod melee; +/// Message payloads that the inner thread listens for. +#[derive(Debug)] +pub enum Message { + Dropping +} -fn main() { - let instance = SingleInstance::new("SLIPPI_DISCORD_RICH_PRESENCE_MTX").unwrap(); - assert!(instance.is_single()); - let (tx, rx) = mpsc::channel::(); - let (mtx, mrx) = mpsc::channel::(); +/// A client that watches for game events and emits status updates to +/// Discord. This is effectively just a message passing route for the +/// background thread, which does all the actual work. +#[derive(Debug)] +pub struct DiscordHandler { + tx: Sender, +} - let cancel_token = Arc::new(Mutex::new(false)); +impl DiscordHandler { + /// Kicks off the background thread, which monitors game state and emits + /// updates to Discord accordingly. + pub fn new(ram_offset: u8) -> Result { + tracing::info!(target: Log::DiscordRPC, "Initializing DiscordRPC"); - { - let cancel_token = cancel_token.clone(); - thread::spawn(move || { - while !*cancel_token.lock().unwrap() { - let discord_tx = tx.clone(); - let tray_tx = mtx.clone(); - // The loop is now managed by a simple spawning of a new thread after a crash - match thread::spawn(move || { - let mut client = melee::MeleeClient::new(); - client.run(discord_tx, tray_tx); - }).join() { - Ok(_) => { /* handle successful exit */ }, - Err(_) => { - // panic - let _ = tx.send(DiscordClientRequest::clear()); - println!("[ERROR] Melee Client crashed. Restarting..."); - sleep(500); - } + // Create a sender and receiver channel pair to communicate between threads. + let (tx, rx) = channel::(); + + // Spawn a new background thread that manages its own loop. If or when + // the loop breaks - either due to shutdown or intentional drop - the underlying + // OS thread will clean itself up. + thread::Builder::new() + .name("SlippiDiscordRPC".to_string()) + .spawn(move || { + if let Err(e) = Self::start(rx, ram_offset) { + tracing::error!( + target: Log::DiscordRPC, + error = ?e, + "SlippiDiscordRPC thread encountered an error: {e}" + ); } - } - }); - } + }) + .map_err(error::DiscordRPCError::ThreadSpawn)?; - let discord_cancel_token = cancel_token.clone(); - thread::spawn(move || { - let mut discord_client = discord::start_client().unwrap(); + Ok(Self { tx }) + } - while !*discord_cancel_token.lock().unwrap() { - let poll_res = rx.try_recv(); - match poll_res { - Ok(msg) => { - println!("{:?}", msg); - match msg.req_type { - DiscordClientRequestType::Queue => discord_client.queue(msg.scene, msg.character), - DiscordClientRequestType::Idle => discord_client.idle(msg.scene, msg.character), - DiscordClientRequestType::Game => discord_client.game(msg.stage, msg.character, msg.mode, msg.timestamp, msg.opp_name), - DiscordClientRequestType::Mainmenu => discord_client.main_menu(), - DiscordClientRequestType::Clear => discord_client.clear() - } - }, - Err(TryRecvError::Disconnected) => break, - Err(TryRecvError::Empty) => {} - } - } - discord_client.close(); - }); + /// Must be called on a background thread. Runs the core event loop. + fn start(rx: Receiver, ram_offset: u8) -> Result<()> { + Ok(()) + } +} - tray::run_tray(mrx); // synchronous +impl Drop for DiscordHandler { + /// Notifies the background thread that we're dropping. The thread should + /// listen for the message and break its runloop accordingly. + fn drop(&mut self) { + tracing::info!(target: Log::DiscordRPC, "Dropping DiscordRPC"); - // cleanup - *cancel_token.lock().unwrap() = true; -} \ No newline at end of file + if let Err(e) = self.tx.send(Message::Dropping) { + tracing::warn!( + target: Log::DiscordRPC, + error = ?e, + "Failed to notify child thread that DiscordRPC is dropping" + ); + } + } +} diff --git a/discord-rpc/src/util.rs b/discord-rpc/src/util.rs index 3c3f142..e7fe202 100644 --- a/discord-rpc/src/util.rs +++ b/discord-rpc/src/util.rs @@ -20,4 +20,4 @@ pub fn get_appdata_file(suffix: &str) -> Option { return Some(base_dirs.config_dir().join(suffix)); } None -} \ No newline at end of file +} diff --git a/dolphin/src/logger/mod.rs b/dolphin/src/logger/mod.rs index 72e0f04..cdaac8f 100644 --- a/dolphin/src/logger/mod.rs +++ b/dolphin/src/logger/mod.rs @@ -53,6 +53,9 @@ pub mod Log { /// Can be used to segment Jukebox logs. pub const Jukebox: &'static str = "SLIPPI_RUST_JUKEBOX"; + + /// Used for any logs specific to the Discord RPC library. + pub const DiscordRPC: &'static str = "SLIPPI_DISCORD_RPC"; } /// Represents a `LogContainer` on the Dolphin side. diff --git a/exi/Cargo.toml b/exi/Cargo.toml index 5d1bc49..f4ac02c 100644 --- a/exi/Cargo.toml +++ b/exi/Cargo.toml @@ -16,9 +16,9 @@ mainline = [] [dependencies] dolphin-integrations = { path = "../dolphin" } +slippi-discord-rpc = { path = "../discord-rpc" } slippi-game-reporter = { path = "../game-reporter" } slippi-jukebox = { path = "../jukebox" } slippi-user = { path = "../user" } -slippi-discord-rpc = { path = "../discord-rpc" } tracing = { workspace = true } ureq = { workspace = true } diff --git a/exi/src/lib.rs b/exi/src/lib.rs index 5e51ee5..dec2c17 100644 --- a/exi/src/lib.rs +++ b/exi/src/lib.rs @@ -10,34 +10,45 @@ use std::time::Duration; use ureq::AgentBuilder; use dolphin_integrations::Log; +use slippi_discord_rpc::DiscordHandler; use slippi_game_reporter::GameReporter; use slippi_jukebox::Jukebox; use slippi_user::UserManager; -// Addition to existing imports -use slippi_discord_rpc::DiscordClientRequest; // This assumes slippi-discord-rpc has this type or function. -use slippi_discord_rpc::DiscordClientRequestType; // Adjust this as necessary based on actual module contents. mod config; pub use config::{Config, FilePathsConfig, SCMConfig}; -/// An EXI Device subclass specific to managing and interacting with the game itself. +/// Configuration instructions that the FFI layer uses to call over here. #[derive(Debug)] -pub struct SlippiEXIDevice { - config: Config, - pub game_reporter: GameReporter, - pub user_manager: UserManager, - pub jukebox: Option, - discord_client: Option, -} - pub enum JukeboxConfiguration { Start { initial_dolphin_system_volume: u8, initial_dolphin_music_volume: u8, }, + Stop, } +/// Configuration instructions that the FFI layer uses to call over here. +#[derive(Debug)] +pub enum DiscordHandlerConfiguration { + Start { + ram_offset: u8 + }, + + Stop, +} + +/// An EXI Device subclass specific to managing and interacting with the game itself. +#[derive(Debug)] +pub struct SlippiEXIDevice { + config: Config, + pub game_reporter: GameReporter, + pub user_manager: UserManager, + pub jukebox: Option, + pub discord_handler: Option, +} + impl SlippiEXIDevice { /// Creates and returns a new `SlippiEXIDevice` with default values. /// @@ -72,30 +83,10 @@ impl SlippiEXIDevice { game_reporter, user_manager, jukebox: None, + discord_handler: None } } - pub fn start_discord_client(&mut self) { - if self.discord_client.is_some() { - tracing::info!(target: Log::SlippiOnline, "Discord client is already running."); - return; - } - - match DiscordClient::new() { // Replace with the actual API call to create a new Discord client instance. - Ok(client) => { - self.discord_client = Some(client); - tracing::info!(target: Log::SlippiOnline, "Discord client started successfully."); - // If needed, run the Discord client in the background, for example with a Tokio task. - // This will require the Discord client to be 'Send' if it is moved to an async block. - tokio::spawn(async move { - self.discord_client.as_mut().unwrap().run().await; - }); - } - Err(e) => { - tracing::error!(target: Log::SlippiOnline, "Failed to start Discord client due to {:?}", e); - } - } - } /// Stubbed for now, but this would get called by the C++ EXI device on DMAWrite. pub fn dma_write(&mut self, _address: usize, _size: usize) {} @@ -136,4 +127,37 @@ impl SlippiEXIDevice { } } } + + /// Configures a new Discord handler, or ensures an existing one is dropped if it's being + /// disabled. + pub fn configure_discord_handler(&mut self, config: DiscordHandlerConfiguration) { + if let DiscordHandlerConfiguration::Stop = config { + self.discord_handler = None; + return; + } + + if self.discord_handler.is_some() { + tracing::warn!(target: Log::SlippiOnline, "Discord handler is already running."); + return; + } + + if let DiscordHandlerConfiguration::Start { + ram_offset + } = config + { + match DiscordHandler::new(ram_offset) { + Ok(handler) => { + self.discord_handler = Some(handler); + }, + + Err(e) => { + tracing::error!( + target: Log::SlippiOnline, + error = ?e, + "Failed to start Discord handler" + ); + } + } + } + } } From be12cf1dda57245ce6784537adad1e280ad7398e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 29 Jan 2024 16:58:43 -0800 Subject: [PATCH 04/13] Replace Jukebox thiserror dependency with workspace version, since we will also use it in the discord handler --- Cargo.lock | 995 +++++++++++++++++++++++++++++++++++++++++++-- jukebox/Cargo.toml | 2 +- 2 files changed, 952 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39fa9af..d9a100c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -26,7 +35,7 @@ dependencies = [ "alsa-sys", "bitflags 1.3.2", "libc", - "nix", + "nix 0.24.3", ] [[package]] @@ -45,6 +54,18 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "app_dirs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e73a24bad9bd6a94d6395382a6c69fe071708ae4409f763c5475e14ee896313d" +dependencies = [ + "ole32-sys", + "shell32-sys", + "winapi 0.2.8", + "xdg", +] + [[package]] name = "atty" version = "0.2.14" @@ -53,7 +74,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -62,6 +83,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.21.4" @@ -228,6 +264,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys 0.8.4", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.6.2" @@ -313,7 +359,7 @@ dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "memoffset", + "memoffset 0.9.0", "scopeguard", ] @@ -338,6 +384,39 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "discord-rich-presence" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47fc4beffb85ee1461588499073a4d9c20dcc7728c4b13d6b282ab6c508947e5" +dependencies = [ + "serde", + "serde_derive", + "serde_json", + "uuid", +] + [[package]] name = "dolphin-integrations" version = "0.1.0" @@ -353,6 +432,15 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -367,7 +455,7 @@ checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" dependencies = [ "errno-dragonfly", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -396,6 +484,27 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -405,12 +514,87 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "h2" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.0.2", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -456,6 +640,77 @@ dependencies = [ "thiserror", ] +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.4.0" @@ -486,6 +741,12 @@ dependencies = [ "hashbrown 0.14.1", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "is-docker" version = "0.2.0" @@ -503,7 +764,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.3", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -588,9 +849,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.148" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libloading" @@ -599,7 +860,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if", - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.0", + "libc", + "redox_syscall 0.4.1", ] [[package]] @@ -639,6 +911,15 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.9.0" @@ -648,6 +929,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -663,6 +950,35 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.7.0" @@ -672,7 +988,7 @@ dependencies = [ "bitflags 1.3.2", "jni-sys", "ndk-sys", - "num_enum", + "num_enum 0.5.11", "raw-window-handle", "thiserror", ] @@ -692,6 +1008,19 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + [[package]] name = "nix" version = "0.24.3" @@ -720,7 +1049,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -743,13 +1072,32 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.3", + "libc", +] + [[package]] name = "num_enum" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" dependencies = [ - "num_enum_derive", + "num_enum_derive 0.5.11", +] + +[[package]] +name = "num_enum" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive 0.6.1", ] [[package]] @@ -764,6 +1112,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num_enum_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "num_threads" version = "0.1.6" @@ -773,6 +1133,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "oboe" version = "0.5.0" @@ -796,12 +1165,33 @@ dependencies = [ "cc", ] +[[package]] +name = "ole32-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d2c49021782e5233cd243168edfa8037574afed4eba4bbaf538b3d8d1789d8c" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "open" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a083c0c7e5e4a8ec4176346cf61f67ac674e8bfb059d9226e1c54a96b377c12" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "open" version = "5.0.0" @@ -814,38 +1204,88 @@ dependencies = [ ] [[package]] -name = "os_str_bytes" -version = "6.5.1" +name = "openssl" +version = "0.10.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" +checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] [[package]] -name = "overload" +name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] [[package]] -name = "parking_lot" -version = "0.12.1" +name = "openssl-probe" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] -name = "parking_lot_core" -version = "0.9.8" +name = "openssl-sys" +version = "0.9.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" dependencies = [ - "cfg-if", + "cc", "libc", - "redox_syscall", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_str_bytes" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -872,12 +1312,28 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "preferences" +version = "2.0.0" +source = "git+https://github.com/andybarron/preferences-rs#bcef27fd41d5a7a95de1b56e7ea7199867c926b4" +dependencies = [ + "app_dirs", + "serde", + "serde_json", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -941,6 +1397,26 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.9.6" @@ -970,6 +1446,44 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "reqwest" +version = "0.11.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.16.20" @@ -982,7 +1496,7 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -994,6 +1508,12 @@ dependencies = [ "cpal", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -1010,7 +1530,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1035,6 +1555,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.15" @@ -1050,6 +1576,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1066,6 +1601,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys 0.8.4", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys 0.8.4", + "libc", +] + [[package]] name = "serde" version = "1.0.188" @@ -1108,6 +1666,18 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1117,17 +1687,86 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell32-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee04b46101f57121c9da2b151988283b6beb79b34f5bb29a58ee48cb695122c" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "shlex" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "single-instance" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4637485391f8545c9d3dbf60f9d9aab27a90c789a700999677583bcb17c8795d" +dependencies = [ + "libc", + "nix 0.23.2", + "thiserror", + "widestring", + "winapi 0.3.9", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slippi-discord-rpc" +version = "0.1.0" +dependencies = [ + "directories", + "discord-rich-presence", + "dolphin-integrations", + "encoding_rs", + "lazy_static", + "num_enum 0.6.1", + "open 4.2.0", + "preferences", + "regex", + "reqwest", + "serde", + "serde_json", + "single-instance", + "structstruck", + "strum", + "strum_macros", + "thiserror", + "tokio", + "tokio-util", + "tracing", + "ureq", +] + [[package]] name = "slippi-exi-device" version = "0.1.0" dependencies = [ "dolphin-integrations", + "slippi-discord-rpc", "slippi-game-reporter", "slippi-jukebox", "slippi-user", @@ -1179,7 +1818,7 @@ name = "slippi-user" version = "0.1.0" dependencies = [ "dolphin-integrations", - "open", + "open 5.0.0", "serde", "serde_json", "tracing", @@ -1192,6 +1831,16 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.5.2" @@ -1204,6 +1853,37 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "structstruck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a052ec87a2d9bdd3a35f85ec6a07a5ac0816e4190b1cbede9d67cccb47ea66d" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "venial", +] + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + [[package]] name = "syn" version = "1.0.109" @@ -1226,6 +1906,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys 0.8.4", + "libc", +] + [[package]] name = "tempfile" version = "3.8.0" @@ -1234,9 +1935,9 @@ checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "redox_syscall 0.3.5", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1318,6 +2019,60 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "toml" version = "0.5.11" @@ -1344,6 +2099,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.37" @@ -1390,6 +2151,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -1446,12 +2213,37 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", +] + [[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "venial" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61584a325b16f97b5b25fcc852eb9550843a251057a5e3e5992d2376f3df4bb2" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "walkdir" version = "2.4.0" @@ -1462,6 +2254,21 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.87" @@ -1544,6 +2351,18 @@ version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +[[package]] +name = "widestring" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -1554,6 +2373,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -1566,7 +2391,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1581,7 +2406,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1590,7 +2415,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -1599,13 +2433,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -1614,42 +2463,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" version = "0.5.16" @@ -1658,3 +2549,19 @@ checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907" dependencies = [ "memchr", ] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" diff --git a/jukebox/Cargo.toml b/jukebox/Cargo.toml index 84d3794..dca921b 100644 --- a/jukebox/Cargo.toml +++ b/jukebox/Cargo.toml @@ -19,5 +19,5 @@ mainline = [] dolphin-integrations = { path = "../dolphin" } hps_decode = { version = "0.2.1", features = ["rodio-source"] } rodio = { version = "0.17.1", default-features = false } -thiserror = "1.0.44" +thiserror = { workspace = true } tracing = { workspace = true } From 289e487b6a32b3bec5c3e4d8479fc798a1661e8a Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 29 Jan 2024 17:00:11 -0800 Subject: [PATCH 05/13] Formatting pass --- discord-rpc/src/error.rs | 2 +- discord-rpc/src/lib.rs | 4 ++-- exi/src/lib.rs | 13 ++++--------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/discord-rpc/src/error.rs b/discord-rpc/src/error.rs index aff2670..16d8dbe 100644 --- a/discord-rpc/src/error.rs +++ b/discord-rpc/src/error.rs @@ -18,5 +18,5 @@ pub enum DiscordRPCError { ChannelSenderDisconnected(#[from] std::sync::mpsc::RecvError), #[error("Unknown DiscordRPC Error")] - Unknown + Unknown, } diff --git a/discord-rpc/src/lib.rs b/discord-rpc/src/lib.rs index 9a4aeb9..1b5acde 100644 --- a/discord-rpc/src/lib.rs +++ b/discord-rpc/src/lib.rs @@ -3,8 +3,8 @@ //! The core of it runs in a background thread, listening for new //! events on each pass of its own loop. -use std::thread; use std::sync::mpsc::{channel, Receiver, Sender}; +use std::thread; use dolphin_integrations::Log; @@ -16,7 +16,7 @@ pub(crate) type Result = std::result::Result; /// Message payloads that the inner thread listens for. #[derive(Debug)] pub enum Message { - Dropping + Dropping, } /// A client that watches for game events and emits status updates to diff --git a/exi/src/lib.rs b/exi/src/lib.rs index dec2c17..ac54b1e 100644 --- a/exi/src/lib.rs +++ b/exi/src/lib.rs @@ -32,9 +32,7 @@ pub enum JukeboxConfiguration { /// Configuration instructions that the FFI layer uses to call over here. #[derive(Debug)] pub enum DiscordHandlerConfiguration { - Start { - ram_offset: u8 - }, + Start { ram_offset: u8 }, Stop, } @@ -83,7 +81,7 @@ impl SlippiEXIDevice { game_reporter, user_manager, jukebox: None, - discord_handler: None + discord_handler: None, } } @@ -141,10 +139,7 @@ impl SlippiEXIDevice { return; } - if let DiscordHandlerConfiguration::Start { - ram_offset - } = config - { + if let DiscordHandlerConfiguration::Start { ram_offset } = config { match DiscordHandler::new(ram_offset) { Ok(handler) => { self.discord_handler = Some(handler); @@ -156,7 +151,7 @@ impl SlippiEXIDevice { error = ?e, "Failed to start Discord handler" ); - } + }, } } } From 21291de34fe9c8180e2b4f567fc4da98481156a7 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 29 Jan 2024 17:18:12 -0800 Subject: [PATCH 06/13] Expose discord-rpc::Config for Dolphin to use. This part might change but it makes some development pieces easier at the moment. --- discord-rpc/src/config.rs | 56 ++++++++++++--------------------------- discord-rpc/src/lib.rs | 33 +++++++++++++++++------ exi/src/lib.rs | 17 +++++++----- 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/discord-rpc/src/config.rs b/discord-rpc/src/config.rs index dd7c7d2..224b47e 100644 --- a/discord-rpc/src/config.rs +++ b/discord-rpc/src/config.rs @@ -1,24 +1,7 @@ -use preferences::{AppInfo, Preferences}; -use ruspiro_singleton::Singleton; - -use crate::melee::SlippiMenuScene; - -pub const APP_INFO: AppInfo = AppInfo { - name: "conf", - author: "Slippi Discord Integration", -}; -const PREFS_KEY: &str = "app_config"; - -pub static CONFIG: Singleton = Singleton::lazy(&|| { - match AppConfig::load(&APP_INFO, PREFS_KEY) { - Ok(cfg) => cfg, - Err(_) => AppConfig::default() - } -}); - structstruck::strike! { - #[strikethrough[derive(Serialize, Deserialize, PartialEq, Debug)]] - pub struct AppConfig { + /// Core configuration object for this library. + #[strikethrough[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]] + pub struct Config { pub global: struct { pub show_in_game_character: bool, pub show_in_game_time: bool @@ -68,12 +51,12 @@ structstruck::strike! { } } -impl Default for AppConfig { +impl Default for Config { fn default() -> Self { - AppConfig { + Config { global: Global { show_in_game_character: true, - show_in_game_time: true + show_in_game_time: true, }, slippi: Slippi { enabled: true, @@ -83,39 +66,34 @@ impl Default for AppConfig { enabled: true, show_rank: true, show_view_ranked_profile_button: true, - show_score: true + show_score: true, }, unranked: Unranked { enabled: true }, direct: Direct { enabled: true }, - teams: Teams { enabled: true } + teams: Teams { enabled: true }, }, uncle_punch: UnclePunch { enabled: true }, vs_mode: VsMode { enabled: true }, training_mode: TrainingMode { enabled: true }, stadium: Stadium { enabled: true, - hrc: Hrc { - enabled: true - }, + hrc: Hrc { enabled: true }, btt: Btt { enabled: true, - show_stage_name: true + show_stage_name: true, }, - mmm: Mmm { - enabled: true - } - } + mmm: Mmm { enabled: true }, + }, } } } -pub fn write_config(val: &AppConfig) { - let _ = val.save(&APP_INFO, PREFS_KEY); -} - // Utility implementations +// Commented out for the moment +/* +use crate::melee::SlippiMenuScene; impl SlippiMenuScene { - pub fn is_enabled(&self, c: &AppConfig) -> bool { + pub fn is_enabled(&self, c: &Config) -> bool { match *self { SlippiMenuScene::Ranked => c.slippi.ranked.enabled, SlippiMenuScene::Unranked => c.slippi.unranked.enabled, @@ -123,4 +101,4 @@ impl SlippiMenuScene { SlippiMenuScene::Teams => c.slippi.teams.enabled } } -} \ No newline at end of file +}*/ diff --git a/discord-rpc/src/lib.rs b/discord-rpc/src/lib.rs index 1b5acde..b410e14 100644 --- a/discord-rpc/src/lib.rs +++ b/discord-rpc/src/lib.rs @@ -8,6 +8,9 @@ use std::thread; use dolphin_integrations::Log; +mod config; +pub use config::Config; + mod error; pub use error::DiscordRPCError; @@ -16,6 +19,7 @@ pub(crate) type Result = std::result::Result; /// Message payloads that the inner thread listens for. #[derive(Debug)] pub enum Message { + ConfigUpdate(Config), Dropping, } @@ -30,8 +34,8 @@ pub struct DiscordHandler { impl DiscordHandler { /// Kicks off the background thread, which monitors game state and emits /// updates to Discord accordingly. - pub fn new(ram_offset: u8) -> Result { - tracing::info!(target: Log::DiscordRPC, "Initializing DiscordRPC"); + pub fn new(ram_offset: u8, config: Config) -> Result { + tracing::info!(target: Log::DiscordRPC, "Initializing DiscordHandler"); // Create a sender and receiver channel pair to communicate between threads. let (tx, rx) = channel::(); @@ -40,13 +44,13 @@ impl DiscordHandler { // the loop breaks - either due to shutdown or intentional drop - the underlying // OS thread will clean itself up. thread::Builder::new() - .name("SlippiDiscordRPC".to_string()) + .name("SlippiDiscordHandler".to_string()) .spawn(move || { - if let Err(e) = Self::start(rx, ram_offset) { + if let Err(e) = Self::start(rx, ram_offset, config) { tracing::error!( target: Log::DiscordRPC, error = ?e, - "SlippiDiscordRPC thread encountered an error: {e}" + "SlippiDiscordHandler thread encountered an error: {e}" ); } }) @@ -56,22 +60,35 @@ impl DiscordHandler { } /// Must be called on a background thread. Runs the core event loop. - fn start(rx: Receiver, ram_offset: u8) -> Result<()> { + fn start(rx: Receiver, ram_offset: u8, config: Config) -> Result<()> { Ok(()) } + + /// Passes a new configuration into the background handler. + pub fn update_config(&mut self, config: Config) { + if let Err(e) = self.tx.send(Message::ConfigUpdate(config)) { + // @TODO: Maybe add an OSD log message here? + + tracing::error!( + target: Log::DiscordRPC, + error = ?e, + "Failed to update DiscordHandler config" + ); + } + } } impl Drop for DiscordHandler { /// Notifies the background thread that we're dropping. The thread should /// listen for the message and break its runloop accordingly. fn drop(&mut self) { - tracing::info!(target: Log::DiscordRPC, "Dropping DiscordRPC"); + tracing::info!(target: Log::DiscordRPC, "Dropping DiscordHandler"); if let Err(e) = self.tx.send(Message::Dropping) { tracing::warn!( target: Log::DiscordRPC, error = ?e, - "Failed to notify child thread that DiscordRPC is dropping" + "Failed to notify child thread that DiscordHandler is dropping" ); } } diff --git a/exi/src/lib.rs b/exi/src/lib.rs index ac54b1e..39d192f 100644 --- a/exi/src/lib.rs +++ b/exi/src/lib.rs @@ -10,7 +10,7 @@ use std::time::Duration; use ureq::AgentBuilder; use dolphin_integrations::Log; -use slippi_discord_rpc::DiscordHandler; +use slippi_discord_rpc::{Config as DiscordHandlerConfig, DiscordHandler}; use slippi_game_reporter::GameReporter; use slippi_jukebox::Jukebox; use slippi_user::UserManager; @@ -32,8 +32,8 @@ pub enum JukeboxConfiguration { /// Configuration instructions that the FFI layer uses to call over here. #[derive(Debug)] pub enum DiscordHandlerConfiguration { - Start { ram_offset: u8 }, - + Start { ram_offset: u8, config: DiscordHandlerConfig }, + UpdateConfig { config: DiscordHandlerConfig }, Stop, } @@ -134,13 +134,18 @@ impl SlippiEXIDevice { return; } - if self.discord_handler.is_some() { + if let Some(discord_handler) = &mut self.discord_handler { + if let DiscordHandlerConfiguration::UpdateConfig { config } = config { + discord_handler.update_config(config); + return; + } + tracing::warn!(target: Log::SlippiOnline, "Discord handler is already running."); return; } - if let DiscordHandlerConfiguration::Start { ram_offset } = config { - match DiscordHandler::new(ram_offset) { + if let DiscordHandlerConfiguration::Start { ram_offset, config } = config { + match DiscordHandler::new(ram_offset, config) { Ok(handler) => { self.discord_handler = Some(handler); }, From f20508acb70c1fe3d11612017bb4ee97023118f0 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 29 Jan 2024 17:39:39 -0800 Subject: [PATCH 07/13] Move melee.rs to be melee/mod.rs to match existing workspace module setup --- discord-rpc/src/{melee.rs => melee/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename discord-rpc/src/{melee.rs => melee/mod.rs} (100%) diff --git a/discord-rpc/src/melee.rs b/discord-rpc/src/melee/mod.rs similarity index 100% rename from discord-rpc/src/melee.rs rename to discord-rpc/src/melee/mod.rs From 9a5d4af10a169bef185f91351950c33831dca006 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 29 Jan 2024 17:46:32 -0800 Subject: [PATCH 08/13] Stub out runloop --- discord-rpc/src/lib.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/discord-rpc/src/lib.rs b/discord-rpc/src/lib.rs index b410e14..c39fac8 100644 --- a/discord-rpc/src/lib.rs +++ b/discord-rpc/src/lib.rs @@ -19,8 +19,8 @@ pub(crate) type Result = std::result::Result; /// Message payloads that the inner thread listens for. #[derive(Debug)] pub enum Message { - ConfigUpdate(Config), Dropping, + UpdateConfig(Config), } /// A client that watches for game events and emits status updates to @@ -61,12 +61,24 @@ impl DiscordHandler { /// Must be called on a background thread. Runs the core event loop. fn start(rx: Receiver, ram_offset: u8, config: Config) -> Result<()> { + loop { + match rx.recv()? { + // Handle any configuration updates. + Message::UpdateConfig(config) => {}, + + // Just break the loop so things exit cleanly. + Message::Dropping => { + break; + }, + } + } + Ok(()) } /// Passes a new configuration into the background handler. pub fn update_config(&mut self, config: Config) { - if let Err(e) = self.tx.send(Message::ConfigUpdate(config)) { + if let Err(e) = self.tx.send(Message::UpdateConfig(config)) { // @TODO: Maybe add an OSD log message here? tracing::error!( From a7217ec07a768e5018957e746cb1f6a50b3f532d Mon Sep 17 00:00:00 2001 From: Anders Madsen Date: Tue, 30 Jan 2024 19:20:00 +0100 Subject: [PATCH 09/13] u8 to usize --- discord-rpc/src/lib.rs | 8 ++++---- exi/src/lib.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/discord-rpc/src/lib.rs b/discord-rpc/src/lib.rs index c39fac8..e80b0a0 100644 --- a/discord-rpc/src/lib.rs +++ b/discord-rpc/src/lib.rs @@ -34,7 +34,7 @@ pub struct DiscordHandler { impl DiscordHandler { /// Kicks off the background thread, which monitors game state and emits /// updates to Discord accordingly. - pub fn new(ram_offset: u8, config: Config) -> Result { + pub fn new(ram_offset: usize, config: Config) -> Result { tracing::info!(target: Log::DiscordRPC, "Initializing DiscordHandler"); // Create a sender and receiver channel pair to communicate between threads. @@ -44,13 +44,13 @@ impl DiscordHandler { // the loop breaks - either due to shutdown or intentional drop - the underlying // OS thread will clean itself up. thread::Builder::new() - .name("SlippiDiscordHandler".to_string()) + .name("DiscordHandler".to_string()) .spawn(move || { if let Err(e) = Self::start(rx, ram_offset, config) { tracing::error!( target: Log::DiscordRPC, error = ?e, - "SlippiDiscordHandler thread encountered an error: {e}" + "DiscordHandler thread encountered an error: {e}" ); } }) @@ -60,7 +60,7 @@ impl DiscordHandler { } /// Must be called on a background thread. Runs the core event loop. - fn start(rx: Receiver, ram_offset: u8, config: Config) -> Result<()> { + fn start(rx: Receiver, ram_offset: usize, config: Config) -> Result<()> { loop { match rx.recv()? { // Handle any configuration updates. diff --git a/exi/src/lib.rs b/exi/src/lib.rs index 39d192f..8f903b7 100644 --- a/exi/src/lib.rs +++ b/exi/src/lib.rs @@ -145,7 +145,7 @@ impl SlippiEXIDevice { } if let DiscordHandlerConfiguration::Start { ram_offset, config } = config { - match DiscordHandler::new(ram_offset, config) { + match DiscordHandler::new(ram_offset.into(), config) { Ok(handler) => { self.discord_handler = Some(handler); }, From 92b1319f8c08965d3138f6700496f90941e08c42 Mon Sep 17 00:00:00 2001 From: Anders Madsen Date: Tue, 30 Jan 2024 22:46:09 +0100 Subject: [PATCH 10/13] Jukebox as foundation Making jukebox the foundation for the discord rpc --- discord-rpc/Cargo.toml | 37 +-- discord-rpc/src/bin.rs | 87 ------ discord-rpc/src/config.rs | 104 ------- discord-rpc/src/discord.rs | 293 ------------------- discord-rpc/src/error.rs | 22 -- discord-rpc/src/errors.rs | 37 +++ discord-rpc/src/lib.rs | 344 +++++++++++++++++----- discord-rpc/src/melee/character.rs | 122 -------- discord-rpc/src/melee/dolphin_mem.rs | 233 --------------- discord-rpc/src/melee/dolphin_user.rs | 34 --- discord-rpc/src/melee/mod.rs | 327 --------------------- discord-rpc/src/melee/msrb.rs | 59 ---- discord-rpc/src/melee/multiman.rs | 9 - discord-rpc/src/melee/stage.rs | 173 ----------- discord-rpc/src/rank.rs | 14 - discord-rpc/src/scenes.rs | 396 ++++++++++++++++++++++++++ discord-rpc/src/tray.rs | 286 ------------------- discord-rpc/src/util.rs | 23 -- discord-rpc/src/utils.rs | 98 +++++++ 19 files changed, 821 insertions(+), 1877 deletions(-) delete mode 100644 discord-rpc/src/bin.rs delete mode 100644 discord-rpc/src/config.rs delete mode 100644 discord-rpc/src/discord.rs delete mode 100644 discord-rpc/src/error.rs create mode 100644 discord-rpc/src/errors.rs delete mode 100644 discord-rpc/src/melee/character.rs delete mode 100644 discord-rpc/src/melee/dolphin_mem.rs delete mode 100644 discord-rpc/src/melee/dolphin_user.rs delete mode 100644 discord-rpc/src/melee/mod.rs delete mode 100644 discord-rpc/src/melee/msrb.rs delete mode 100644 discord-rpc/src/melee/multiman.rs delete mode 100644 discord-rpc/src/melee/stage.rs delete mode 100644 discord-rpc/src/rank.rs create mode 100644 discord-rpc/src/scenes.rs delete mode 100644 discord-rpc/src/tray.rs delete mode 100644 discord-rpc/src/util.rs create mode 100644 discord-rpc/src/utils.rs diff --git a/discord-rpc/Cargo.toml b/discord-rpc/Cargo.toml index 51cf60c..6f3be31 100644 --- a/discord-rpc/Cargo.toml +++ b/discord-rpc/Cargo.toml @@ -1,30 +1,23 @@ [package] name = "slippi-discord-rpc" +description = "A library for interfacing with Discord Rich Presence, providing real-time game state updates from Dolphin emulator." version = "0.1.0" +authors = [ + "Slippi Team", + "Anders Madsen " +] edition = "2021" +publish = false -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = [] +ishiiruka = [] +mainline = [] [dependencies] dolphin-integrations = { path = "../dolphin" } -serde = { workspace = true } -serde_json = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } -ureq = { workspace = true } - -directories = "5.0.0" -discord-rich-presence = "0.2.3" -encoding_rs = "0.8.32" -lazy_static = "1.4.0" -num_enum = "0.6.1" -open = "4.1.0" -preferences = { git = "https://github.com/andybarron/preferences-rs" } -regex = "1.8.1" -reqwest = { version = "0.11.16", features = ["json"] } -single-instance = "0.3.3" -structstruck = "0.4.1" -strum = "0.24.1" -strum_macros = "0.24.3" -tokio = { version = "1.27.0", features = ["full"] } -tokio-util = "0.7.7" +hps_decode = "0.1.1" +process-memory = "0.5.0" +rodio = "0.17.1" +thiserror = "1.0.44" +tracing = { workspace = true } \ No newline at end of file diff --git a/discord-rpc/src/bin.rs b/discord-rpc/src/bin.rs deleted file mode 100644 index 03ff2c1..0000000 --- a/discord-rpc/src/bin.rs +++ /dev/null @@ -1,87 +0,0 @@ -// TODO Sessions - each scene has a minor 0 which is the css. if you leave the major scene, the session ends, otherwise when not in-game we show when the session started -// ^ option name "Show overall game session when not in-game" -// TODO HRC & BTT Records in discord -// TODO Ranked match score, button "Viw opponent ranked profile", show details in stage striking already (in discord rich presence, signalize that you are in stage striking as well) -// TODO clean up melee.rs, move structs/enums away in coherent bundles -//#![windows_subsystem = "windows"] -#![feature(generic_const_exprs)] - -#[macro_use] -extern crate serde_derive; - -use discord::{DiscordClientRequest, DiscordClientRequestType}; -use single_instance::SingleInstance; -use std::sync::{mpsc, Arc, Mutex}; -use util::sleep; -use std::thread; -use std::sync::mpsc::TryRecvError; - -use crate::tray::MeleeTrayEvent; - -mod config; -mod discord; -mod tray; -mod rank; -mod util; -mod melee; - -fn main() { - let instance = SingleInstance::new("SLIPPI_DISCORD_RICH_PRESENCE_MTX").unwrap(); - assert!(instance.is_single()); - let (tx, rx) = mpsc::channel::(); - let (mtx, mrx) = mpsc::channel::(); - - let cancel_token = Arc::new(Mutex::new(false)); - - { - let cancel_token = cancel_token.clone(); - thread::spawn(move || { - while !*cancel_token.lock().unwrap() { - let discord_tx = tx.clone(); - let tray_tx = mtx.clone(); - // The loop is now managed by a simple spawning of a new thread after a crash - match thread::spawn(move || { - let mut client = melee::MeleeClient::new(); - client.run(discord_tx, tray_tx); - }).join() { - Ok(_) => { /* handle successful exit */ }, - Err(_) => { - // panic - let _ = tx.send(DiscordClientRequest::clear()); - println!("[ERROR] Melee Client crashed. Restarting..."); - sleep(500); - } - } - } - }); - } - - let discord_cancel_token = cancel_token.clone(); - thread::spawn(move || { - let mut discord_client = discord::start_client().unwrap(); - - while !*discord_cancel_token.lock().unwrap() { - let poll_res = rx.try_recv(); - match poll_res { - Ok(msg) => { - println!("{:?}", msg); - match msg.req_type { - DiscordClientRequestType::Queue => discord_client.queue(msg.scene, msg.character), - DiscordClientRequestType::Idle => discord_client.idle(msg.scene, msg.character), - DiscordClientRequestType::Game => discord_client.game(msg.stage, msg.character, msg.mode, msg.timestamp, msg.opp_name), - DiscordClientRequestType::Mainmenu => discord_client.main_menu(), - DiscordClientRequestType::Clear => discord_client.clear() - } - }, - Err(TryRecvError::Disconnected) => break, - Err(TryRecvError::Empty) => {} - } - } - discord_client.close(); - }); - - tray::run_tray(mrx); // synchronous - - // cleanup - *cancel_token.lock().unwrap() = true; -} \ No newline at end of file diff --git a/discord-rpc/src/config.rs b/discord-rpc/src/config.rs deleted file mode 100644 index 224b47e..0000000 --- a/discord-rpc/src/config.rs +++ /dev/null @@ -1,104 +0,0 @@ -structstruck::strike! { - /// Core configuration object for this library. - #[strikethrough[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]] - pub struct Config { - pub global: struct { - pub show_in_game_character: bool, - pub show_in_game_time: bool - }, - pub slippi: struct { - pub enabled: bool, - pub show_queueing: bool, - pub show_opponent_name: bool, - pub ranked: struct { - pub enabled: bool, - pub show_rank: bool, - pub show_view_ranked_profile_button: bool, - pub show_score: bool - }, - pub unranked: struct { - pub enabled: bool - }, - pub direct: struct { - pub enabled: bool - }, - pub teams: struct { - pub enabled: bool - } - }, - pub uncle_punch: struct { - pub enabled: bool - }, - pub vs_mode: struct { - pub enabled: bool - }, - pub training_mode: struct { - pub enabled: bool - }, - pub stadium: struct { - pub enabled: bool, - pub hrc: struct { - pub enabled: bool - }, - pub btt: struct { - pub enabled: bool, - pub show_stage_name: bool - }, - pub mmm: struct { - pub enabled: bool - } - } - } -} - -impl Default for Config { - fn default() -> Self { - Config { - global: Global { - show_in_game_character: true, - show_in_game_time: true, - }, - slippi: Slippi { - enabled: true, - show_queueing: true, - show_opponent_name: true, - ranked: Ranked { - enabled: true, - show_rank: true, - show_view_ranked_profile_button: true, - show_score: true, - }, - unranked: Unranked { enabled: true }, - direct: Direct { enabled: true }, - teams: Teams { enabled: true }, - }, - uncle_punch: UnclePunch { enabled: true }, - vs_mode: VsMode { enabled: true }, - training_mode: TrainingMode { enabled: true }, - stadium: Stadium { - enabled: true, - hrc: Hrc { enabled: true }, - btt: Btt { - enabled: true, - show_stage_name: true, - }, - mmm: Mmm { enabled: true }, - }, - } - } -} - -// Utility implementations -// Commented out for the moment -/* -use crate::melee::SlippiMenuScene; -impl SlippiMenuScene { - pub fn is_enabled(&self, c: &Config) -> bool { - match *self { - SlippiMenuScene::Ranked => c.slippi.ranked.enabled, - SlippiMenuScene::Unranked => c.slippi.unranked.enabled, - SlippiMenuScene::Direct => c.slippi.direct.enabled, - SlippiMenuScene::Teams => c.slippi.teams.enabled - } - } -}*/ diff --git a/discord-rpc/src/discord.rs b/discord-rpc/src/discord.rs deleted file mode 100644 index f0e29a7..0000000 --- a/discord-rpc/src/discord.rs +++ /dev/null @@ -1,293 +0,0 @@ -use discord_rich_presence::{activity::{self, Timestamps, Button}, DiscordIpc, DiscordIpcClient}; - -use crate::{util::current_unix_time, melee::{stage::{MeleeStage, OptionalMeleeStage}, character::{MeleeCharacter, OptionalMeleeCharacter}, MeleeScene, SlippiMenuScene, dolphin_user::get_connect_code}, rank, config::CONFIG}; -use crate::util; - -#[derive(Debug, PartialEq, Clone)] -pub enum DiscordClientRequestType { - Clear, - Queue, - Game, - Mainmenu, - Idle, -} - -#[derive(Debug, PartialEq, Clone)] -pub enum DiscordClientRequestTimestampMode { - None, - Start, - Static, // like Start, but we never update even if the timestamp changes. Used for non-ingame actions. - End -} - -#[derive(Debug, Clone)] -pub struct DiscordClientRequestTimestamp { - pub mode: DiscordClientRequestTimestampMode, - pub timestamp: i64 -} - -impl DiscordClientRequestTimestamp { - pub fn none() -> Self { Self { mode: DiscordClientRequestTimestampMode::None, timestamp: 0 } } -} - -// we ignore this field -impl PartialEq for DiscordClientRequestTimestamp { - fn eq(&self, o: &Self) -> bool { - // if the game was in pause for too long, resynchronize by saying that this payload is not the same as the other. - // To respect the rate limit, we choose a relatively high amount of seconds - self.mode == DiscordClientRequestTimestampMode::Static || self.timestamp.abs_diff(o.timestamp) < 15 - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct DiscordClientRequest { - pub req_type: DiscordClientRequestType, - pub scene: Option, - pub stage: OptionalMeleeStage, - pub character: OptionalMeleeCharacter, - pub mode: String, - pub timestamp: DiscordClientRequestTimestamp, - pub opp_name: Option -} - -impl Default for DiscordClientRequest { - fn default() -> Self { - DiscordClientRequest { - req_type: DiscordClientRequestType::Clear, - scene: None, - stage: OptionalMeleeStage(None), - character: OptionalMeleeCharacter(None), - mode: "".into(), - timestamp: DiscordClientRequestTimestamp { - mode: DiscordClientRequestTimestampMode::Static, - timestamp: current_unix_time(), - }, - opp_name: None - } - } -} - -impl DiscordClientRequest { - pub fn clear() -> Self { Default::default() } - pub fn queue(scene: Option, character: Option) -> Self { - Self { - req_type: DiscordClientRequestType::Queue, - scene, - character: OptionalMeleeCharacter(character), - ..Default::default() - } - } - pub fn idle(scene: Option, character: Option) -> Self { - Self { - req_type: DiscordClientRequestType::Idle, - scene, - character: OptionalMeleeCharacter(character), - ..Default::default() - } - } - pub fn main_menu() -> Self { - Self { - req_type: DiscordClientRequestType::Mainmenu, - ..Default::default() - } - } - pub fn game(stage: Option, character: Option, mode: MeleeScene, timestamp: DiscordClientRequestTimestamp, opp_name: Option) -> Self { - Self { - req_type: DiscordClientRequestType::Game, - stage: OptionalMeleeStage(stage), - character: OptionalMeleeCharacter(character), - mode: mode.to_string(), - timestamp, - opp_name, - ..Default::default() - } - } -} - -pub struct DiscordClient { - client: DiscordIpcClient -} - -impl DiscordClient { - pub fn clear(&mut self) { - self.client.clear_activity().unwrap(); - } - pub async fn queue(&mut self, scene: Option, character: OptionalMeleeCharacter) { - let mut large_image = "slippi".into(); - let mut large_text = "Searching".into(); - let mut buttons = Vec::with_capacity(1); - let mut _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = "".into(); - if CONFIG.with_ref(|c| c.slippi.ranked.show_rank) { - let connect_code_opt = get_connect_code(); - if connect_code_opt.is_some() { - let connect_code = connect_code_opt.unwrap(); - if connect_code.is_valid() { - let fmt_code = connect_code.as_url(); - - let rank_info = rank::get_rank_info(fmt_code.as_str()).await.unwrap(); - large_image = rank_info.name.to_lowercase().replace(" ", "_"); - large_text = format!("{} | {} ELO", rank_info.name, util::round(rank_info.elo, 2)); - if CONFIG.with_ref(|c| c.slippi.ranked.show_view_ranked_profile_button) { - _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = format!("https://slippi.gg/user/{}", fmt_code.as_str()); - buttons.push(Button::new("Get Slippi", "https://slippi.gg/")); - buttons.push(Button::new("View Ranked Profile", _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it.as_str())); - } - } - } - } - - self.client.set_activity( - activity::Activity::new() - .assets({ - let mut activity = activity::Assets::new(); - if !large_image.is_empty() { activity = activity.large_image(large_image.as_str()); } - if !large_text.is_empty() { activity = activity.large_text(large_text.as_str()); } - activity.small_image(character.as_discord_resource().as_str()) - .small_text(character.to_string().as_str()) - }) - .buttons(buttons) - .timestamps(self.current_timestamp()) - .details(scene.and_then(|v| Some(v.to_string())).unwrap_or("".into()).as_str()) - .state("In Queue") - ).unwrap() - - } - pub async fn main_menu(&mut self) { - let mut large_image = "slippi".into(); - let mut large_text = "Idle".into(); - let mut buttons = Vec::with_capacity(1); - let mut _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = "".into(); - if CONFIG.with_ref(|c| c.slippi.ranked.show_rank) { - let connect_code_opt = get_connect_code(); - if connect_code_opt.is_some() { - let connect_code = connect_code_opt.unwrap(); - if connect_code.is_valid() { - let fmt_code = connect_code.as_url(); - - let rank_info = rank::get_rank_info(fmt_code.as_str()).await.unwrap(); - large_image = "slippi"; - large_text = format!("{} | {} ELO", rank_info.name, util::round(rank_info.elo, 2)); - if CONFIG.with_ref(|c| c.slippi.ranked.show_view_ranked_profile_button) { - _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = format!("https://slippi.gg/user/{}", fmt_code.as_str()); - buttons.push(Button::new("Get Slippi", "https://slippi.gg/")); - buttons.push(Button::new("View Ranked Profile", _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it.as_str())); - - } - } - } - } - - self.client.set_activity( - activity::Activity::new() - .assets({ - let mut activity = activity::Assets::new(); - if !large_image.is_empty() { activity = activity.large_image(large_image); } - if !large_text.is_empty() { activity = activity.large_text(large_text.as_str()); } - activity - }) - .buttons(buttons) - .timestamps(self.current_timestamp()) - .details("Super Smash Bros. Melee") - .state("Main Menu") - ).unwrap() - } - - pub async fn idle(&mut self, scene: Option, character: OptionalMeleeCharacter) { - let mut large_image = "slippi".into(); - let mut large_text = "Idle".into(); - let mut buttons = Vec::with_capacity(1); - let mut _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = "".into(); - if CONFIG.with_ref(|c| c.slippi.ranked.show_rank) { - let connect_code_opt = get_connect_code(); - if connect_code_opt.is_some() { - let connect_code = connect_code_opt.unwrap(); - if connect_code.is_valid() { - let fmt_code = connect_code.as_url(); - - let rank_info = rank::get_rank_info(fmt_code.as_str()).await.unwrap(); - large_image = rank_info.name.to_lowercase().replace(" ", "_"); - large_text = format!("{} | {} ELO", rank_info.name, util::round(rank_info.elo, 2)); - if CONFIG.with_ref(|c| c.slippi.ranked.show_view_ranked_profile_button) { - _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = format!("https://slippi.gg/user/{}", fmt_code.as_str()); - buttons.push(Button::new("Get Slippi", "https://slippi.gg/")); - buttons.push(Button::new("View Ranked Profile", _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it.as_str())); - } - } - } - } - - self.client.set_activity( - activity::Activity::new() - .assets({ - let mut activity = activity::Assets::new(); - if !large_image.is_empty() { activity = activity.large_image(large_image.as_str()); } - if !large_text.is_empty() { activity = activity.large_text(large_text.as_str()); } - activity.small_image(character.as_discord_resource().as_str()) - .small_text(character.to_string().as_str()) - }) - .buttons(buttons) - .timestamps(self.current_timestamp()) - .details(scene.and_then(|v| Some(v.to_string())).unwrap_or("".into()).as_str()) - .state("Character Selection Screen") - ).unwrap() - - } - - pub async fn game(&mut self, stage: OptionalMeleeStage, character: OptionalMeleeCharacter, mode: String, timestamp: DiscordClientRequestTimestamp, opp_name: Option) { - let mut large_image = "slippi".into(); - let mut large_text = "Idle".into(); - let mut buttons = Vec::with_capacity(1); - let mut _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = "".into(); - if CONFIG.with_ref(|c| c.slippi.ranked.show_rank) { - let connect_code_opt = get_connect_code(); - if connect_code_opt.is_some() { - let connect_code = connect_code_opt.unwrap(); - if connect_code.is_valid() { - let fmt_code = connect_code.as_url(); - - let rank_info = rank::get_rank_info(fmt_code.as_str()).await.unwrap(); - large_image = rank_info.name.to_lowercase().replace(" ", "_"); - large_text = format!("{} | {} ELO", rank_info.name, util::round(rank_info.elo, 2)); - if CONFIG.with_ref(|c| c.slippi.ranked.show_view_ranked_profile_button) { - _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it = format!("https://slippi.gg/user/{}", fmt_code.as_str()); - buttons.push(Button::new("Get Slippi", "https://slippi.gg/")); - buttons.push(Button::new("View Ranked Profile", _i_unfortunately_have_to_use_this_variable_because_of_rust_but_im_thankful_for_it.as_str())); - } - } - } - } - self.client.set_activity( - activity::Activity::new() - .assets( - activity::Assets::new() - .large_image(stage.as_discord_resource().as_str()) - .large_text(stage.to_string().as_str()) - .small_image(character.as_discord_resource().as_str()) - .small_text(character.to_string().as_str()) - ) - .timestamps( - if timestamp.mode == DiscordClientRequestTimestampMode::None { Timestamps::new() } - else if (timestamp.mode as u8) < (DiscordClientRequestTimestampMode::End as u8) { Timestamps::new().start(timestamp.timestamp) } - else { Timestamps::new().end(timestamp.timestamp) }) - .buttons(buttons) - .details(mode.as_str()) - .state(opp_name.and_then(|n| Some(format!("Playing against {}", n))).unwrap_or("In Game".into()).as_str()) - ).unwrap() - - } - - pub fn close(&mut self) { - self.client.close().unwrap(); - } - - fn current_timestamp(&self) -> Timestamps { - Timestamps::new().start(util::current_unix_time()) - } -} - -pub fn start_client() -> Result> { - let mut client = DiscordIpcClient::new("1096595344600604772")?; - client.connect()?; - - Ok(DiscordClient { client }) -} \ No newline at end of file diff --git a/discord-rpc/src/error.rs b/discord-rpc/src/error.rs deleted file mode 100644 index 16d8dbe..0000000 --- a/discord-rpc/src/error.rs +++ /dev/null @@ -1,22 +0,0 @@ -use thiserror::Error; - -use crate::Message; - -/// Any error type that can be raised by this library. -#[derive(Error, Debug)] -pub enum DiscordRPCError { - #[error("{0}")] - GenericIO(#[from] std::io::Error), - - #[error("Failed to spawn thread: {0}")] - ThreadSpawn(std::io::Error), - - #[error("The channel receiver has disconnected, implying that the data could never be received.")] - ChannelReceiverDisconnected(#[from] std::sync::mpsc::SendError), - - #[error("The channel sender has disconnected, implying no further messages will be received.")] - ChannelSenderDisconnected(#[from] std::sync::mpsc::RecvError), - - #[error("Unknown DiscordRPC Error")] - Unknown, -} diff --git a/discord-rpc/src/errors.rs b/discord-rpc/src/errors.rs new file mode 100644 index 0000000..ec10307 --- /dev/null +++ b/discord-rpc/src/errors.rs @@ -0,0 +1,37 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DiscordRPCError { + #[error("{0}")] + GenericIO(#[from] std::io::Error), + + #[error("Failed to spawn thread: {0}")] + ThreadSpawn(std::io::Error), + + #[error("Unexpected null pointer or unaligned read from Dolphin's memory: {0}")] + DolphinMemoryRead(std::io::Error), + + #[error("Failed to decode music file: {0}")] + MusicFileDecoding(#[from] hps_decode::hps::HpsParseError), + + #[error("Unable to get an audio device handle: {0}")] + AudioDevice(#[from] rodio::StreamError), + + #[error("Unable to play sound with rodio: {0}")] + AudioPlayback(#[from] rodio::PlayError), + + #[error("Failed to parse ISO's Filesystem Table: {0}")] + FstParse(String), + + #[error("Failed to seek the ISO: {0}")] + IsoSeek(std::io::Error), + + #[error("Failed to read the ISO: {0}")] + IsoRead(std::io::Error), + + #[error("The provided game file is not supported")] + UnsupportedIso, + + #[error("Unknown Jukebox Error")] + Unknown, +} \ No newline at end of file diff --git a/discord-rpc/src/lib.rs b/discord-rpc/src/lib.rs index e80b0a0..bfc2259 100644 --- a/discord-rpc/src/lib.rs +++ b/discord-rpc/src/lib.rs @@ -1,107 +1,313 @@ -//! This module implements native Discord integration for Slippi. -//! -//! The core of it runs in a background thread, listening for new -//! events on each pass of its own loop. - +use std::convert::TryInto; +use std::fs::File; +use std::ops::ControlFlow::{self, Break, Continue}; use std::sync::mpsc::{channel, Receiver, Sender}; -use std::thread; +use std::{thread::sleep, time::Duration}; + +use dolphin_integrations::{Color, Dolphin, Duration as OSDDuration, Log}; +use hps_decode::{Hps, PcmIterator}; +use process_memory::{LocalMember, Memory}; +use rodio::{OutputStream, Sink}; + +mod errors; +pub use errors::DiscordRPCError; +use DiscordRPCError::*; -use dolphin_integrations::Log; +mod scenes; +use scenes::scene_ids::*; -mod config; -pub use config::Config; -mod error; -pub use error::DiscordRPCError; +mod utils; pub(crate) type Result = std::result::Result; -/// Message payloads that the inner thread listens for. +/// Represents a foreign method from the Dolphin side for grabbing the current volume. +/// Dolphin represents this as a number from 0 - 100; 0 being mute. +pub type ForeignGetVolumeFn = unsafe extern "C" fn() -> std::ffi::c_int; + +const THREAD_LOOP_SLEEP_TIME_MS: u64 = 30; +const CHILD_THREAD_COUNT: usize = 2; + +/// By default Slippi DiscordRPC plays music slightly louder than vanilla melee +/// does. This reduces the overall music volume output to 80%. Not totally sure +/// if that's the correct amount, but it sounds about right. +const VOLUME_REDUCTION_MULTIPLIER: f32 = 0.8; + +#[derive(Debug, PartialEq)] +struct DolphinGameState { + in_game: bool, + in_menus: bool, + scene_major: u8, + scene_minor: u8, + stage_id: u8, + volume: f32, + is_paused: bool, + match_info: u8, +} + +impl Default for DolphinGameState { + fn default() -> Self { + Self { + in_game: false, + in_menus: false, + scene_major: SCENE_MAIN_MENU, + scene_minor: 0, + stage_id: 0, + volume: 0.0, + is_paused: false, + match_info: 0, + } + } +} + #[derive(Debug)] -pub enum Message { - Dropping, - UpdateConfig(Config), +enum MeleeEvent { + TitleScreenEntered, + MenuEntered, + LotteryEntered, + GameStart(u8), // stage id + GameEnd, + RankedStageStrikeEntered, + VsOnlineOpponent, + Pause, + Unpause, + SetVolume(f32), + NoOp, +} + +#[derive(Debug, Clone)] +enum DiscordRPCEvent { + Dropped, } -/// A client that watches for game events and emits status updates to -/// Discord. This is effectively just a message passing route for the -/// background thread, which does all the actual work. #[derive(Debug)] -pub struct DiscordHandler { - tx: Sender, +pub struct DiscordRPC { + channel_senders: [Sender; CHILD_THREAD_COUNT], } -impl DiscordHandler { - /// Kicks off the background thread, which monitors game state and emits - /// updates to Discord accordingly. - pub fn new(ram_offset: usize, config: Config) -> Result { - tracing::info!(target: Log::DiscordRPC, "Initializing DiscordHandler"); +impl DiscordRPC { + /// Returns a DiscordRPC instance that will immediately spawn two child threads + /// to try and read game memory and play music. When the returned instance is + /// dropped, the child threads will terminate and the music will stop. + pub fn new(m_p_ram: *const u8, iso_path: String, get_dolphin_volume_fn: ForeignGetVolumeFn) -> Result { + tracing::info!(target: Log::DiscordRPC, "Initializing Slippi Discord RPC"); + + // We are implicitly trusting that these pointers will outlive the jukebox instance + let m_p_ram = m_p_ram as usize; + let get_dolphin_volume = move || unsafe { get_dolphin_volume_fn() } as f32 / 100.0; - // Create a sender and receiver channel pair to communicate between threads. - let (tx, rx) = channel::(); + // This channel is used for the `DiscordRPCMessageDispatcher` thread to send + // messages to the `DiscordRPCMusicPlayer` thread + let (melee_event_tx, melee_event_rx) = channel::(); - // Spawn a new background thread that manages its own loop. If or when - // the loop breaks - either due to shutdown or intentional drop - the underlying - // OS thread will clean itself up. - thread::Builder::new() - .name("DiscordHandler".to_string()) + // These channels allow the jukebox instance to notify both child + // threads when something important happens. Currently its only purpose + // is to notify them that the instance is about to be dropped so they + // should terminate + let (message_dispatcher_thread_tx, message_dispatcher_thread_rx) = channel::(); + let (music_thread_tx, music_thread_rx) = channel::(); + + // Spawn message dispatcher thread + std::thread::Builder::new() + .name("DiscordRPCMessageDispatcher".to_string()) .spawn(move || { - if let Err(e) = Self::start(rx, ram_offset, config) { - tracing::error!( + match Self::dispatch_messages(m_p_ram, get_dolphin_volume, message_dispatcher_thread_rx, melee_event_tx) { + Err(e) => tracing::error!( target: Log::DiscordRPC, error = ?e, - "DiscordHandler thread encountered an error: {e}" - ); + "DiscordRPCMessageDispatcher thread encountered an error: {e}" + ), + _ => (), } }) - .map_err(error::DiscordRPCError::ThreadSpawn)?; + .map_err(ThreadSpawn)?; - Ok(Self { tx }) + } - /// Must be called on a background thread. Runs the core event loop. - fn start(rx: Receiver, ram_offset: usize, config: Config) -> Result<()> { + /// This thread continuously reads select values from game memory as well + /// as the current `volume` value in the dolphin configuration. If it + /// notices anything change, it will dispatch a message to the + /// `DiscordRPCMusicPlayer` thread. + fn dispatch_messages( + m_p_ram: usize, + get_dolphin_volume: impl Fn() -> f32, + message_dispatcher_thread_rx: Receiver, + melee_event_tx: Sender, + ) -> Result<()> { + // Initial "dolphin state" that will get updated over time + let mut prev_state = DolphinGameState::default(); + loop { - match rx.recv()? { - // Handle any configuration updates. - Message::UpdateConfig(config) => {}, - - // Just break the loop so things exit cleanly. - Message::Dropping => { - break; - }, + // Stop the thread if the jukebox instance will be been dropped + if let Ok(event) = message_dispatcher_thread_rx.try_recv() { + if matches!(event, DiscordRPCEvent::Dropped) { + return Ok(()); + } + } + + // Continuously check if the dolphin state has changed + let state = Self::read_dolphin_game_state(&m_p_ram, get_dolphin_volume())?; + + // If the state has changed, + if prev_state != state { + // dispatch a message to the music player thread + let event = Self::produce_melee_event(&prev_state, &state); + tracing::info!(target: Log::DiscordRPC, "{:?}", event); + + melee_event_tx.send(event).ok(); + prev_state = state; } + + sleep(Duration::from_millis(THREAD_LOOP_SLEEP_TIME_MS)); } + } + + /// This thread listens for incoming messages from the + /// `DiscordRPCMessageDispatcher` thread and handles music playback + /// accordingly. + - Ok(()) + /// Handle a events received in the audio playback thread, by changing tracks, + /// adjusting volume etc. + fn handle_melee_event( + event: MeleeEvent, + sink: &Sink, + volume: &mut f32, + ) -> ControlFlow<()> { + use self::MeleeEvent::*; + + // TODO: + // - Intro movie + // + // - classic vs screen + // - classic victory screen + // - classic game over screen + // - classic credits + // - classic "congratulations movie" + // - Adventure mode field intro music + + match event { + TitleScreenEntered | GameEnd => { + + NoOp; + }, + MenuEntered => { + + NoOp; + }, + LotteryEntered => { + NoOp; + }, + VsOnlineOpponent => { + NoOp; + }, + RankedStageStrikeEntered => { + NoOp; + }, + GameStart(stage_id) => { + NoOp; + }, + Pause => { + sink.set_volume(*volume * 0.2); + return Continue(()); + }, + Unpause => { + sink.set_volume(*volume); + return Continue(()); + }, + SetVolume(received_volume) => { + sink.set_volume(received_volume); + *volume = received_volume; + return Continue(()); + }, + NoOp => { + return Continue(()); + }, + }; + + Break(()) } - /// Passes a new configuration into the background handler. - pub fn update_config(&mut self, config: Config) { - if let Err(e) = self.tx.send(Message::UpdateConfig(config)) { - // @TODO: Maybe add an OSD log message here? + /// Given the previous dolphin state and current dolphin state, produce an event + fn produce_melee_event(prev_state: &DolphinGameState, state: &DolphinGameState) -> MeleeEvent { + let vs_screen_1 = state.scene_major == SCENE_VS_ONLINE + && prev_state.scene_minor != SCENE_VS_ONLINE_VERSUS + && state.scene_minor == SCENE_VS_ONLINE_VERSUS; + let vs_screen_2 = prev_state.scene_minor == SCENE_VS_ONLINE_VERSUS && state.stage_id == 0; + let entered_vs_online_opponent_screen = vs_screen_1 || vs_screen_2; + + if state.scene_major == SCENE_VS_ONLINE + && prev_state.scene_minor != SCENE_VS_ONLINE_RANKED + && state.scene_minor == SCENE_VS_ONLINE_RANKED + { + MeleeEvent::RankedStageStrikeEntered + } else if !prev_state.in_menus && state.in_menus { + MeleeEvent::MenuEntered + } else if prev_state.scene_major != SCENE_TITLE_SCREEN && state.scene_major == SCENE_TITLE_SCREEN { + MeleeEvent::TitleScreenEntered + } else if entered_vs_online_opponent_screen { + MeleeEvent::VsOnlineOpponent + } else if prev_state.scene_major != SCENE_TROPHY_LOTTERY && state.scene_major == SCENE_TROPHY_LOTTERY { + MeleeEvent::LotteryEntered + } else if (!prev_state.in_game && state.in_game) || prev_state.stage_id != state.stage_id { + MeleeEvent::GameStart(state.stage_id) + } else if prev_state.in_game && state.in_game && state.match_info == 1 { + MeleeEvent::GameEnd + } else if prev_state.volume != state.volume { + MeleeEvent::SetVolume(state.volume) + } else if !prev_state.is_paused && state.is_paused { + MeleeEvent::Pause + } else if prev_state.is_paused && !state.is_paused { + MeleeEvent::Unpause + } else { + MeleeEvent::NoOp + } + } - tracing::error!( - target: Log::DiscordRPC, - error = ?e, - "Failed to update DiscordHandler config" - ); + /// Create a `DolphinGameState` by reading Dolphin's memory + fn read_dolphin_game_state(m_p_ram: &usize, dolphin_volume_percent: f32) -> Result { + #[inline(always)] + fn read(offset: usize) -> Result { + Ok(unsafe { LocalMember::::new_offset(vec![offset]).read().map_err(DolphinMemoryRead)? }) } + // https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L8 + let melee_volume_percent = ((read::(m_p_ram + 0x45C384)? as f32 - 100.0) * -1.0) / 100.0; + // https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L16 + let scene_major = read::(m_p_ram + 0x479D30)?; + // https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L19 + let scene_minor = read::(m_p_ram + 0x479D33)?; + // https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L357 + let stage_id = read::(m_p_ram + 0x49E753)?; + // https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L248 + // 0 = in game, 1 = GAME! screen, 2 = Stage clear in 1p mode? (maybe also victory screen), 3 = menu + let match_info = read::(m_p_ram + 0x46B6A0)?; + // https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L353 + let is_paused = read::(m_p_ram + 0x4D640F)? == 1; + + Ok(DolphinGameState { + in_game: utils::is_in_game(scene_major, scene_minor), + in_menus: utils::is_in_menus(scene_major, scene_minor), + scene_major, + scene_minor, + volume: dolphin_volume_percent * melee_volume_percent * VOLUME_REDUCTION_MULTIPLIER, + stage_id, + is_paused, + match_info, + }) } } -impl Drop for DiscordHandler { - /// Notifies the background thread that we're dropping. The thread should - /// listen for the message and break its runloop accordingly. +impl Drop for DiscordRPC { fn drop(&mut self) { - tracing::info!(target: Log::DiscordRPC, "Dropping DiscordHandler"); - - if let Err(e) = self.tx.send(Message::Dropping) { - tracing::warn!( - target: Log::DiscordRPC, - error = ?e, - "Failed to notify child thread that DiscordHandler is dropping" - ); + tracing::info!(target: Log::DiscordRPC, "Dropping Slippi DiscordRPC"); + for sender in &self.channel_senders { + if let Err(e) = sender.send(DiscordRPCEvent::Dropped) { + tracing::warn!( + target: Log::DiscordRPC, + "Failed to notify child thread that DiscordRPC is dropping: {e}" + ); + } } } } diff --git a/discord-rpc/src/melee/character.rs b/discord-rpc/src/melee/character.rs deleted file mode 100644 index a7689df..0000000 --- a/discord-rpc/src/melee/character.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::fmt::Display; - -use num_enum::TryFromPrimitive; - -#[derive(Debug, Clone, Copy, PartialEq, TryFromPrimitive)] -#[repr(u8)] -pub enum MeleeCharacter { - DrMario = 0x16, - Mario = 0x08, - Luigi = 0x07, - Bowser = 0x05, - Peach = 0x0C, - Yoshi = 0x11, - DonkeyKong = 0x01, - CaptainFalcon = 0x00, - Ganondorf = 0x19, - Falco = 0x14, - Fox = 0x02, - Ness = 0x0B, - IceClimbers = 0x0E, - Kirby = 0x04, - Samus = 0x10, - Zelda = 0x12, - Sheik = 0x13, - Link = 0x06, - YoungLink = 0x15, - Pichu = 0x18, - Pikachu = 0x0D, - Jigglypuff = 0x0F, - Mewtwo = 0x0A, - MrGameAndWatch = 0x03, - Marth = 0x09, - Roy = 0x17, - Hidden = 0xFF -} - -impl MeleeCharacter { - // useful when fetching from player card character address, however remains unused for now - pub fn from_css(css_index: u8) -> Option { - match css_index { - 0 => Some(Self::DrMario), - 1 => Some(Self::Mario), - 2 => Some(Self::Luigi), - 3 => Some(Self::Bowser), - 4 => Some(Self::Peach), - 5 => Some(Self::Yoshi), - 6 => Some(Self::DonkeyKong), - 7 => Some(Self::CaptainFalcon), - 8 => Some(Self::Ganondorf), - 9 => Some(Self::Falco), - 10 => Some(Self::Fox), - 11 => Some(Self::Ness), - 12 => Some(Self::IceClimbers), - 13 => Some(Self::Kirby), - 14 => Some(Self::Samus), - 15 => Some(Self::Zelda), - 16 => Some(Self::Link), - 17 => Some(Self::YoungLink), - 18 => Some(Self::Pichu), - 19 => Some(Self::Pikachu), - 20 => Some(Self::Jigglypuff), - 21 => Some(Self::Mewtwo), - 22 => Some(Self::MrGameAndWatch), - 23 => Some(Self::Marth), - 24 => Some(Self::Roy), - _ => None - } - } -} - -impl Display for MeleeCharacter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match *self { - Self::DrMario => write!(f, "Dr. Mario"), - Self::Mario => write!(f, "Mario"), - Self::Luigi => write!(f, "Luigi"), - Self::Bowser => write!(f, "Bowser"), - Self::Peach => write!(f, "Peach"), - Self::Yoshi => write!(f, "Yoshi"), - Self::DonkeyKong => write!(f, "Donkey Kong"), - Self::CaptainFalcon => write!(f, "Captain Falcon"), - Self::Ganondorf => write!(f, "Ganondorf"), - Self::Falco => write!(f, "Falco"), - Self::Fox => write!(f, "Fox"), - Self::Ness => write!(f, "Ness"), - Self::IceClimbers => write!(f, "Ice Climbers"), - Self::Kirby => write!(f, "Kirby"), - Self::Samus => write!(f, "Samus"), - Self::Zelda => write!(f, "Zelda"), - Self::Sheik => write!(f, "Sheik"), - Self::Link => write!(f, "Link"), - Self::YoungLink => write!(f, "Young Link"), - Self::Pichu => write!(f, "Pichu"), - Self::Pikachu => write!(f, "Pikachu"), - Self::Jigglypuff => write!(f, "Jigglypuff"), - Self::Mewtwo => write!(f, "Mewtwo"), - Self::MrGameAndWatch => write!(f, "Mr. Game & Watch"), - Self::Marth => write!(f, "Marth"), - Self::Roy => write!(f, "Roy"), - Self::Hidden => write!(f, "Hidden") - } - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct OptionalMeleeCharacter(pub Option); -impl OptionalMeleeCharacter { - pub fn as_discord_resource(&self) -> String { - self.0.as_ref().and_then(|c| - if *c == MeleeCharacter::Hidden { Some("transparent".into()) } - else { Some(format!("char{}", (*c) as u8) ) } - ).unwrap_or("questionmark".into()) - } -} -impl Display for OptionalMeleeCharacter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &(*self).0 { - Some(v) => write!(f, "{}", v), - _ => write!(f, "Unknown character") - } - } -} diff --git a/discord-rpc/src/melee/dolphin_mem.rs b/discord-rpc/src/melee/dolphin_mem.rs deleted file mode 100644 index a790b2d..0000000 --- a/discord-rpc/src/melee/dolphin_mem.rs +++ /dev/null @@ -1,233 +0,0 @@ -use std::ffi::c_void; -use std::mem; -use std::str::from_utf8_unchecked; - -use encoding_rs::SHIFT_JIS; -use windows::Win32::Foundation::ERROR_PARTIAL_COPY; -use windows::Win32::Foundation::GetLastError; -use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory; -use windows::Win32::System::Memory::MEMORY_BASIC_INFORMATION; -use windows::Win32::System::Memory::VirtualQueryEx; -use windows::Win32::System::ProcessStatus::PSAPI_WORKING_SET_EX_BLOCK; -use windows::Win32::System::ProcessStatus::PSAPI_WORKING_SET_EX_INFORMATION; -use windows::Win32::System::ProcessStatus::QueryWorkingSetEx; -use windows::Win32::{System::{Diagnostics::ToolHelp::{CreateToolhelp32Snapshot, PROCESSENTRY32, TH32CS_SNAPPROCESS, Process32Next}, Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, GetExitCodeProcess}}, Foundation::{STILL_ACTIVE, HANDLE, CloseHandle}}; - -const VALID_PROCESS_NAMES: &'static [&'static str] = &["Dolphin.exe", "Slippi Dolphin.exe", "Slippi_Dolphin.exe", "DolphinWx.exe", "DolphinQt2.exe"]; -const GC_RAM_START: u32 = 0x80000000; -const GC_RAM_END: u32 = 0x81800000; -const GC_RAM_SIZE: usize = 0x2000000; -const MEM_MAPPED: u32 = 0x40000; - -pub struct DolphinMemory { - process_handle: Option, - dolphin_base_addr: Option<*mut c_void>, - dolphin_addr_size: Option -} - -impl DolphinMemory { - pub fn new() -> Self { - DolphinMemory { process_handle: None, dolphin_base_addr: None, dolphin_addr_size: None } - } - - pub fn find_process(&mut self) -> bool { - unsafe { - let mut status: u32 = 0; - let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).unwrap(); - let mut pe32 = PROCESSENTRY32 { - dwSize: mem::size_of::() as u32, - cntUsage: 0, - th32ProcessID: 0, - th32DefaultHeapID: 0, - th32ModuleID: 0, - cntThreads: 0, - th32ParentProcessID: 0, - pcPriClassBase: 0, - dwFlags: 0, - szExeFile: [0; 260] - }; - - loop { - if !Process32Next(snapshot, &mut pe32 as *mut _).as_bool() { - break; - } - let name = from_utf8_unchecked(&pe32.szExeFile); - if VALID_PROCESS_NAMES.iter().any(|&e| name.starts_with(e)) { - println!("{}", name); - let handle_res = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pe32.th32ProcessID); - if handle_res.is_ok() { - let handle = handle_res.unwrap(); - if GetExitCodeProcess(handle, &mut status as *mut _).as_bool() && status as i32 == STILL_ACTIVE.0 { - self.process_handle = Some(handle); - break; - } - } else { - // ? handle is supposed to be null so what will be closed... ported from m-overlay, see reference on the top - CloseHandle(handle_res.unwrap()); - self.process_handle = None; - } - } else { - self.process_handle = None; - } - } - CloseHandle(snapshot); - return self.has_process(); - } - } - - pub fn has_process(&self) -> bool { - self.process_handle.is_some() - } - - pub fn check_process_running(&mut self) -> bool { - if self.process_handle.is_none() { - return false; - } - - let mut status: u32 = 0; - unsafe { - if GetExitCodeProcess(self.process_handle.unwrap(), &mut status as *mut _).as_bool() && status as i32 != STILL_ACTIVE.0 { - self.reset(); - return false; - } - } - return true; - } - - pub fn read(&mut self, addr: u32) -> Option where [u8; mem::size_of::()]:{ - if !self.has_process() || (!self.has_gamecube_ram_offset() && !self.find_gamecube_ram_offset()) { - return None; - } - - let mut addr = addr; - if addr >= GC_RAM_START && addr <= GC_RAM_END { - addr -= GC_RAM_START; - } else { - println!("[MEMORY] Attempt to read from invalid address {:#08x}", addr); - return None; - } - - let raddr = self.dolphin_base_addr.unwrap() as usize + addr as usize; - let mut output = [0u8; mem::size_of::()]; - let size = mem::size_of::(); - let mut memread: usize = 0; - - unsafe { - let success = ReadProcessMemory(self.process_handle.unwrap(), raddr as *const c_void, &mut output as *mut _ as *mut c_void, size, Some(&mut memread as *mut _)); - if success.as_bool() && memread == size { - // because win32 decides to give me the output in the wrong endianness, we'll reverse it - output.reverse(); // TODO figure out if we really have to do this, i would like to avoid it if possible - return Some(mem::transmute_copy(&output)); - } else { - let err = GetLastError().0; - println!("[MEMORY] Failed reading from address {:#08X} ERROR {}", addr, err); - if err == ERROR_PARTIAL_COPY.0 { // game probably closed, reset the dolphin ram offset - self.dolphin_addr_size = None; - self.dolphin_base_addr = None; - } - return None; - } - } - } - - pub fn read_string(&mut self, addr: u32) -> Option where [(); mem::size_of::<[u8; LEN]>()]:{ - let res = self.read::<[u8; LEN]>(addr); - if res.is_none() { - return None; - } - - let mut raw = res.unwrap(); - raw.reverse(); // we apparently have to reverse it again due to how the string is gathered - - return match std::str::from_utf8(&raw) { - Ok(v) => Some(v.trim_end_matches(char::from(0)).into()), - Err(e) => { - println!("Invalid utf-8 string => {:?} | {}", res.unwrap(), e.to_string()); - None - } - }; - } - - pub fn read_string_shift_jis(&mut self, addr: u32) -> Option where [(); mem::size_of::<[u8; LEN]>()]:{ - let res = self.read::<[u8; LEN]>(addr); - if res.is_none() { - return None; - } - - let mut raw = res.unwrap(); - raw.reverse(); // we apparently have to reverse it again due to how the string is gathered - - let (dec_res, _enc, errors) = SHIFT_JIS.decode(&raw); - if errors { - println!("Invalid shift-jis string => {:?}", res.unwrap()) - } - return Some(dec_res.as_ref().trim_end_matches(char::from(0)).to_string()); - } - - pub fn pointer_indirection(&mut self, addr: u32, amount: u32) -> Option { - let mut curr = self.read::(addr); - for n in 2..=amount { - if curr.is_none() { - return None; - } - curr = self.read::(curr.unwrap()); - } - curr - } - - /*pub fn write(&self) { - - }*/ - - fn find_gamecube_ram_offset(&mut self) -> bool { - if !self.has_process() { - return false; - } - - unsafe { - let mut info: MEMORY_BASIC_INFORMATION = Default::default(); - let mut address: usize = 0; - - while VirtualQueryEx(self.process_handle.unwrap(), Some(address as *const c_void), &mut info as *mut _, mem::size_of::()) == mem::size_of::() { - address = address + info.RegionSize / mem::size_of::(); - // Dolphin stores the GameCube RAM address space in 32MB chunks. - // Extended memory override can allow up to 64MB. - if info.RegionSize >= GC_RAM_SIZE && info.RegionSize % GC_RAM_SIZE == 0 && info.Type.0 == MEM_MAPPED { - let mut wsinfo = PSAPI_WORKING_SET_EX_INFORMATION { - VirtualAddress: 0 as *mut c_void, - VirtualAttributes: PSAPI_WORKING_SET_EX_BLOCK { Flags: 0 } - }; - wsinfo.VirtualAddress = info.BaseAddress; - - if QueryWorkingSetEx(self.process_handle.unwrap(), &mut wsinfo as *mut _ as *mut c_void, mem::size_of::().try_into().unwrap()).as_bool() { - if (wsinfo.VirtualAttributes.Flags & 1) == 1 && info.BaseAddress != 0 as *mut c_void { - self.dolphin_base_addr = Some(info.BaseAddress); - self.dolphin_addr_size = Some(info.RegionSize); - - println!("Dolphin Base Address: {:?}", self.dolphin_base_addr); - println!("Dolphin Address Size: {:?}", self.dolphin_addr_size); - return true; - } - } - } - } - } - - return false; - } - - fn has_gamecube_ram_offset(&self) -> bool { - self.dolphin_base_addr.is_some() - } - - fn reset(&mut self) { - self.process_handle = None; - self.dolphin_base_addr = None; - self.dolphin_addr_size = None; - } -} - -pub mod util { - macro_rules! R13 {($offset:expr) => { 0x804db6a0 - $offset }} - pub(crate) use R13; -} diff --git a/discord-rpc/src/melee/dolphin_user.rs b/discord-rpc/src/melee/dolphin_user.rs deleted file mode 100644 index a9ca5f9..0000000 --- a/discord-rpc/src/melee/dolphin_user.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::fs; - -use lazy_static::lazy_static; -use regex::Regex; -use serde_json::Value; -use crate::util::get_appdata_file; - -pub struct ConnectCode(String); -impl ConnectCode { - pub fn is_valid(&self) -> bool { - lazy_static! { - static ref RE: Regex = Regex::new("^([A-Za-z0-9])+#[0-9]{1,6}$").unwrap(); - } - RE.is_match(self.0.as_str()) - } - - pub fn as_url(&self) -> String { - self.0.to_lowercase().replace("#", "-") - } -} - -pub fn get_connect_code() -> Option { - if let Some(user_json_path) = get_appdata_file("Slippi Launcher/netplay/User/Slippi/user.json") { - if user_json_path.is_file() && user_json_path.exists() { - return fs::read_to_string(user_json_path).ok().and_then(|data| { - match serde_json::from_str::(data.as_str()) { - Ok(data) => data["connectCode"].as_str().and_then(|v| Some(ConnectCode(v.into()))), - _ => None - } - }); - } - } - None -} \ No newline at end of file diff --git a/discord-rpc/src/melee/mod.rs b/discord-rpc/src/melee/mod.rs deleted file mode 100644 index bc03c9c..0000000 --- a/discord-rpc/src/melee/mod.rs +++ /dev/null @@ -1,327 +0,0 @@ -use std::{fmt::Display}; - -use num_enum::TryFromPrimitive; -use strum::{IntoEnumIterator}; -use strum_macros::{Display, EnumIter}; -use tokio_util::sync::CancellationToken; - -use crate::{discord::{DiscordClientRequest, DiscordClientRequestType, DiscordClientRequestTimestamp, DiscordClientRequestTimestampMode}, util::{current_unix_time, sleep}, melee::{stage::MeleeStage, character::MeleeCharacter}, config::{CONFIG}, tray::MeleeTrayEvent}; - -use self::{dolphin_mem::{DolphinMemory, util::R13}, msrb::MSRBOffset, multiman::MultiManVariant}; - -mod dolphin_mem; -mod msrb; -mod multiman; -pub mod stage; -pub mod character; -pub mod dolphin_user; - -// reference: https://github.com/akaneia/m-ex/blob/master/MexTK/include/match.h#L11-L14 -#[derive(PartialEq, EnumIter, Clone, Copy)] -enum TimerMode { - Countup = 3, - Countdown = 2, - Hidden = 1, - Frozen = 0, -} - -#[derive(TryFromPrimitive, Display, Debug)] -#[repr(u8)] -enum MatchmakingMode { - Idle = 0, - Initializing = 1, - Matchmaking = 2, - OpponentConnecting = 3, - ConnectionSuccess = 4, - ErrorEncountered = 5 -} - -#[derive(Debug, TryFromPrimitive, Display, PartialEq, Clone, Copy)] -#[repr(u8)] -pub enum SlippiMenuScene { - Ranked = 0, - Unranked = 1, - Direct = 2, - Teams = 3 -} - -pub struct MeleeClient { - mem: DolphinMemory, - last_payload: DiscordClientRequest, - last_tray_event: MeleeTrayEvent -} - -#[derive(PartialEq, Clone, Copy,Debug)] -pub enum MeleeScene { - MainMenu, - VsMode, - UnclePunch, - TrainingMode, - SlippiOnline(Option), - SlippiCss(Option), - HomeRunContest, - TargetTest(Option), - MultiManMelee(MultiManVariant) -} - -impl Display for MeleeScene { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match *self { - Self::MainMenu => write!(f, "Main Menu"), - Self::VsMode => write!(f, "Vs. Mode"), - Self::UnclePunch => write!(f, "UnclePunch Training Mode"), - Self::TrainingMode => write!(f, "Training Mode"), - Self::SlippiOnline(Some(scene)) => write!(f, "{}", scene), - Self::SlippiOnline(None) => write!(f, "Slippi Online"), - Self::HomeRunContest => write!(f, "Home-Run Contest"), - Self::TargetTest(stage_opt) => { - if stage_opt.is_some() && CONFIG.with_ref(|c| c.stadium.btt.show_stage_name) { - write!(f, "{}", stage_opt.unwrap()) - } else { - write!(f, "Target Test") - } - }, - Self::MultiManMelee(variant) => write!(f, "Multi-Man Melee ({})", match variant { - MultiManVariant::TenMan => "10 man", - MultiManVariant::HundredMan => "100 man", - MultiManVariant::ThreeMinute => "3 min", - MultiManVariant::FifteenMinute => "15 min", - MultiManVariant::Endless => "Endless", - MultiManVariant::Cruel => "Cruel", - }), - Self::SlippiCss(_) => unimplemented!(), - } - } -} - -impl MeleeClient { - pub fn new() -> Self { - MeleeClient { mem: DolphinMemory::new(), last_payload: DiscordClientRequest::clear(), last_tray_event: MeleeTrayEvent::Disconnected } - } - - fn get_player_port(&mut self) -> Option { self.mem.read::(R13!(0x5108)) } - fn get_slippi_player_port(&mut self) -> Option { self.mem.read_msrb(MSRBOffset::MsrbLocalPlayerIndex) } - fn get_opp_name(&mut self) -> Option { self.mem.read_msrb_string::<31>(MSRBOffset::MsrbOppName) } - fn get_player_connect_code(&mut self, port: u8) -> Option { - const PLAYER_CONNECTCODE_OFFSETS: [MSRBOffset; 4] = [MSRBOffset::MsrbP1ConnectCode, MSRBOffset::MsrbP2ConnectCode, MSRBOffset::MsrbP3ConnectCode, MSRBOffset::MsrbP4ConnectCode]; - self.mem.read_msrb_string_shift_jis::<10>(PLAYER_CONNECTCODE_OFFSETS[port as usize]) - } - fn get_character_selection(&mut self, port: u8) -> Option { - // 0x04 = character, 0x05 = skin (reference: https://github.com/bkacjios/m-overlay/blob/master/source/modules/games/GALE01-2.lua#L199-L202) - const PLAYER_SELECTION_BLOCKS: [u32; 4] = [0x8043208B, 0x80432093, 0x8043209B, 0x804320A3]; - self.mem.read::(PLAYER_SELECTION_BLOCKS[port as usize] + 0x04).and_then(|v| MeleeCharacter::try_from(v).ok()) - } - fn timer_mode(&mut self) -> TimerMode { - const MATCH_INIT: u32 = 0x8046DB68; // first byte, reference: https://github.com/akaneia/m-ex/blob/master/MexTK/include/match.h#L136 - self.mem.read::(MATCH_INIT).and_then(|v| { - for timer_mode in TimerMode::iter() { - let val = timer_mode as u8; - if v & val == val { - return Some(timer_mode); - } - } - None - }).unwrap_or(TimerMode::Countup) - } - fn game_time(&mut self) -> i64 { self.mem.read::(0x8046B6C8).and_then(|v| Some(v)).unwrap_or(0) as i64 } - fn matchmaking_type(&mut self) -> Option { - self.mem.read_msrb::(MSRBOffset::MsrbConnectionState).and_then(|v| MatchmakingMode::try_from(v).ok()) - } - fn slippi_online_scene(&mut self) -> Option { self.mem.read::(R13!(0x5060)).and_then(|v| SlippiMenuScene::try_from(v).ok()) } - /*fn game_variant(&mut self) -> Option { - const GAME_ID_ADDR: u32 = 0x80000000; - const GAME_ID_LEN: usize = 0x06; - - let game_id = self.mem.read_string::(GAME_ID_ADDR); - if game_id.is_none() { - return None; - } - return match game_id.unwrap().as_str() { - "GALE01" => Some(MeleeGameVariant::Vanilla), - "GTME01" => Some(MeleeGameVariant::UnclePunch), - _ => None - } - }*/ - - - fn get_melee_scene(&mut self) -> Option { - const MAJOR_SCENE: u32 = 0x80479D30; - const MINOR_SCENE: u32 = 0x80479D33; - let scene_tuple = (self.mem.read::(MAJOR_SCENE).unwrap_or(0), self.mem.read::(MINOR_SCENE).unwrap_or(0)); - - // Print the scene_tuple to the console - println!("Major Scene: {:?}", self.mem.read::(MAJOR_SCENE).unwrap_or(0)); - println!("Minor Scene: {:?}", self.mem.read::(MAJOR_SCENE).unwrap_or(0)); - match scene_tuple { - (0, 0) => Some(MeleeScene::MainMenu), - (1, 0) => Some(MeleeScene::MainMenu), - (1, 1) => Some(MeleeScene::MainMenu), - (2, 2) => Some(MeleeScene::VsMode), - (43, 1) => Some(MeleeScene::UnclePunch), - (28, 2) => Some(MeleeScene::TrainingMode), - (28, 28) => Some(MeleeScene::TrainingMode), - (8, 2) => Some(MeleeScene::SlippiOnline(self.slippi_online_scene())), - (8, 0) => Some(MeleeScene::SlippiCss(self.slippi_online_scene())), - (8, 8) => Some(MeleeScene::SlippiCss(self.slippi_online_scene())), - (32, 1) => Some(MeleeScene::HomeRunContest), - (15, 1) => Some(MeleeScene::TargetTest(self.get_stage())), - (33, 1) => Some(MeleeScene::MultiManMelee(MultiManVariant::TenMan)), - (34, 1) => Some(MeleeScene::MultiManMelee(MultiManVariant::HundredMan)), - (35, 1) => Some(MeleeScene::MultiManMelee(MultiManVariant::ThreeMinute)), - (36, 1) => Some(MeleeScene::MultiManMelee(MultiManVariant::FifteenMinute)), - (37, 1) => Some(MeleeScene::MultiManMelee(MultiManVariant::Endless)), - (38, 1) => Some(MeleeScene::MultiManMelee(MultiManVariant::Cruel)), - _ => None - } - } - fn get_stage(&mut self) -> Option { - self.mem.read::(0x8049E6C8 + 0x88 + 0x03).and_then(|v| MeleeStage::try_from(v).ok()) - } - fn get_character(&mut self, player_id: u8) -> Option { - const PLAYER_BLOCKS: [u32; 4] = [0x80453080, 0x80453F10, 0x80454DA0, 0x80455C30]; - self.mem.read::(PLAYER_BLOCKS[player_id as usize] + 0x07).and_then(|v| MeleeCharacter::try_from(v).ok()) - } - - pub fn run(&mut self, stop_signal: CancellationToken, discord_send: tokio::sync::mpsc::Sender, tray_send: std::sync::mpsc::Sender) { - const RUN_INTERVAL: u64 = 1000; - macro_rules! send_discord_msg { - ($req:expr) => { - if self.last_payload != $req { - let _ = discord_send.blocking_send($req); - self.last_payload = $req; - } - }; - } - - loop { - if stop_signal.is_cancelled() { - return; - } - if !self.mem.has_process() { - println!("{}", if self.mem.find_process() { "Found" } else { "Searching process..." }); - } else { - self.mem.check_process_running(); - } - - { - let has_process = self.mem.has_process(); - if has_process == (self.last_tray_event == MeleeTrayEvent::Disconnected) { - let tray_ev = if has_process { MeleeTrayEvent::Connected } else { MeleeTrayEvent::Disconnected }; - self.last_tray_event = tray_ev; - let _ = tray_send.send(tray_ev); - } - } - - CONFIG.with_ref(|c| { - // self.get_game_variant(); - let gamemode_opt: Option = self.get_melee_scene(); - - if gamemode_opt.is_some() { - let gamemode: MeleeScene = gamemode_opt.unwrap(); - - // Check if we are queueing a game - if c.slippi.enabled && c.slippi.show_queueing && match gamemode { - MeleeScene::SlippiCss(scene) => - scene.and_then(|s| Some(s.is_enabled(c))).unwrap_or(true), - _ => false - } { - match self.matchmaking_type() { - Some(MatchmakingMode::Initializing) | Some(MatchmakingMode::Matchmaking) => { - let port_op = self.get_player_port(); - if !port_op.is_none() { - let port = port_op.unwrap(); - let character = if c.global.show_in_game_character { self.get_character_selection(port) } else { Some(MeleeCharacter::Hidden) }; - match gamemode { - MeleeScene::SlippiCss(scene) => { - let request = DiscordClientRequest::queue( - scene, - character - ); - send_discord_msg!(request.clone()); - }, - _ => {/* shouldn't happen */} - } - } - } - Some(MatchmakingMode::Idle) => { - let port_op = self.get_player_port(); - if !port_op.is_none() { - let port = port_op.unwrap(); - let character = if c.global.show_in_game_character { self.get_character_selection(port) } else { Some(MeleeCharacter::Hidden) }; - match gamemode { - MeleeScene::SlippiCss(scene) => { - let request = DiscordClientRequest::idle( - scene, - character - ); - send_discord_msg!(request.clone()); - }, - _ => {/* shouldn't happen */} - } - } - } - Some(_) => { - send_discord_msg!(DiscordClientRequest::clear()); - }, // sometimes it's none, probably because the pointer indirection changes during the asynchronous memory requests - _ => {} - } - // Else, we want to see if the current game mode is enabled in the config (we're in-game) - } else if match gamemode { - - MeleeScene::MainMenu => true, - MeleeScene::SlippiCss(_) => false, // if we are in css, ignore - MeleeScene::SlippiOnline(scene) => c.slippi.enabled && - scene.and_then(|s| Some(s.is_enabled(c))).unwrap_or(true), - MeleeScene::UnclePunch => c.uncle_punch.enabled, - MeleeScene::TrainingMode => c.training_mode.enabled, - MeleeScene::VsMode => c.vs_mode.enabled, - MeleeScene::HomeRunContest => c.stadium.enabled && c.stadium.hrc.enabled, - MeleeScene::TargetTest(_) => c.stadium.enabled && c.stadium.btt.enabled, - MeleeScene::MultiManMelee(_) => c.stadium.enabled && c.stadium.mmm.enabled - } { - let game_time = self.game_time(); - let timestamp = if c.global.show_in_game_time { - DiscordClientRequestTimestamp { - mode: match self.timer_mode() { - TimerMode::Countdown => DiscordClientRequestTimestampMode::End, - TimerMode::Frozen => DiscordClientRequestTimestampMode::Static, - _ => DiscordClientRequestTimestampMode::Start - }, - timestamp: if self.timer_mode() == TimerMode::Countdown { current_unix_time() + game_time } else { current_unix_time() - game_time } - } - } else { - DiscordClientRequestTimestamp::none() - }; - let player_index = match gamemode { - MeleeScene::VsMode => self.get_player_port(), - MeleeScene::SlippiOnline(_) => self.get_slippi_player_port(), - _ => Some(0u8) // default to port 1, mostly the case in single player modes like training mode/unclepunch - }.unwrap_or(0u8); - - let request = if let MeleeScene::MainMenu = gamemode { - // For main menu, do not show character or stage - DiscordClientRequest::main_menu() - } else { - // For other game modes, construct the request normally - DiscordClientRequest::game( - match gamemode { MeleeScene::TargetTest(scene) => scene, _ => self.get_stage() }, - if c.global.show_in_game_character { self.get_character(player_index) } else { Some(MeleeCharacter::Hidden) }, - gamemode, - timestamp, - if match gamemode { MeleeScene::SlippiOnline(_) => true, _ => false } && c.slippi.show_opponent_name { self.get_opp_name() } else { None } - ) - }; - - send_discord_msg!(request.clone()); - } else { - send_discord_msg!(DiscordClientRequest::clear()); - } - } else if self.last_payload.req_type != DiscordClientRequestType::Clear { - send_discord_msg!(DiscordClientRequest::clear()); - } - }); - - sleep(RUN_INTERVAL); - } - } -} \ No newline at end of file diff --git a/discord-rpc/src/melee/msrb.rs b/discord-rpc/src/melee/msrb.rs deleted file mode 100644 index 2af7924..0000000 --- a/discord-rpc/src/melee/msrb.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::mem; - -use super::dolphin_mem::DolphinMemory; - -const MATCH_STRUCT_LEN: isize = 0x138; - -// reference: https://github.com/project-slippi/slippi-ssbm-asm/blob/0be644aff85986eae17e96f4c98b3342ab087d05/Online/Online.s#L311-L344 -#[derive(Clone, Copy)] -pub enum MSRBOffset { - MsrbConnectionState = 0, // u8, matchmaking state defined above - MsrbIsLocalPlayerReady = Self::MsrbConnectionState as isize + 1, // bool - MsrbIsRemotePlayerReady = Self::MsrbIsLocalPlayerReady as isize + 1, // bool - MsrbLocalPlayerIndex = Self::MsrbIsRemotePlayerReady as isize + 1, // u8 - MsrbRemotePlayerIndex = Self::MsrbLocalPlayerIndex as isize + 1, // u8s - MsrbRngOffset = Self::MsrbRemotePlayerIndex as isize + 1, // u32 - MsrbDelayFrames = Self::MsrbRngOffset as isize + 4, // u8 - MsrbUserChatmsgId = Self::MsrbDelayFrames as isize + 1, // u8 - MsrbOppChatmsgId = Self::MsrbUserChatmsgId as isize + 1, // u8 - MsrbChatmsgPlayerIndex = Self::MsrbOppChatmsgId as isize + 1, // u8 - MsrbVsLeftPlayers = Self::MsrbChatmsgPlayerIndex as isize + 1, // u32 player ports 0xP1P2P3PN - MsrbVsRightPlayers = Self::MsrbVsLeftPlayers as isize + 4, // u32 player ports 0xP1P2P3PN - MsrbLocalName = Self::MsrbVsRightPlayers as isize + 4, // char[31] - MsrbP1Name = Self::MsrbLocalName as isize + 31, // char[31] - MsrbP2Name = Self::MsrbP1Name as isize + 31, // char[31] - MsrbP3Name = Self::MsrbP2Name as isize + 31, // char[31] - MsrbP4Name = Self::MsrbP3Name as isize + 31, // char[31] - MsrbOppName = Self::MsrbP4Name as isize + 31, // char[31] - MsrbP1ConnectCode = Self::MsrbOppName as isize + 31, // char[10] hashtag is shift-jis - MsrbP2ConnectCode = Self::MsrbP1ConnectCode as isize + 10, // char[10] hashtag is shift-jis - MsrbP3ConnectCode = Self::MsrbP2ConnectCode as isize + 10, // char[10] hashtag is shift-jis - MsrbP4ConnectCode = Self::MsrbP3ConnectCode as isize + 10, // char[10] hashtag is shift-jis - MsrbP1SlippiUid = Self::MsrbP4ConnectCode as isize + 10, // char[29] - MsrbP2SlippiUid = Self::MsrbP1SlippiUid as isize + 29, // char[29] - MsrbP3SlippiUid = Self::MsrbP2SlippiUid as isize + 29, // char[29] - MsrbP4SlippiUid = Self::MsrbP3SlippiUid as isize + 29, // char[29] - MsrbErrorMsg = Self::MsrbP4SlippiUid as isize + 29, // char[241] - ErrorMessageLen = 241, - MsrbGameInfoBlock = Self::MsrbErrorMsg as isize + Self::ErrorMessageLen as isize, // MATCH_STRUCT_LEN - MsrbMatchId = Self::MsrbGameInfoBlock as isize + MATCH_STRUCT_LEN, // char[51] - MsrbSize = Self::MsrbMatchId as isize + 51, -} - -impl DolphinMemory { - fn msrb_ptr(&mut self) -> Option { - const CSSDT_BUF_ADDR: u32 = 0x80005614; // reference: https://github.com/project-slippi/slippi-ssbm-asm/blob/0be644aff85986eae17e96f4c98b3342ab087d05/Online/Online.s#L31 - self.pointer_indirection(CSSDT_BUF_ADDR, 2) - } - pub fn read_msrb(&mut self, offset: MSRBOffset) -> Option where [u8; mem::size_of::()]: { - self.msrb_ptr().and_then(|ptr| self.read::(ptr + offset as u32)) - } - - pub fn read_msrb_string(&mut self, offset: MSRBOffset) -> Option where [u8; mem::size_of::<[u8; LEN]>()]: { - self.msrb_ptr().and_then(|ptr| self.read_string::(ptr + offset as u32)) - } - - pub fn read_msrb_string_shift_jis(&mut self, offset: MSRBOffset) -> Option where [u8; mem::size_of::<[u8; LEN]>()]: { - self.msrb_ptr().and_then(|ptr| self.read_string_shift_jis::(ptr + offset as u32)) - } -} diff --git a/discord-rpc/src/melee/multiman.rs b/discord-rpc/src/melee/multiman.rs deleted file mode 100644 index 0aa6999..0000000 --- a/discord-rpc/src/melee/multiman.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[derive(PartialEq, Clone, Copy, Debug)] -pub enum MultiManVariant { - TenMan, - HundredMan, - ThreeMinute, - FifteenMinute, - Endless, - Cruel -} \ No newline at end of file diff --git a/discord-rpc/src/melee/stage.rs b/discord-rpc/src/melee/stage.rs deleted file mode 100644 index fdb4c4e..0000000 --- a/discord-rpc/src/melee/stage.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::fmt::Display; - -use num_enum::TryFromPrimitive; - -use super::character::MeleeCharacter; - -#[derive(Debug, PartialEq, Copy, Clone, TryFromPrimitive)] -#[repr(u8)] -pub enum MeleeStage { - // Dummy, (unused) - // Test, (unused) - Castle = 2, - Rcruise, - Kongo, - Garden, - Greatbay, - Shrine, - Zebes, - Kraid, - Story, - Yoster, - Izumi, - Greens, - Corneria, - Venom, - PStad, - Pura, - MuteCity, - BigBlue, - Onett, - Fourside, - IceMt, - // IceTop, (unused) - Mk1 = 24, - Mk2, - Akaneia, - FlatZone, - OldPu, - OldStory, - OldKongo, - // AdvKraid, (unused) - // AdvShrine, (unused) - // AdvZr, (unused) - // AdvBr, (unused) - // AdvTe, (unused) - Battle = 36, - FD, - - HomeRunStadium = 67, - - MarioTargetTest = 40, - CaptainFalconTargetTest, - YoungLinkTargetTest, - DonkeyKongTargetTest, - DrMarioTargetTest, - FalcoTargetTest, - FoxTargetTest, - IceClimbersTargetTest, - KirbyTargetTest, - BowserTargetTest, - LinkTargetTest, - LuigiTargetTest, - MarthTargetTest, - MewtwoTargetTest, - NessTargetTest, - PeachTargetTest, - PichuTargetTest, - PikachuTargetTest, - JigglypuffTargetTest, - SamusTargetTest, - SheikTargetTest, - YoshiTargetTest, - ZeldaTargetTest, - MrGameAndWatchTargetTest, - RoyTargetTest, - GanondorfTargetTest, -} - -impl MeleeStage { - fn is_target_test(&self) -> bool { - *self as u8 >= MeleeStage::MarioTargetTest as u8 && *self as u8 <= MeleeStage::GanondorfTargetTest as u8 - } -} - -impl Display for MeleeStage { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match *self { - // Self::Dummy => write!(f, "Dummy"), - // Self::Test => write!(f, "Test"), - Self::Castle => write!(f, "Princess Peach's Castle"), - Self::Rcruise => write!(f, "Rainbow Cruise"), - Self::Kongo => write!(f, "Kongo Jungle"), - Self::Garden => write!(f, "Jungle Japes"), - Self::Greatbay => write!(f, "Great Bay"), - Self::Shrine => write!(f, "Temple"), - Self::Zebes => write!(f, "Brinstar"), - Self::Kraid => write!(f, "Brinstar Depths"), - Self::Story => write!(f, "Yoshi's Story"), - Self::Yoster => write!(f, "Yoshi's Island"), - Self::Izumi => write!(f, "Fountain of Dreams"), - Self::Greens => write!(f, "Green Greens"), - Self::Corneria => write!(f, "Corneria"), - Self::Venom => write!(f, "Venom"), - Self::PStad => write!(f, "Pokemon Stadium"), - Self::Pura => write!(f, "Poke Floats"), - Self::MuteCity => write!(f, "Mute City"), - Self::BigBlue => write!(f, "Big Blue"), - Self::Onett => write!(f, "Onett"), - Self::Fourside => write!(f, "Fourside"), - Self::IceMt => write!(f, "IcicleMountain"), - // Self::IceTop => write!(f, "Icetop"), - Self::Mk1 => write!(f, "Mushroom Kingdom"), - Self::Mk2 => write!(f, "Mushroom Kingdom II"), - Self::Akaneia => write!(f, "Akaneia"), - Self::FlatZone => write!(f, "Flat Zone"), - Self::OldPu => write!(f, "Dream Land"), - Self::OldStory => write!(f, "Yoshi's Island (N64)"), - Self::OldKongo => write!(f, "Kongo Jungle (N64)"), - Self::Battle => write!(f, "Battlefield"), - Self::FD => write!(f, "Final Destination"), - Self::HomeRunStadium => write!(f, "Home-Run Stadium"), - - Self::DrMarioTargetTest => write!(f, "Target Test ({})", MeleeCharacter::DrMario), - Self::MarioTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Mario), - Self::LuigiTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Luigi), - Self::BowserTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Bowser), - Self::PeachTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Peach), - Self::YoshiTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Yoshi), - Self::DonkeyKongTargetTest => write!(f, "Target Test ({})", MeleeCharacter::DonkeyKong), - Self::CaptainFalconTargetTest => write!(f, "Target Test ({})", MeleeCharacter::CaptainFalcon), - Self::GanondorfTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Ganondorf), - Self::FalcoTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Falco), - Self::FoxTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Fox), - Self::NessTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Ness), - Self::IceClimbersTargetTest => write!(f, "Target Test ({})", MeleeCharacter::IceClimbers), - Self::KirbyTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Kirby), - Self::SamusTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Samus), - Self::SheikTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Sheik), - Self::ZeldaTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Zelda), - Self::LinkTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Link), - Self::YoungLinkTargetTest => write!(f, "Target Test ({})", MeleeCharacter::YoungLink), - Self::PichuTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Pichu), - Self::PikachuTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Pikachu), - Self::JigglypuffTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Jigglypuff), - Self::MewtwoTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Mewtwo), - Self::MrGameAndWatchTargetTest => write!(f, "Target Test ({})", MeleeCharacter::MrGameAndWatch), - Self::MarthTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Marth), - Self::RoyTargetTest => write!(f, "Target Test ({})", MeleeCharacter::Roy), - } - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct OptionalMeleeStage(pub Option); -impl OptionalMeleeStage { - pub fn as_discord_resource(&self) -> String { - self.0.as_ref().and_then(|c| { - if c.is_target_test() { - Some("stagebtt".into()) - } else { - Some(format!("stage{}", (*c) as u8)) - } - }).unwrap_or("questionmark".into()) - } -} -impl Display for OptionalMeleeStage { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &(*self).0 { - Some(v) => write!(f, "{}", v), - _ => write!(f, "Unknown stage") - } - } -} \ No newline at end of file diff --git a/discord-rpc/src/rank.rs b/discord-rpc/src/rank.rs deleted file mode 100644 index 9e57cf3..0000000 --- a/discord-rpc/src/rank.rs +++ /dev/null @@ -1,14 +0,0 @@ -use serde::Deserialize; - -#[derive(Deserialize)] -pub struct RankInfo { - #[serde(alias = "rank")] - pub name: String, - #[serde(alias = "rating")] - pub elo: f32 -} - -pub async fn get_rank_info(code: &str) -> Result> { - let res = reqwest::get(format!("http://slprank.com/rank/{}?raw", code)).await?; - Ok(res.json::().await?) -} \ No newline at end of file diff --git a/discord-rpc/src/scenes.rs b/discord-rpc/src/scenes.rs new file mode 100644 index 0000000..18cfc9f --- /dev/null +++ b/discord-rpc/src/scenes.rs @@ -0,0 +1,396 @@ +/// Sourced from M'Overlay: https://github.com/bkacjios/m-overlay/blob/d8c629d/source/melee.lua +#[rustfmt::skip] +#[allow(dead_code)] +pub(crate) mod scene_ids { + pub(crate) const MATCH_NO_RESULT: u8 = 0x00; + pub(crate) const MATCH_GAME: u8 = 0x02; + pub(crate) const MATCH_STAGE_CLEAR: u8 = 0x03; + pub(crate) const MATCH_STAGE_FAILURE: u8 = 0x04; + pub(crate) const MATCH_STAGE_CLEAR3: u8 = 0x05; + pub(crate) const MATCH_NEW_RECORD: u8 = 0x06; + pub(crate) const MATCH_NO_CONTEST: u8 = 0x07; + pub(crate) const MATCH_RETRY: u8 = 0x08; + pub(crate) const MATCH_GAME_CLEAR: u8 = 0x09; + + // MAJOR FLAGS + pub(crate) const SCENE_TITLE_SCREEN: u8 = 0x00; + + pub(crate) const SCENE_MAIN_MENU: u8 = 0x01; + // MENU FLAGS + pub(crate) const MENU_MAIN: u8 = 0x00; + pub(crate) const SELECT_MAIN_1P: u8 = 0x00; + pub(crate) const SELECT_MAIN_VS: u8 = 0x01; + pub(crate) const SELECT_MAIN_TROPHY: u8 = 0x02; + pub(crate) const SELECT_MAIN_OPTIONS: u8 = 0x03; + pub(crate) const SELECT_MAIN_DATA: u8 = 0x04; + + pub(crate) const MENU_1P: u8 = 0x01; + pub(crate) const SELECT_1P_REGULAR: u8 = 0x00; + pub(crate) const SELECT_1P_EVENT: u8 = 0x01; + pub(crate) const SELECT_1P_ONLINE: u8 = 0x2; + pub(crate) const SELECT_1P_STADIUM: u8 = 0x03; + pub(crate) const SELECT_1P_TRAINING: u8 = 0x04; + + pub(crate) const MENU_VS: u8 = 0x02; + pub(crate) const SELECT_VS_MELEE: u8 = 0x00; + pub(crate) const SELECT_VS_TOURNAMENT: u8 = 0x01; + pub(crate) const SELECT_VS_SPECIAL: u8 = 0x02; + pub(crate) const SELECT_VS_CUSTOM: u8 = 0x03; + pub(crate) const SELECT_VS_NAMEENTRY: u8 = 0x04; + + pub(crate) const MENU_TROPHIES: u8 = 0x03; + pub(crate) const SELECT_TROPHIES_GALLERY: u8 = 0x00; + pub(crate) const SELECT_TROPHIES_LOTTERY: u8 = 0x01; + pub(crate) const SELECT_TROPHIES_COLLECTION: u8 = 0x02; + + pub(crate) const MENU_OPTIONS: u8 = 0x04; + pub(crate) const SELECT_OPTIONS_RUMBLE: u8 = 0x00; + pub(crate) const SELECT_OPTIONS_SOUND: u8 = 0x01; + pub(crate) const SELECT_OPTIONS_DISPLAY: u8 = 0x02; + pub(crate) const SELECT_OPTIONS_UNKNOWN: u8 = 0x03; + pub(crate) const SELECT_OPTIONS_LANGUAGE: u8 = 0x04; + pub(crate) const SELECT_OPTIONS_ERASE_DATA: u8 = 0x05; + + pub(crate) const MENU_ONLINE: u8 = 0x08; + pub(crate) const SELECT_ONLINE_RANKED: u8 = 0x00; + pub(crate) const SELECT_ONLINE_UNRANKED: u8 = 0x01; + pub(crate) const SELECT_ONLINE_DIRECT: u8 = 0x02; + pub(crate) const SELECT_ONLINE_TEAMS: u8 = 0x03; + pub(crate) const SELECT_ONLINE_LOGOUT: u8 = 0x05; + + pub(crate) const MENU_STADIUM: u8 = 0x09; + pub(crate) const SELECT_STADIUM_TARGET_TEST: u8 = 0x00; + pub(crate) const SELECT_STADIUM_HOMERUN_CONTEST: u8 = 0x01; + pub(crate) const SELECT_STADIUM_MULTIMAN_MELEE: u8 = 0x02; + + pub(crate) const MENU_RUMBLE: u8 = 0x13; + pub(crate) const MENU_SOUND: u8 = 0x14; + pub(crate) const MENU_DISPLAY: u8 = 0x15; + pub(crate) const MENU_UNKNOWN1: u8 = 0x16; + pub(crate) const MENU_LANGUAGE: u8 = 0x17; + + pub(crate) const SCENE_VS_MODE: u8 = 0x02; + // MINOR FLAGS + pub(crate) const SCENE_VS_CSS: u8 = 0x0; + pub(crate) const SCENE_VS_SSS: u8 = 0x1; + pub(crate) const SCENE_VS_INGAME: u8 = 0x2; + pub(crate) const SCENE_VS_POSTGAME: u8 = 0x4; + + pub(crate) const SCENE_CLASSIC_MODE: u8 = 0x03; + pub(crate) const SCENE_CLASSIC_LEVEL_1_VS: u8 = 0x00; + pub(crate) const SCENE_CLASSIC_LEVEL_1: u8 = 0x01; + pub(crate) const SCENE_CLASSIC_LEVEL_2_VS: u8 = 0x02; + pub(crate) const SCENE_CLASSIC_LEVEL_2: u8 = 0x03; + pub(crate) const SCENE_CLASSIC_LEVEL_3_VS: u8 = 0x04; + pub(crate) const SCENE_CLASSIC_LEVEL_3: u8 = 0x05; + pub(crate) const SCENE_CLASSIC_LEVEL_4_VS: u8 = 0x06; + pub(crate) const SCENE_CLASSIC_LEVEL_4: u8 = 0x07; + // pub(crate) const SCENE_CLASSIC_LEVEL_5_VS: u8 = 0x08; + // pub(crate) const SCENE_CLASSIC_LEVEL_5: u8 = 0x09; + pub(crate) const SCENE_CLASSIC_LEVEL_5_VS: u8 = 0x10; + pub(crate) const SCENE_CLASSIC_LEVEL_5: u8 = 0x09; + + pub(crate) const SCENE_CLASSIC_LEVEL_16: u8 = 0x20; + pub(crate) const SCENE_CLASSIC_LEVEL_16_VS: u8 = 0x21; + + pub(crate) const SCENE_CLASSIC_LEVEL_24: u8 = 0x30; + pub(crate) const SCENE_CLASSIC_LEVEL_24_VS: u8 = 0x31; + + pub(crate) const SCENE_CLASSIC_BREAK_THE_TARGETS_INTRO: u8 = 0x16; + pub(crate) const SCENE_CLASSIC_BREAK_THE_TARGETS: u8 = 0x17; + + pub(crate) const SCENE_CLASSIC_TROPHY_STAGE_INTRO: u8 = 0x28; + pub(crate) const SCENE_CLASSIC_TROPHY_STAGE_TARGETS: u8 = 0x29; + + pub(crate) const SCENE_CLASSIC_RACE_TO_FINISH_INTRO: u8 = 0x40; + pub(crate) const SCENE_CLASSIC_RACE_TO_FINISH_TARGETS: u8 = 0x41; + + pub(crate) const SCENE_CLASSIC_LEVEL_56: u8 = 0x38; + pub(crate) const SCENE_CLASSIC_LEVEL_56_VS: u8 = 0x39; + + pub(crate) const SCENE_CLASSIC_MASTER_HAND: u8 = 0x51; + + pub(crate) const SCENE_CLASSIC_CONTINUE: u8 = 0x69; + pub(crate) const SCENE_CLASSIC_CSS: u8 = 0x70; + + pub(crate) const SCENE_ADVENTURE_MODE: u8 = 0x04; + + pub(crate) const SCENE_ADVENTURE_MUSHROOM_KINGDOM_INTRO: u8 = 0x00; + pub(crate) const SCENE_ADVENTURE_MUSHROOM_KINGDOM: u8 = 0x01; + pub(crate) const SCENE_ADVENTURE_MUSHROOM_KINGDOM_LUIGI: u8 = 0x02; + pub(crate) const SCENE_ADVENTURE_MUSHROOM_KINGDOM_BATTLE: u8 = 0x03; + + pub(crate) const SCENE_ADVENTURE_MUSHROOM_KONGO_JUNGLE_INTRO: u8 = 0x08; + pub(crate) const SCENE_ADVENTURE_MUSHROOM_KONGO_JUNGLE_TINY_BATTLE: u8 = 0x09; + pub(crate) const SCENE_ADVENTURE_MUSHROOM_KONGO_JUNGLE_GIANT_BATTLE: u8 = 0x0A; + + pub(crate) const SCENE_ADVENTURE_UNDERGROUND_MAZE_INTRO: u8 = 0x10; + pub(crate) const SCENE_ADVENTURE_UNDERGROUND_MAZE: u8 = 0x11; + pub(crate) const SCENE_ADVENTURE_HYRULE_TEMPLE_BATTLE: u8 = 0x12; + + pub(crate) const SCENE_ADVENTURE_BRINSTAR_INTRO: u8 = 0x18; + pub(crate) const SCENE_ADVENTURE_BRINSTAR: u8 = 0x19; + + pub(crate) const SCENE_ADVENTURE_ESCAPE_ZEBES_INTRO: u8 = 0x1A; + pub(crate) const SCENE_ADVENTURE_ESCAPE_ZEBES: u8 = 0x1B; + pub(crate) const SCENE_ADVENTURE_ESCAPE_ZEBES_ESCAPE: u8 = 0x1C; + + pub(crate) const SCENE_ADVENTURE_GREEN_GREENS_INTRO: u8 = 0x20; + pub(crate) const SCENE_ADVENTURE_GREEN_GREENS_KIRBY_BATTLE: u8 = 0x21; + pub(crate) const SCENE_ADVENTURE_GREEN_GREENS_KIRBY_TEAM_INTRO: u8 = 0x22; + pub(crate) const SCENE_ADVENTURE_GREEN_GREENS_KIRBY_TEAM_BATTLE: u8 = 0x23; + pub(crate) const SCENE_ADVENTURE_GREEN_GREENS_GIANT_KIRBY_INTRO: u8 = 0x24; + pub(crate) const SCENE_ADVENTURE_GREEN_GREENS_GIANT_KIRBY_BATTLE: u8 = 0x25; + + pub(crate) const SCENE_ADVENTURE_CORNERIA_INTRO: u8 = 0x28; + pub(crate) const SCENE_ADVENTURE_CORNERIA_BATTLE_1: u8 = 0x29; + pub(crate) const SCENE_ADVENTURE_CORNERIA_RAID: u8 = 0x2A; + pub(crate) const SCENE_ADVENTURE_CORNERIA_BATTLE_2: u8 = 0x2B; + pub(crate) const SCENE_ADVENTURE_CORNERIA_BATTLE_3: u8 = 0x2C; + + pub(crate) const SCENE_ADVENTURE_POKEMON_STADIUM_INTRO: u8 = 0x30; + pub(crate) const SCENE_ADVENTURE_POKEMON_STADIUM_BATTLE: u8 = 0x31; + + pub(crate) const SCENE_ADVENTURE_FZERO_GRAND_PRIX_CARS: u8 = 0x38; + pub(crate) const SCENE_ADVENTURE_FZERO_GRAND_PRIX_INTRO: u8 = 0x39; + pub(crate) const SCENE_ADVENTURE_FZERO_GRAND_PRIX_RACE: u8 = 0x3A; + pub(crate) const SCENE_ADVENTURE_FZERO_GRAND_PRIX_BATTLE: u8 = 0x3B; + + pub(crate) const SCENE_ADVENTURE_ONETT_INTRO: u8 = 0x40; + pub(crate) const SCENE_ADVENTURE_ONETT_BATTLE: u8 = 0x41; + + pub(crate) const SCENE_ADVENTURE_ICICLE_MOUNTAIN_INTRO: u8 = 0x48; + pub(crate) const SCENE_ADVENTURE_ICICLE_MOUNTAIN_CLIMB: u8 = 0x49; + + pub(crate) const SCENE_ADVENTURE_BATTLEFIELD_INTRO: u8 = 0x50; + pub(crate) const SCENE_ADVENTURE_BATTLEFIELD_BATTLE: u8 = 0x51; + pub(crate) const SCENE_ADVENTURE_BATTLEFIELD_METAL_INTRO: u8 = 0x52; + pub(crate) const SCENE_ADVENTURE_BATTLEFIELD_METAL_BATTLE: u8 = 0x53; + + pub(crate) const SCENE_ADVENTURE_FINAL_DESTINATION_INTRO: u8 = 0x58; + pub(crate) const SCENE_ADVENTURE_FINAL_DESTINATION_BATTLE: u8 = 0x59; + pub(crate) const SCENE_ADVENTURE_FINAL_DESTINATION_POSE: u8 = 0x5A; + pub(crate) const SCENE_ADVENTURE_FINAL_DESTINATION_WINNER: u8 = 0x5B; + + pub(crate) const SCENE_ADVENTURE_CSS: u8 = 0x70; + + pub(crate) const SCENE_ALL_STAR_MODE: u8 = 0x05; + pub(crate) const SCENE_ALL_STAR_LEVEL_1: u8 = 0x00; + pub(crate) const SCENE_ALL_STAR_REST_AREA_1: u8 = 0x01; + pub(crate) const SCENE_ALL_STAR_LEVEL_2: u8 = 0x02; + pub(crate) const SCENE_ALL_STAR_REST_AREA_2: u8 = 0x03; + pub(crate) const SCENE_ALL_STAR_LEVEL_3: u8 = 0x04; + pub(crate) const SCENE_ALL_STAR_REST_AREA_3: u8 = 0x05; + pub(crate) const SCENE_ALL_STAR_LEVEL_4: u8 = 0x06; + pub(crate) const SCENE_ALL_STAR_REST_AREA_4: u8 = 0x07; + pub(crate) const SCENE_ALL_STAR_LEVEL_5: u8 = 0x08; + pub(crate) const SCENE_ALL_STAR_REST_AREA_5: u8 = 0x09; + pub(crate) const SCENE_ALL_STAR_LEVEL_6: u8 = 0x10; + pub(crate) const SCENE_ALL_STAR_REST_AREA_6: u8 = 0x11; + pub(crate) const SCENE_ALL_STAR_LEVEL_7: u8 = 0x12; + pub(crate) const SCENE_ALL_STAR_REST_AREA_7: u8 = 0x13; + pub(crate) const SCENE_ALL_STAR_LEVEL_8: u8 = 0x14; + pub(crate) const SCENE_ALL_STAR_REST_AREA_8: u8 = 0x15; + pub(crate) const SCENE_ALL_STAR_LEVEL_9: u8 = 0x16; + pub(crate) const SCENE_ALL_STAR_REST_AREA_9: u8 = 0x17; + pub(crate) const SCENE_ALL_STAR_LEVEL_10: u8 = 0x18; + pub(crate) const SCENE_ALL_STAR_REST_AREA_10: u8 = 0x19; + pub(crate) const SCENE_ALL_STAR_LEVEL_11: u8 = 0x20; + pub(crate) const SCENE_ALL_STAR_REST_AREA_11: u8 = 0x21; + pub(crate) const SCENE_ALL_STAR_LEVEL_12: u8 = 0x22; + pub(crate) const SCENE_ALL_STAR_REST_AREA_12: u8 = 0x23; + pub(crate) const SCENE_ALL_STAR_LEVEL_13: u8 = 0x24; + pub(crate) const SCENE_ALL_STAR_REST_AREA_13: u8 = 0x25; + pub(crate) const SCENE_ALL_STAR_LEVEL_14: u8 = 0x26; + pub(crate) const SCENE_ALL_STAR_REST_AREA_14: u8 = 0x27; + pub(crate) const SCENE_ALL_STAR_LEVEL_15: u8 = 0x28; + pub(crate) const SCENE_ALL_STAR_REST_AREA_15: u8 = 0x29; + pub(crate) const SCENE_ALL_STAR_LEVEL_16: u8 = 0x30; + pub(crate) const SCENE_ALL_STAR_REST_AREA_16: u8 = 0x31; + pub(crate) const SCENE_ALL_STAR_LEVEL_17: u8 = 0x32; + pub(crate) const SCENE_ALL_STAR_REST_AREA_17: u8 = 0x33; + pub(crate) const SCENE_ALL_STAR_LEVEL_18: u8 = 0x34; + pub(crate) const SCENE_ALL_STAR_REST_AREA_18: u8 = 0x35; + pub(crate) const SCENE_ALL_STAR_LEVEL_19: u8 = 0x36; + pub(crate) const SCENE_ALL_STAR_REST_AREA_19: u8 = 0x37; + pub(crate) const SCENE_ALL_STAR_LEVEL_20: u8 = 0x38; + pub(crate) const SCENE_ALL_STAR_REST_AREA_20: u8 = 0x39; + pub(crate) const SCENE_ALL_STAR_LEVEL_21: u8 = 0x40; + pub(crate) const SCENE_ALL_STAR_REST_AREA_21: u8 = 0x41; + pub(crate) const SCENE_ALL_STAR_LEVEL_22: u8 = 0x42; + pub(crate) const SCENE_ALL_STAR_REST_AREA_22: u8 = 0x43; + pub(crate) const SCENE_ALL_STAR_LEVEL_23: u8 = 0x44; + pub(crate) const SCENE_ALL_STAR_REST_AREA_23: u8 = 0x45; + pub(crate) const SCENE_ALL_STAR_LEVEL_24: u8 = 0x46; + pub(crate) const SCENE_ALL_STAR_REST_AREA_24: u8 = 0x47; + pub(crate) const SCENE_ALL_STAR_LEVEL_25: u8 = 0x48; + pub(crate) const SCENE_ALL_STAR_REST_AREA_25: u8 = 0x49; + pub(crate) const SCENE_ALL_STAR_LEVEL_26: u8 = 0x50; + pub(crate) const SCENE_ALL_STAR_REST_AREA_26: u8 = 0x51; + pub(crate) const SCENE_ALL_STAR_LEVEL_27: u8 = 0x52; + pub(crate) const SCENE_ALL_STAR_REST_AREA_28: u8 = 0x53; + pub(crate) const SCENE_ALL_STAR_LEVEL_29: u8 = 0x54; + pub(crate) const SCENE_ALL_STAR_REST_AREA_29: u8 = 0x55; + pub(crate) const SCENE_ALL_STAR_LEVEL_30: u8 = 0x56; + pub(crate) const SCENE_ALL_STAR_REST_AREA_30: u8 = 0x57; + pub(crate) const SCENE_ALL_STAR_LEVEL_31: u8 = 0x58; + pub(crate) const SCENE_ALL_STAR_REST_AREA_31: u8 = 0x59; + pub(crate) const SCENE_ALL_STAR_LEVEL_32: u8 = 0x60; + pub(crate) const SCENE_ALL_STAR_REST_AREA_32: u8 = 0x61; + pub(crate) const SCENE_ALL_STAR_CSS: u8 = 0x70; + + pub(crate) const SCENE_DEBUG: u8 = 0x06; + pub(crate) const SCENE_SOUND_TEST: u8 = 0x07; + + pub(crate) const SCENE_VS_ONLINE: u8 = 0x08; // SLIPPI ONLINE + pub(crate) const SCENE_VS_ONLINE_CSS: u8 = 0x00; + pub(crate) const SCENE_VS_ONLINE_SSS: u8 = 0x01; + pub(crate) const SCENE_VS_ONLINE_INGAME: u8 = 0x02; + pub(crate) const SCENE_VS_ONLINE_VERSUS: u8 = 0x04; + pub(crate) const SCENE_VS_ONLINE_RANKED: u8 = 0x05; + + pub(crate) const SCENE_UNKOWN_1: u8 = 0x09; + pub(crate) const SCENE_CAMERA_MODE: u8 = 0x0A; + pub(crate) const SCENE_TROPHY_GALLERY: u8 = 0x0B; + pub(crate) const SCENE_TROPHY_LOTTERY: u8 = 0x0C; + pub(crate) const SCENE_TROPHY_COLLECTION: u8 = 0x0D; + + pub(crate) const SCENE_START_MATCH: u8 = 0x0E; // Slippi Replays + pub(crate) const SCENE_START_MATCH_INGAME: u8 = 0x01; // Set when the replay is actually playing out + pub(crate) const SCENE_START_MATCH_UNKNOWN: u8 = 0x03; // Seems to be set right before the match loads + + pub(crate) const SCENE_TARGET_TEST: u8 = 0x0F; + pub(crate) const SCENE_TARGET_TEST_CSS: u8 = 0x00; + pub(crate) const SCENE_TARGET_TEST_INGAME: u8 = 0x1; + + pub(crate) const SCENE_SUPER_SUDDEN_DEATH: u8 = 0x10; + pub(crate) const SCENE_SSD_CSS: u8 = 0x00; + pub(crate) const SCENE_SSD_SSS: u8 = 0x01; + pub(crate) const SCENE_SSD_INGAME: u8 = 0x02; + pub(crate) const SCENE_SSD_POSTGAME: u8 = 0x04; + + pub(crate) const MENU_INVISIBLE_MELEE: u8 = 0x11; + pub(crate) const MENU_INVISIBLE_MELEE_CSS: u8 = 0x00; + pub(crate) const MENU_INVISIBLE_MELEE_SSS: u8 = 0x01; + pub(crate) const MENU_INVISIBLE_MELEE_INGAME: u8 = 0x02; + pub(crate) const MENU_INVISIBLE_MELEE_POSTGAME: u8 = 0x04; + + pub(crate) const MENU_SLOW_MO_MELEE: u8 = 0x12; + pub(crate) const MENU_SLOW_MO_MELEE_CSS: u8 = 0x00; + pub(crate) const MENU_SLOW_MO_MELEE_SSS: u8 = 0x01; + pub(crate) const MENU_SLOW_MO_MELEE_INGAME: u8 = 0x02; + pub(crate) const MENU_SLOW_MO_MELEE_POSTGAME: u8 = 0x04; + + pub(crate) const MENU_LIGHTNING_MELEE: u8 = 0x13; + pub(crate) const MENU_LIGHTNING_MELEE_CSS: u8 = 0x00; + pub(crate) const MENU_LIGHTNING_MELEE_SSS: u8 = 0x01; + pub(crate) const MENU_LIGHTNING_MELEE_INGAME: u8 = 0x02; + pub(crate) const MENU_LIGHTNING_MELEE_POSTGAME: u8 = 0x04; + + pub(crate) const SCENE_CHARACTER_APPROACHING: u8 = 0x14; + + pub(crate) const SCENE_CLASSIC_MODE_COMPLETE: u8 = 0x15; + pub(crate) const SCENE_CLASSIC_MODE_TROPHY: u8 = 0x00; + pub(crate) const SCENE_CLASSIC_MODE_CREDITS: u8 = 0x01; + pub(crate) const SCENE_CLASSIC_MODE_CHARACTER_VIDEO: u8 = 0x02; + pub(crate) const SCENE_CLASSIC_MODE_CONGRATS: u8 = 0x03; + + pub(crate) const SCENE_ADVENTURE_MODE_COMPLETE: u8 = 0x16; + pub(crate) const SCENE_ADVENTURE_MODE_TROPHY: u8 = 0x00; + pub(crate) const SCENE_ADVENTURE_MODE_CREDITS: u8 = 0x01; + pub(crate) const SCENE_ADVENTURE_MODE_CHARACTER_VIDEO: u8 = 0x02; + pub(crate) const SCENE_ADVENTURE_MODE_CONGRATS: u8 = 0x03; + + pub(crate) const SCENE_ALL_STAR_COMPLETE: u8 = 0x17; + pub(crate) const SCENE_ALL_STAR_TROPHY: u8 = 0x00; + pub(crate) const SCENE_ALL_STAR_CREDITS: u8 = 0x01; + pub(crate) const SCENE_ALL_STAR_CHARACTER_VIDEO: u8 = 0x02; + pub(crate) const SCENE_ALL_STAR_CONGRATS: u8 = 0x03; + + pub(crate) const SCENE_TITLE_SCREEN_IDLE: u8 = 0x18; + pub(crate) const SCENE_TITLE_SCREEN_IDLE_INTRO_VIDEO: u8 = 0x0; + pub(crate) const SCENE_TITLE_SCREEN_IDLE_FIGHT_1: u8 = 0x1; + pub(crate) const SCENE_TITLE_SCREEN_IDLE_BETWEEN_FIGHTS: u8 = 0x2; + pub(crate) const SCENE_TITLE_SCREEN_IDLE_FIGHT_2: u8 = 0x3; + pub(crate) const SCENE_TITLE_SCREEN_IDLE_HOW_TO_PLAY: u8 = 0x4; + + pub(crate) const SCENE_ADVENTURE_MODE_CINEMEATIC: u8 = 0x19; + pub(crate) const SCENE_CHARACTER_UNLOCKED: u8 = 0x1A; + + pub(crate) const SCENE_TOURNAMENT: u8 = 0x1B; + pub(crate) const SCENE_TOURNAMENT_CSS: u8 = 0x0; + pub(crate) const SCENE_TOURNAMENT_BRACKET: u8 = 0x1; + pub(crate) const SCENE_TOURNAMENT_INGAME: u8 = 0x4; + pub(crate) const SCENE_TOURNAMENT_POSTGAME: u8 = 0x6; + + pub(crate) const SCENE_TRAINING_MODE: u8 = 0x1C; + pub(crate) const SCENE_TRAINING_CSS: u8 = 0x0; + pub(crate) const SCENE_TRAINING_SSS: u8 = 0x1; + pub(crate) const SCENE_TRAINING_INGAME: u8 = 0x2; + + pub(crate) const SCENE_TINY_MELEE: u8 = 0x1D; + pub(crate) const SCENE_TINY_MELEE_CSS: u8 = 0x0; + pub(crate) const SCENE_TINY_MELEE_SSS: u8 = 0x1; + pub(crate) const SCENE_TINY_MELEE_INGAME: u8 = 0x2; + pub(crate) const SCENE_TINY_MELEE_POSTGAME: u8 = 0x4; + + pub(crate) const SCENE_GIANT_MELEE: u8 = 0x1E; + pub(crate) const SCENE_GIANT_MELEE_CSS: u8 = 0x0; + pub(crate) const SCENE_GIANT_MELEE_SSS: u8 = 0x1; + pub(crate) const SCENE_GIANT_MELEE_INGAME: u8 = 0x2; + pub(crate) const SCENE_GIANT_MELEE_POSTGAME: u8 = 0x4; + + pub(crate) const SCENE_STAMINA_MODE: u8 = 0x1F; + pub(crate) const SCENE_STAMINA_MODE_CSS: u8 = 0x0; + pub(crate) const SCENE_STAMINA_MODE_SSS: u8 = 0x1; + pub(crate) const SCENE_STAMINA_MODE_INGAME: u8 = 0x2; + pub(crate) const SCENE_STAMINA_MODE_POSTGAME: u8 = 0x4; + + pub(crate) const SCENE_HOME_RUN_CONTEST: u8 = 0x20; + pub(crate) const SCENE_HOME_RUN_CONTEST_CSS: u8 = 0x0; + pub(crate) const SCENE_HOME_RUN_CONTEST_INGAME: u8 = 0x1; + + pub(crate) const SCENE_10_MAN_MELEE: u8 = 0x21; + pub(crate) const SCENE_10_MAN_MELEE_CSS: u8 = 0x00; + pub(crate) const SCENE_10_MAN_MELEE_INGAME: u8 = 0x01; + + pub(crate) const SCENE_100_MAN_MELEE: u8 = 0x22; + pub(crate) const SCENE_100_MAN_MELEE_CSS: u8 = 0x00; + pub(crate) const SCENE_100_MAN_MELEE_INGAME: u8 = 0x01; + + pub(crate) const SCENE_3_MINUTE_MELEE: u8 = 0x23; + pub(crate) const SCENE_3_MINUTE_MELEE_CSS: u8 = 0x00; + pub(crate) const SCENE_3_MINUTE_MELEE_INGAME: u8 = 0x01; + + pub(crate) const SCENE_15_MINUTE_MELEE: u8 = 0x24; + pub(crate) const SCENE_15_MINUTE_MELEE_CSS: u8 = 0x00; + pub(crate) const SCENE_15_MINUTE_MELEE_INGAME: u8 = 0x01; + + pub(crate) const SCENE_ENDLESS_MELEE: u8 = 0x25; + pub(crate) const SCENE_ENDLESS_MELEE_CSS: u8 = 0x00; + pub(crate) const SCENE_ENDLESS_MELEE_INGAME: u8 = 0x01; + + pub(crate) const SCENE_CRUEL_MELEE: u8 = 0x26; + pub(crate) const SCENE_CRUEL_MELEE_CSS: u8 = 0x00; + pub(crate) const SCENE_CRUEL_MELEE_INGAME: u8 = 0x01; + + pub(crate) const SCENE_PROGRESSIVE_SCAN: u8 = 0x27; + pub(crate) const SCENE_PLAY_INTRO_VIDEO: u8 = 0x28; + pub(crate) const SCENE_MEMORY_CARD_OVERWRITE: u8 = 0x29; + + pub(crate) const SCENE_FIXED_CAMERA_MODE: u8 = 0x2A; + pub(crate) const SCENE_FIXED_CAMERA_MODE_CSS: u8 = 0x0; + pub(crate) const SCENE_FIXED_CAMERA_MODE_SSS: u8 = 0x1; + pub(crate) const SCENE_FIXED_CAMERA_MODE_INGAME: u8 = 0x2; + pub(crate) const SCENE_FIXED_CAMERA_MODE_POSTGAME: u8 = 0x4; + + pub(crate) const SCENE_EVENT_MATCH: u8 = 0x2B; + pub(crate) const SCENE_EVENT_MATCH_SELECT: u8 = 0x0; + pub(crate) const SCENE_EVENT_MATCH_INGAME: u8 = 0x1; + + pub(crate) const SCENE_SINGLE_BUTTON_MODE: u8 = 0x2C; + pub(crate) const SCENE_SINGLE_BUTTON_MODE_CSS: u8 = 0x0; + pub(crate) const SCENE_SINGLE_BUTTON_MODE_SSS: u8 = 0x1; + pub(crate) const SCENE_SINGLE_BUTTON_MODE_INGAME: u8 = 0x2; + +} \ No newline at end of file diff --git a/discord-rpc/src/tray.rs b/discord-rpc/src/tray.rs deleted file mode 100644 index f693111..0000000 --- a/discord-rpc/src/tray.rs +++ /dev/null @@ -1,286 +0,0 @@ -use std::{mem::MaybeUninit, sync::{atomic::{AtomicBool, self}, Arc, mpsc::Receiver}}; - -use trayicon::{TrayIconBuilder, MenuBuilder}; -use windows::Win32::UI::WindowsAndMessaging::{TranslateMessage, DispatchMessageA, PeekMessageA, PM_REMOVE}; - -use crate::{config::{CONFIG, AppConfig, write_config, APP_INFO}, util::get_appdata_file}; - -use {std::sync::mpsc}; - -struct ExtendedMenuBuilder(MenuBuilder); -impl ExtendedMenuBuilder { - fn new() -> ExtendedMenuBuilder { - ExtendedMenuBuilder(MenuBuilder::::new()) - } - fn checkable(self, name: &str, is_checked: bool, id: TrayEvents) -> Self { - ExtendedMenuBuilder(self.0.checkable(name, is_checked, id)) - } - // checkable with enabled check - fn cwec(self, name: &str, is_checked: bool, id: TrayEvents, enable: &[bool]) -> Self { - ExtendedMenuBuilder(self.0.with(trayicon::MenuItem::Checkable { - id, - name: name.into(), - disabled: enable.iter().any(|v| !v), - is_checked, - icon: None - })) - } - fn submenu(self, name: &str, menu: MenuBuilder) -> Self { - ExtendedMenuBuilder(self.0.submenu(name, menu)) - } -} -impl From for MenuBuilder { - fn from(value: ExtendedMenuBuilder) -> Self { - value.0 - } -} - -#[derive(PartialEq, Clone, Copy)] -pub enum MeleeTrayEvent { - Connected, - Disconnected -} - -#[derive(Clone, Eq, PartialEq, Debug)] -enum TrayEvents { - _Unused, - - // Global - ShowInGameCharacter, - ShowInGameTime, - - // Slippi - EnableSlippi, - SlippiShowQueueing, - SlippiShowOpponentName, - - SlippiEnableRanked, - SlippiRankedShowRank, - SlippiRankedShowViewRankedProfileButton, - SlippiRankedShowScore, - - SlippiEnableUnranked, - - SlippiEnableDirect, - - SlippiEnableTeams, - - // Unclepunch - EnableUnclePunch, - - // Training Mode - EnableTrainingMode, - - // Vs. Mode - EnableVsMode, - - // Stadium - EnableStadium, - - StadiumEnableHRC, - - StadiumEnableBTT, - StadiumBTTShowStageName, - - StadiumEnableMMM, - - // Miscallaneous - OpenConfig, - Quit, -} - -fn build_menu(melee_connected: &Arc) -> MenuBuilder { - CONFIG.with_ref(|c| { - MenuBuilder::new() - .with(trayicon::MenuItem::Item { - id: TrayEvents::_Unused, - name: if melee_connected.load(atomic::Ordering::Relaxed) { "✔️ Connected to Dolphin process" } else { "❌ Searching for Dolphin process..." }.into(), - disabled: true, - icon: None - }) - .separator() - .submenu( - "Global", - MenuBuilder::new() - .checkable("Show Character", c.global.show_in_game_character, TrayEvents::ShowInGameCharacter) - .checkable("Show In-Game Time", c.global.show_in_game_time, TrayEvents::ShowInGameTime) - ) - .submenu( - "Slippi Online", - ExtendedMenuBuilder::new() - .checkable("Enabled", c.slippi.enabled, TrayEvents::EnableSlippi) - .cwec("Show activity when searching", c.slippi.show_queueing, TrayEvents::SlippiShowQueueing, &[c.slippi.enabled]) - .cwec("Show opponent name", c.slippi.show_opponent_name, TrayEvents::SlippiShowOpponentName, &[c.slippi.enabled]) - .submenu( - "Ranked", - ExtendedMenuBuilder::new() - .cwec("Enabled", c.slippi.ranked.enabled, TrayEvents::SlippiEnableRanked, &[c.slippi.enabled]) - .cwec("Show rank", c.slippi.ranked.show_rank, TrayEvents::SlippiRankedShowRank, &[c.slippi.enabled, c.slippi.ranked.enabled]) - .cwec("Show \"View Ranked Profile\" button", c.slippi.ranked.show_view_ranked_profile_button, TrayEvents::SlippiRankedShowViewRankedProfileButton, &[c.slippi.enabled, c.slippi.ranked.enabled]) - .cwec("Show match score", c.slippi.ranked.show_score, TrayEvents::SlippiRankedShowScore, &[c.slippi.enabled, c.slippi.ranked.enabled]) - .into() - ) - .submenu( - "Unranked", - ExtendedMenuBuilder::new() - .cwec("Enabled", c.slippi.unranked.enabled, TrayEvents::SlippiEnableUnranked, &[c.slippi.enabled]) - .into() - ) - .submenu( - "Direct", - ExtendedMenuBuilder::new() - .cwec("Enabled", c.slippi.direct.enabled, TrayEvents::SlippiEnableDirect, &[c.slippi.enabled]) - .into() - ) - .submenu( - "Teams", - ExtendedMenuBuilder::new() - .cwec("Enabled", c.slippi.teams.enabled, TrayEvents::SlippiEnableTeams, &[c.slippi.enabled]) - .into() - ) - .into() - ) - .submenu( - "UnclePunch", - MenuBuilder::new() - .checkable("Enabled", c.uncle_punch.enabled, TrayEvents::EnableUnclePunch) - ) - .submenu( - "Training Mode", - MenuBuilder::new() - .checkable("Enabled", c.training_mode.enabled, TrayEvents::EnableTrainingMode) - ) - .submenu( - "Vs. Mode", - MenuBuilder::new() - .checkable("Enabled", c.vs_mode.enabled, TrayEvents::EnableVsMode) - ) - .submenu( - "Stadium", - ExtendedMenuBuilder::new() - .checkable("Enabled", c.stadium.enabled, TrayEvents::EnableStadium) - .submenu( - "Home-Run Contest", - ExtendedMenuBuilder::new() - .cwec("Enabled", c.stadium.hrc.enabled, TrayEvents::StadiumEnableHRC, &[c.stadium.enabled]) - .into() - ) - .submenu( - "Target Test", - ExtendedMenuBuilder::new() - .cwec("Enabled", c.stadium.btt.enabled, TrayEvents::StadiumEnableBTT, &[c.stadium.enabled]) - .cwec("Show stage name", c.stadium.btt.show_stage_name, TrayEvents::StadiumBTTShowStageName, &[c.stadium.enabled]) - .into() - ) - .submenu( - "Multi-Man Melee", - ExtendedMenuBuilder::new() - .cwec("Enabled", c.stadium.mmm.enabled, TrayEvents::StadiumEnableMMM, &[c.stadium.enabled]) - .into() - ) - .into() - ) - .separator() - .item("Open Configuration File", TrayEvents::OpenConfig) - .item("Quit", TrayEvents::Quit) - }) -} - -pub fn run_tray(mrx: Receiver) { - let melee_connected = Arc::new(AtomicBool::new(false)); - - let (s, r) = mpsc::channel::(); - let icon_raw = include_bytes!("../assets/icon.ico"); - - let mut tray_icon = TrayIconBuilder::new() - .sender(s) - .icon_from_buffer(icon_raw) - .tooltip("Slippi Discord Integration") - .menu( - build_menu(&melee_connected) - ) - .build() - .unwrap(); - - let should_end = Arc::new(AtomicBool::new(false)); - let shared_should_end = should_end.clone(); - std::thread::spawn(move || { - let mut update_menu = || { - tray_icon.set_menu(&build_menu(&melee_connected)).unwrap(); - }; - let mut toggle_handler = |modifier: fn(&mut AppConfig)| { - CONFIG.with_mut(|c| { modifier(c); write_config(c); }); - update_menu(); - }; - - loop { - if let Ok(melee_ev) = mrx.try_recv() { - melee_connected.store(melee_ev == MeleeTrayEvent::Connected, atomic::Ordering::Relaxed); - toggle_handler(|_|{}); - } - if let Ok(tray_ev) = r.try_recv() { - match tray_ev { - TrayEvents::ShowInGameCharacter => toggle_handler(|f| f.global.show_in_game_character = !f.global.show_in_game_character), - TrayEvents::ShowInGameTime => toggle_handler(|f| f.global.show_in_game_time = !f.global.show_in_game_time), - - TrayEvents::EnableSlippi => toggle_handler(|f| f.slippi.enabled = !f.slippi.enabled), - TrayEvents::SlippiShowQueueing => toggle_handler(|f| f.slippi.show_queueing = !f.slippi.show_queueing), - TrayEvents::SlippiShowOpponentName => toggle_handler(|f| f.slippi.show_opponent_name = !f.slippi.show_opponent_name), - - TrayEvents::SlippiEnableRanked => toggle_handler(|f| f.slippi.ranked.enabled = !f.slippi.ranked.enabled), - TrayEvents::SlippiRankedShowRank => toggle_handler(|f| f.slippi.ranked.show_rank = !f.slippi.ranked.show_rank), - TrayEvents::SlippiRankedShowViewRankedProfileButton => toggle_handler(|f| f.slippi.ranked.show_view_ranked_profile_button = !f.slippi.ranked.show_view_ranked_profile_button), - TrayEvents::SlippiRankedShowScore => toggle_handler(|f| f.slippi.ranked.show_score = !f.slippi.ranked.show_score), - - TrayEvents::SlippiEnableUnranked => toggle_handler(|f| f.slippi.unranked.enabled = !f.slippi.unranked.enabled), - - TrayEvents::SlippiEnableDirect => toggle_handler(|f| f.slippi.direct.enabled = !f.slippi.direct.enabled), - - TrayEvents::SlippiEnableTeams => toggle_handler(|f| f.slippi.teams.enabled = !f.slippi.teams.enabled), - - TrayEvents::EnableUnclePunch => toggle_handler(|f| f.uncle_punch.enabled = !f.uncle_punch.enabled), - - TrayEvents::EnableVsMode => toggle_handler(|f| f.vs_mode.enabled = !f.vs_mode.enabled), - - TrayEvents::EnableTrainingMode => toggle_handler(|f| f.training_mode.enabled = !f.training_mode.enabled), - - TrayEvents::EnableStadium => toggle_handler(|f| f.stadium.enabled = !f.stadium.enabled), - - TrayEvents::StadiumEnableHRC => toggle_handler(|f| f.stadium.hrc.enabled = !f.stadium.hrc.enabled), - - TrayEvents::StadiumEnableBTT => toggle_handler(|f| f.stadium.btt.enabled = !f.stadium.btt.enabled), - TrayEvents::StadiumBTTShowStageName => toggle_handler(|f| f.stadium.btt.show_stage_name = !f.stadium.btt.show_stage_name), - - TrayEvents::StadiumEnableMMM => toggle_handler(|f| f.stadium.mmm.enabled = !f.stadium.mmm.enabled), - - TrayEvents::OpenConfig => { - if let Some(conf_file) = get_appdata_file(format!("{}/{}/app_config.prefs.json", APP_INFO.author, APP_INFO.name).as_str()) { - if conf_file.is_file() && conf_file.exists() { - let _ = open::that(conf_file); - } - } - } - TrayEvents::Quit => { - should_end.store(true, atomic::Ordering::Relaxed); - break; - }, - TrayEvents::_Unused => {} - } - } - } - }); - // Application message loop - loop { - if shared_should_end.load(atomic::Ordering::Relaxed) { - break; - } - unsafe { - let mut msg = MaybeUninit::uninit(); - let bret = PeekMessageA(msg.as_mut_ptr(), None, 0, 0, PM_REMOVE); - if bret.as_bool() { - TranslateMessage(msg.as_ptr()); - DispatchMessageA(msg.as_ptr()); - } - } - } -} \ No newline at end of file diff --git a/discord-rpc/src/util.rs b/discord-rpc/src/util.rs deleted file mode 100644 index e7fe202..0000000 --- a/discord-rpc/src/util.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::{time::{SystemTime, UNIX_EPOCH, Duration}, thread, path::PathBuf}; - -use directories::BaseDirs; - -pub fn current_unix_time() -> i64 { - SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs().try_into().unwrap() -} - -pub fn sleep(millis: u64) { - thread::sleep(Duration::from_millis(millis)); -} - -pub fn round(x: f32, decimals: u32) -> f32 { - let y = 10i32.pow(decimals) as f32; - (x * y).round() / y -} - -pub fn get_appdata_file(suffix: &str) -> Option { - if let Some(base_dirs) = BaseDirs::new() { - return Some(base_dirs.config_dir().join(suffix)); - } - None -} diff --git a/discord-rpc/src/utils.rs b/discord-rpc/src/utils.rs new file mode 100644 index 0000000..78809c6 --- /dev/null +++ b/discord-rpc/src/utils.rs @@ -0,0 +1,98 @@ +use std::fs::File; +use std::io::{Read, Seek}; + +use crate::scenes::scene_ids::*; +use crate::{DiscordRPCError::*, Result}; + + + +/// Returns true if the user is in an actual match +/// Sourced from M'Overlay: https://github.com/bkacjios/m-overlay/blob/d8c629d/source/melee.lua#L1177 +pub(crate) fn is_in_game(scene_major: u8, scene_minor: u8) -> bool { + if scene_major == SCENE_ALL_STAR_MODE && scene_minor < SCENE_ALL_STAR_CSS { + return true; + } + if scene_major == SCENE_VS_MODE || scene_major == SCENE_VS_ONLINE { + return scene_minor == SCENE_VS_INGAME; + } + if (SCENE_TRAINING_MODE..=SCENE_STAMINA_MODE).contains(&scene_major) || scene_major == SCENE_FIXED_CAMERA_MODE { + return scene_minor == SCENE_TRAINING_INGAME; + } + if scene_major == SCENE_EVENT_MATCH { + return scene_minor == SCENE_EVENT_MATCH_INGAME; + } + if scene_major == SCENE_CLASSIC_MODE && scene_minor < SCENE_CLASSIC_CONTINUE { + return scene_minor % 2 == 1; + } + if scene_major == SCENE_ADVENTURE_MODE { + return scene_minor == SCENE_ADVENTURE_MUSHROOM_KINGDOM + || scene_minor == SCENE_ADVENTURE_MUSHROOM_KINGDOM_BATTLE + || scene_minor == SCENE_ADVENTURE_MUSHROOM_KONGO_JUNGLE_TINY_BATTLE + || scene_minor == SCENE_ADVENTURE_MUSHROOM_KONGO_JUNGLE_GIANT_BATTLE + || scene_minor == SCENE_ADVENTURE_UNDERGROUND_MAZE + || scene_minor == SCENE_ADVENTURE_HYRULE_TEMPLE_BATTLE + || scene_minor == SCENE_ADVENTURE_BRINSTAR + || scene_minor == SCENE_ADVENTURE_ESCAPE_ZEBES + || scene_minor == SCENE_ADVENTURE_GREEN_GREENS_KIRBY_BATTLE + || scene_minor == SCENE_ADVENTURE_GREEN_GREENS_KIRBY_TEAM_BATTLE + || scene_minor == SCENE_ADVENTURE_GREEN_GREENS_GIANT_KIRBY_BATTLE + || scene_minor == SCENE_ADVENTURE_CORNERIA_BATTLE_1 + || scene_minor == SCENE_ADVENTURE_CORNERIA_BATTLE_2 + || scene_minor == SCENE_ADVENTURE_CORNERIA_BATTLE_3 + || scene_minor == SCENE_ADVENTURE_POKEMON_STADIUM_BATTLE + || scene_minor == SCENE_ADVENTURE_FZERO_GRAND_PRIX_RACE + || scene_minor == SCENE_ADVENTURE_FZERO_GRAND_PRIX_BATTLE + || scene_minor == SCENE_ADVENTURE_ONETT_BATTLE + || scene_minor == SCENE_ADVENTURE_ICICLE_MOUNTAIN_CLIMB + || scene_minor == SCENE_ADVENTURE_BATTLEFIELD_BATTLE + || scene_minor == SCENE_ADVENTURE_BATTLEFIELD_METAL_BATTLE + || scene_minor == SCENE_ADVENTURE_FINAL_DESTINATION_BATTLE; + } + if scene_major == SCENE_TARGET_TEST { + return scene_minor == SCENE_TARGET_TEST_INGAME; + } + if (SCENE_SUPER_SUDDEN_DEATH..=MENU_LIGHTNING_MELEE).contains(&scene_major) { + return scene_minor == SCENE_SSD_INGAME; + } + if (SCENE_HOME_RUN_CONTEST..=SCENE_CRUEL_MELEE).contains(&scene_major) { + return scene_minor == SCENE_HOME_RUN_CONTEST_INGAME; + } + if scene_major == SCENE_TITLE_SCREEN_IDLE { + return scene_minor == SCENE_TITLE_SCREEN_IDLE_FIGHT_1 || scene_minor == SCENE_TITLE_SCREEN_IDLE_FIGHT_2; + } + + false +} + +/// Returns true if the player navigating the menus (including CSS and SSS) +/// Sourced from M'Overlay: https://github.com/bkacjios/m-overlay/blob/d8c629d/source/melee.lua#L1243 +pub(crate) fn is_in_menus(scene_major: u8, scene_minor: u8) -> bool { + if scene_major == SCENE_MAIN_MENU { + return true; + } + if scene_major == SCENE_VS_MODE { + return scene_minor == SCENE_VS_CSS || scene_minor == SCENE_VS_SSS; + } + if scene_major == SCENE_VS_ONLINE { + return scene_minor == SCENE_VS_ONLINE_CSS || scene_minor == SCENE_VS_ONLINE_SSS || scene_minor == SCENE_VS_ONLINE_RANKED; + } + if (SCENE_TRAINING_MODE..=SCENE_STAMINA_MODE).contains(&scene_major) || scene_major == SCENE_FIXED_CAMERA_MODE { + return scene_minor == SCENE_TRAINING_CSS || scene_minor == SCENE_TRAINING_SSS; + } + if scene_major == SCENE_EVENT_MATCH { + return scene_minor == SCENE_EVENT_MATCH_SELECT; + } + if scene_major == SCENE_CLASSIC_MODE || scene_major == SCENE_ADVENTURE_MODE || scene_major == SCENE_ALL_STAR_MODE { + return scene_minor == SCENE_CLASSIC_CSS; + } + if scene_major == SCENE_TARGET_TEST { + return scene_minor == SCENE_TARGET_TEST_CSS; + } + if (SCENE_SUPER_SUDDEN_DEATH..=MENU_LIGHTNING_MELEE).contains(&scene_major) { + return scene_minor == SCENE_SSD_CSS || scene_minor == SCENE_SSD_SSS; + } + if (SCENE_HOME_RUN_CONTEST..=SCENE_CRUEL_MELEE).contains(&scene_major) { + return scene_minor == SCENE_HOME_RUN_CONTEST_CSS; + } + false +} From 62e3abff15f05bd89e15ef2af85059240fe39fd7 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 30 Jan 2024 17:17:27 -0800 Subject: [PATCH 11/13] Adds back support for accessing game memory offset. This data is only accessible after both the EXI device and the memory subsystem have initialized, so there's now a new callback that is notified after initialization of both has completed. This callback is also where the Discord handler is launched; there may still be ongoing work here regarding preferences/launching/etc, so WIP. This also corrects the log name to match the one in the Ishiiruka repo. --- Cargo.lock | 1039 ++++----------------------- discord-rpc/src/lib.rs | 13 +- dolphin/src/logger/mod.rs | 2 +- exi/src/lib.rs | 30 +- ffi/includes/SlippiRustExtensions.h | 5 + ffi/src/exi.rs | 23 + 6 files changed, 199 insertions(+), 913 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9a100c..d28979d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "addr2line" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" -dependencies = [ - "gimli", -] - [[package]] name = "adler" version = "1.0.2" @@ -35,7 +26,7 @@ dependencies = [ "alsa-sys", "bitflags 1.3.2", "libc", - "nix 0.24.3", + "nix", ] [[package]] @@ -55,16 +46,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] -name = "app_dirs" -version = "1.2.1" +name = "arrayvec" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e73a24bad9bd6a94d6395382a6c69fe071708ae4409f763c5475e14ee896313d" -dependencies = [ - "ole32-sys", - "shell32-sys", - "winapi 0.2.8", - "xdg", -] +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "atty" @@ -74,7 +59,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -83,21 +68,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "backtrace" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - [[package]] name = "base64" version = "0.21.4" @@ -142,6 +112,18 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "bytemuck" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2490600f404f2b94c167e31d3ed1d5f3c225a0f3b80230053b3e0b7b962bd9" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.5.0" @@ -254,6 +236,12 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "claxon" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" + [[package]] name = "combine" version = "4.6.6" @@ -264,16 +252,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys 0.8.4", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.6.2" @@ -359,7 +337,7 @@ dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "memoffset 0.9.0", + "memoffset", "scopeguard", ] @@ -384,39 +362,6 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" -[[package]] -name = "directories" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - -[[package]] -name = "discord-rich-presence" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47fc4beffb85ee1461588499073a4d9c20dcc7728c4b13d6b282ab6c508947e5" -dependencies = [ - "serde", - "serde_derive", - "serde_json", - "uuid", -] - [[package]] name = "dolphin-integrations" version = "0.1.0" @@ -455,7 +400,7 @@ checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" dependencies = [ "errno-dragonfly", "libc", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -484,27 +429,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.0" @@ -514,87 +438,12 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures-channel" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" - -[[package]] -name = "futures-sink" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" - -[[package]] -name = "futures-task" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" - -[[package]] -name = "futures-util" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "pin-utils", -] - -[[package]] -name = "getrandom" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gimli" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" - [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" -[[package]] -name = "h2" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap 2.0.2", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -629,86 +478,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] -name = "hps_decode" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be54de29095314358927941aff17c348b7ba400447c63019e62c238359943f1" -dependencies = [ - "nom", - "rayon", - "rodio", - "thiserror", -] - -[[package]] -name = "http" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.3" +name = "hound" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" [[package]] -name = "hyper" -version = "0.14.28" +name = "hps_decode" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "3a265f4bd80aa3aee69875279ebd2e1ab3dd976fae2ead395b22c7ae8e5c7247" dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", + "rayon", + "thiserror", ] [[package]] -name = "hyper-tls" -version = "0.5.0" +name = "hps_decode" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "3be54de29095314358927941aff17c348b7ba400447c63019e62c238359943f1" dependencies = [ - "bytes", - "hyper", - "native-tls", - "tokio", - "tokio-native-tls", + "nom", + "rayon", + "rodio", + "thiserror", ] [[package]] @@ -741,12 +535,6 @@ dependencies = [ "hashbrown 0.14.1", ] -[[package]] -name = "ipnet" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" - [[package]] name = "is-docker" version = "0.2.0" @@ -764,7 +552,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.3", "rustix", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -847,6 +635,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + [[package]] name = "libc" version = "0.2.152" @@ -860,18 +659,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if", - "winapi 0.3.9", -] - -[[package]] -name = "libredox" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" -dependencies = [ - "bitflags 2.4.0", - "libc", - "redox_syscall 0.4.1", + "winapi", ] [[package]] @@ -896,6 +684,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "mach2" version = "0.4.1" @@ -911,15 +708,6 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.9.0" @@ -929,12 +717,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -950,35 +732,6 @@ dependencies = [ "adler", ] -[[package]] -name = "mio" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "native-tls" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "ndk" version = "0.7.0" @@ -988,7 +741,7 @@ dependencies = [ "bitflags 1.3.2", "jni-sys", "ndk-sys", - "num_enum 0.5.11", + "num_enum", "raw-window-handle", "thiserror", ] @@ -1008,19 +761,6 @@ dependencies = [ "jni-sys", ] -[[package]] -name = "nix" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - [[package]] name = "nix" version = "0.24.3" @@ -1049,7 +789,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1072,32 +812,13 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.3", - "libc", -] - [[package]] name = "num_enum" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" dependencies = [ - "num_enum_derive 0.5.11", -] - -[[package]] -name = "num_enum" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" -dependencies = [ - "num_enum_derive 0.6.1", + "num_enum_derive", ] [[package]] @@ -1112,18 +833,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "num_enum_derive" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.38", -] - [[package]] name = "num_threads" version = "0.1.6" @@ -1133,15 +842,6 @@ dependencies = [ "libc", ] -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - [[package]] name = "oboe" version = "0.5.0" @@ -1166,13 +866,12 @@ dependencies = [ ] [[package]] -name = "ole32-sys" -version = "0.2.0" +name = "ogg" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d2c49021782e5233cd243168edfa8037574afed4eba4bbaf538b3d8d1789d8c" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" dependencies = [ - "winapi 0.2.8", - "winapi-build", + "byteorder", ] [[package]] @@ -1181,17 +880,6 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" -[[package]] -name = "open" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a083c0c7e5e4a8ec4176346cf61f67ac674e8bfb059d9226e1c54a96b377c12" -dependencies = [ - "is-wsl", - "libc", - "pathdiff", -] - [[package]] name = "open" version = "5.0.0" @@ -1204,58 +892,8 @@ dependencies = [ ] [[package]] -name = "openssl" -version = "0.10.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" -dependencies = [ - "bitflags 2.4.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.38", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "os_str_bytes" -version = "6.5.1" +name = "os_str_bytes" +version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" @@ -1283,9 +921,9 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", + "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets", ] [[package]] @@ -1312,28 +950,12 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" -[[package]] -name = "preferences" -version = "2.0.0" -source = "git+https://github.com/andybarron/preferences-rs#bcef27fd41d5a7a95de1b56e7ea7199867c926b4" -dependencies = [ - "app_dirs", - "serde", - "serde_json", -] - [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -1353,6 +975,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "process-memory" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9599c34fcc8067c3105dc746c0ce85e3ea61784568b8234179fad490b1dcc1" +dependencies = [ + "libc", + "mach", + "winapi", +] + [[package]] name = "quote" version = "1.0.33" @@ -1397,26 +1030,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_users" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" -dependencies = [ - "getrandom", - "libredox", - "thiserror", -] - [[package]] name = "regex" version = "1.9.6" @@ -1446,44 +1059,6 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" -[[package]] -name = "reqwest" -version = "0.11.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-tls", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "system-configuration", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - [[package]] name = "ring" version = "0.16.20" @@ -1496,7 +1071,7 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1505,15 +1080,13 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdf1d4dea18dff2e9eb6dca123724f8b60ef44ad74a9ad283cdfe025df7e73fa" dependencies = [ + "claxon", "cpal", + "hound", + "lewton", + "symphonia", ] -[[package]] -name = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -1530,7 +1103,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -1555,12 +1128,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "rustversion" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" - [[package]] name = "ryu" version = "1.0.15" @@ -1576,15 +1143,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -1601,29 +1159,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "security-framework" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-foundation-sys 0.8.4", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" -dependencies = [ - "core-foundation-sys 0.8.4", - "libc", -] - [[package]] name = "serde" version = "1.0.188" @@ -1666,18 +1201,6 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -1687,78 +1210,22 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shell32-sys" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee04b46101f57121c9da2b151988283b6beb79b34f5bb29a58ee48cb695122c" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - [[package]] name = "shlex" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - -[[package]] -name = "single-instance" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4637485391f8545c9d3dbf60f9d9aab27a90c789a700999677583bcb17c8795d" -dependencies = [ - "libc", - "nix 0.23.2", - "thiserror", - "widestring", - "winapi 0.3.9", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - [[package]] name = "slippi-discord-rpc" version = "0.1.0" dependencies = [ - "directories", - "discord-rich-presence", "dolphin-integrations", - "encoding_rs", - "lazy_static", - "num_enum 0.6.1", - "open 4.2.0", - "preferences", - "regex", - "reqwest", - "serde", - "serde_json", - "single-instance", - "structstruck", - "strum", - "strum_macros", + "hps_decode 0.1.1", + "process-memory", + "rodio", "thiserror", - "tokio", - "tokio-util", "tracing", - "ureq", ] [[package]] @@ -1794,7 +1261,7 @@ name = "slippi-jukebox" version = "0.1.0" dependencies = [ "dolphin-integrations", - "hps_decode", + "hps_decode 0.2.1", "rodio", "thiserror", "tracing", @@ -1818,7 +1285,7 @@ name = "slippi-user" version = "0.1.0" dependencies = [ "dolphin-integrations", - "open 5.0.0", + "open", "serde", "serde_json", "tracing", @@ -1831,16 +1298,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" -[[package]] -name = "socket2" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" -dependencies = [ - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "spin" version = "0.5.2" @@ -1854,34 +1311,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] -name = "structstruck" -version = "0.4.1" +name = "symphonia" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a052ec87a2d9bdd3a35f85ec6a07a5ac0816e4190b1cbede9d67cccb47ea66d" +checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941" dependencies = [ - "heck", - "proc-macro2", - "quote", - "venial", + "lazy_static", + "symphonia-bundle-mp3", + "symphonia-core", + "symphonia-metadata", ] [[package]] -name = "strum" -version = "0.24.1" +name = "symphonia-bundle-mp3" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] [[package]] -name = "strum_macros" -version = "0.24.3" +name = "symphonia-core" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142" dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89c3e1937e31d0e068bbe829f66b2f2bfaa28d056365279e0ef897172c3320c0" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", ] [[package]] @@ -1906,27 +1382,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys 0.8.4", - "libc", -] - [[package]] name = "tempfile" version = "3.8.0" @@ -1935,9 +1390,9 @@ checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.3.5", + "redox_syscall", "rustix", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -2019,60 +1474,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "tokio" -version = "1.35.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "num_cpus", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-macros" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.38", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - [[package]] name = "toml" version = "0.5.11" @@ -2099,12 +1500,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - [[package]] name = "tracing" version = "0.1.37" @@ -2151,12 +1546,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - [[package]] name = "unicode-bidi" version = "0.3.13" @@ -2213,37 +1602,12 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "uuid" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom", -] - [[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "venial" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61584a325b16f97b5b25fcc852eb9550843a251057a5e3e5992d2376f3df4bb2" -dependencies = [ - "proc-macro2", - "quote", -] - [[package]] name = "walkdir" version = "2.4.0" @@ -2254,21 +1618,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - [[package]] name = "wasm-bindgen" version = "0.2.87" @@ -2351,18 +1700,6 @@ version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" -[[package]] -name = "widestring" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" - -[[package]] -name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" - [[package]] name = "winapi" version = "0.3.9" @@ -2373,12 +1710,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -2391,7 +1722,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -2406,7 +1737,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.5", + "windows-targets", ] [[package]] @@ -2415,16 +1746,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.0", + "windows-targets", ] [[package]] @@ -2433,28 +1755,13 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -2463,84 +1770,42 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" - [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" - [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" - [[package]] name = "winnow" version = "0.5.16" @@ -2549,19 +1814,3 @@ checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907" dependencies = [ "memchr", ] - -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] -name = "xdg" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" diff --git a/discord-rpc/src/lib.rs b/discord-rpc/src/lib.rs index bfc2259..0c782d7 100644 --- a/discord-rpc/src/lib.rs +++ b/discord-rpc/src/lib.rs @@ -81,19 +81,18 @@ enum DiscordRPCEvent { } #[derive(Debug)] -pub struct DiscordRPC { +pub struct DiscordActivityHandler { channel_senders: [Sender; CHILD_THREAD_COUNT], } -impl DiscordRPC { +impl DiscordActivityHandler { /// Returns a DiscordRPC instance that will immediately spawn two child threads /// to try and read game memory and play music. When the returned instance is /// dropped, the child threads will terminate and the music will stop. - pub fn new(m_p_ram: *const u8, iso_path: String, get_dolphin_volume_fn: ForeignGetVolumeFn) -> Result { + pub fn new(m_p_ram: usize, iso_path: String, get_dolphin_volume_fn: ForeignGetVolumeFn) -> Result { tracing::info!(target: Log::DiscordRPC, "Initializing Slippi Discord RPC"); // We are implicitly trusting that these pointers will outlive the jukebox instance - let m_p_ram = m_p_ram as usize; let get_dolphin_volume = move || unsafe { get_dolphin_volume_fn() } as f32 / 100.0; // This channel is used for the `DiscordRPCMessageDispatcher` thread to send @@ -298,14 +297,14 @@ impl DiscordRPC { } } -impl Drop for DiscordRPC { +impl Drop for DiscordActivityHandler { fn drop(&mut self) { - tracing::info!(target: Log::DiscordRPC, "Dropping Slippi DiscordRPC"); + tracing::info!(target: Log::DiscordRPC, "Dropping Slippi DiscordActivityHandler"); for sender in &self.channel_senders { if let Err(e) = sender.send(DiscordRPCEvent::Dropped) { tracing::warn!( target: Log::DiscordRPC, - "Failed to notify child thread that DiscordRPC is dropping: {e}" + "Failed to notify child thread that DiscordActivityHandler is dropping: {e}" ); } } diff --git a/dolphin/src/logger/mod.rs b/dolphin/src/logger/mod.rs index cdaac8f..796d971 100644 --- a/dolphin/src/logger/mod.rs +++ b/dolphin/src/logger/mod.rs @@ -55,7 +55,7 @@ pub mod Log { pub const Jukebox: &'static str = "SLIPPI_RUST_JUKEBOX"; /// Used for any logs specific to the Discord RPC library. - pub const DiscordRPC: &'static str = "SLIPPI_DISCORD_RPC"; + pub const DiscordRPC: &'static str = "SLIPPI_RUST_DISCORD_RPC"; } /// Represents a `LogContainer` on the Dolphin side. diff --git a/exi/src/lib.rs b/exi/src/lib.rs index 8f903b7..36a5cef 100644 --- a/exi/src/lib.rs +++ b/exi/src/lib.rs @@ -10,7 +10,7 @@ use std::time::Duration; use ureq::AgentBuilder; use dolphin_integrations::Log; -use slippi_discord_rpc::{Config as DiscordHandlerConfig, DiscordHandler}; +use slippi_discord_rpc::{Config as DiscordActivityHandlerConfig, DiscordActivityHandler}; use slippi_game_reporter::GameReporter; use slippi_jukebox::Jukebox; use slippi_user::UserManager; @@ -31,9 +31,9 @@ pub enum JukeboxConfiguration { /// Configuration instructions that the FFI layer uses to call over here. #[derive(Debug)] -pub enum DiscordHandlerConfiguration { - Start { ram_offset: u8, config: DiscordHandlerConfig }, - UpdateConfig { config: DiscordHandlerConfig }, +pub enum DiscordActivityHandlerConfiguration { + Start { m_p_ram: usize, config: DiscordActivityHandlerConfig }, + UpdateConfig { config: DiscordActivityHandlerConfig }, Stop, } @@ -44,7 +44,7 @@ pub struct SlippiEXIDevice { pub game_reporter: GameReporter, pub user_manager: UserManager, pub jukebox: Option, - pub discord_handler: Option, + pub discord_handler: Option, } impl SlippiEXIDevice { @@ -91,6 +91,16 @@ impl SlippiEXIDevice { /// Stubbed for now, but this would get called by the C++ EXI device on DMARead. pub fn dma_read(&mut self, _address: usize, _size: usize) {} + /// Called when the Memory system on Dolphin has initialized - i.e, when it's safe to + /// check and read the offset for memory watching. This launches any background tasks that + /// need access to that parameter. + pub fn on_memory_initialized(&mut self, m_p_ram: usize) { + self.configure_discord_handler(DiscordActivityHandlerConfiguration::Start { + m_p_ram, + config: DiscordActivityHandlerConfig::default() + }); + } + /// Configures a new Jukebox, or ensures an existing one is dropped if it's being disabled. pub fn configure_jukebox(&mut self, config: JukeboxConfiguration) { if let JukeboxConfiguration::Stop = config { @@ -128,14 +138,14 @@ impl SlippiEXIDevice { /// Configures a new Discord handler, or ensures an existing one is dropped if it's being /// disabled. - pub fn configure_discord_handler(&mut self, config: DiscordHandlerConfiguration) { - if let DiscordHandlerConfiguration::Stop = config { + pub fn configure_discord_handler(&mut self, config: DiscordActivityHandlerConfiguration) { + if let DiscordActivityHandlerConfiguration::Stop = config { self.discord_handler = None; return; } if let Some(discord_handler) = &mut self.discord_handler { - if let DiscordHandlerConfiguration::UpdateConfig { config } = config { + if let DiscordActivityHandlerConfiguration::UpdateConfig { config } = config { discord_handler.update_config(config); return; } @@ -144,8 +154,8 @@ impl SlippiEXIDevice { return; } - if let DiscordHandlerConfiguration::Start { ram_offset, config } = config { - match DiscordHandler::new(ram_offset.into(), config) { + if let DiscordActivityHandlerConfiguration::Start { m_p_ram, config } = config { + match DiscordActivityHandler::new(m_p_ram, config) { Ok(handler) => { self.discord_handler = Some(handler); }, diff --git a/ffi/includes/SlippiRustExtensions.h b/ffi/includes/SlippiRustExtensions.h index 6e50fb8..b7e16bd 100644 --- a/ffi/includes/SlippiRustExtensions.h +++ b/ffi/includes/SlippiRustExtensions.h @@ -66,6 +66,11 @@ uintptr_t slprs_exi_device_create(SlippiRustEXIConfig config); /// can safely shut down and clean up. void slprs_exi_device_destroy(uintptr_t exi_device_instance_ptr); +/// This method is for the C++ side to notify that the Memory system is initialized and ready +/// for use; the EXI device can then initialize any systems it needs that rely on the offset. +void slprs_exi_device_on_memory_initialized(uintptr_t exi_device_instance_ptr, + const uint8_t *m_pRAM); + /// This method should be called from the EXI device subclass shim that's registered on /// the Dolphin side, corresponding to: /// diff --git a/ffi/src/exi.rs b/ffi/src/exi.rs index 1206964..291c122 100644 --- a/ffi/src/exi.rs +++ b/ffi/src/exi.rs @@ -90,6 +90,29 @@ pub extern "C" fn slprs_exi_device_destroy(exi_device_instance_ptr: usize) { } } +/// This method is for the C++ side to notify that the Memory system is initialized and ready +/// for use; the EXI device can then initialize any systems it needs that rely on the offset. +#[no_mangle] +pub extern "C" fn slprs_exi_device_on_memory_initialized(exi_device_instance_ptr: usize, m_pRAM: *const u8) { + let offset = m_pRAM as usize; + + tracing::warn!( + target: Log::SlippiOnline, + ptr = exi_device_instance_ptr, + m_pRAM = offset + ); + + // Coerce the instance back from the pointer. This is theoretically safe since we control + // the C++ side and can guarantee that the `exi_device_instance_ptr` pointer is only owned + // by the C++ EXI device, and is created/destroyed with the corresponding lifetimes. + let mut device = unsafe { Box::from_raw(exi_device_instance_ptr as *mut SlippiEXIDevice) }; + + device.on_memory_initialized(offset); + + // Fall back into a raw pointer so Rust doesn't obliterate the object + let _leak = Box::into_raw(device); +} + /// This method should be called from the EXI device subclass shim that's registered on /// the Dolphin side, corresponding to: /// From de96765cad32d3f7a48b1c0f363a1a4558795eed Mon Sep 17 00:00:00 2001 From: Jas Laferriere Date: Thu, 1 Feb 2024 13:59:21 -0500 Subject: [PATCH 12/13] fix the discord rpc stub so that it logs events --- discord-rpc/src/lib.rs | 157 +++++----------------------- discord-rpc/src/utils.rs | 6 -- exi/src/lib.rs | 30 +++--- ffi/includes/SlippiRustExtensions.h | 4 +- ffi/src/exi.rs | 25 +++-- 5 files changed, 60 insertions(+), 162 deletions(-) diff --git a/discord-rpc/src/lib.rs b/discord-rpc/src/lib.rs index 0c782d7..2430eb1 100644 --- a/discord-rpc/src/lib.rs +++ b/discord-rpc/src/lib.rs @@ -1,13 +1,8 @@ -use std::convert::TryInto; -use std::fs::File; -use std::ops::ControlFlow::{self, Break, Continue}; use std::sync::mpsc::{channel, Receiver, Sender}; use std::{thread::sleep, time::Duration}; -use dolphin_integrations::{Color, Dolphin, Duration as OSDDuration, Log}; -use hps_decode::{Hps, PcmIterator}; +use dolphin_integrations::Log; use process_memory::{LocalMember, Memory}; -use rodio::{OutputStream, Sink}; mod errors; pub use errors::DiscordRPCError; @@ -16,22 +11,11 @@ use DiscordRPCError::*; mod scenes; use scenes::scene_ids::*; - mod utils; pub(crate) type Result = std::result::Result; -/// Represents a foreign method from the Dolphin side for grabbing the current volume. -/// Dolphin represents this as a number from 0 - 100; 0 being mute. -pub type ForeignGetVolumeFn = unsafe extern "C" fn() -> std::ffi::c_int; - const THREAD_LOOP_SLEEP_TIME_MS: u64 = 30; -const CHILD_THREAD_COUNT: usize = 2; - -/// By default Slippi DiscordRPC plays music slightly louder than vanilla melee -/// does. This reduces the overall music volume output to 80%. Not totally sure -/// if that's the correct amount, but it sounds about right. -const VOLUME_REDUCTION_MULTIPLIER: f32 = 0.8; #[derive(Debug, PartialEq)] struct DolphinGameState { @@ -40,7 +24,6 @@ struct DolphinGameState { scene_major: u8, scene_minor: u8, stage_id: u8, - volume: f32, is_paused: bool, match_info: u8, } @@ -53,7 +36,6 @@ impl Default for DolphinGameState { scene_major: SCENE_MAIN_MENU, scene_minor: 0, stage_id: 0, - volume: 0.0, is_paused: false, match_info: 0, } @@ -71,82 +53,66 @@ enum MeleeEvent { VsOnlineOpponent, Pause, Unpause, - SetVolume(f32), NoOp, } #[derive(Debug, Clone)] -enum DiscordRPCEvent { +enum Message { Dropped, } #[derive(Debug)] pub struct DiscordActivityHandler { - channel_senders: [Sender; CHILD_THREAD_COUNT], + tx: Sender, } impl DiscordActivityHandler { /// Returns a DiscordRPC instance that will immediately spawn two child threads /// to try and read game memory and play music. When the returned instance is /// dropped, the child threads will terminate and the music will stop. - pub fn new(m_p_ram: usize, iso_path: String, get_dolphin_volume_fn: ForeignGetVolumeFn) -> Result { + pub fn new(m_p_ram: usize) -> Result { tracing::info!(target: Log::DiscordRPC, "Initializing Slippi Discord RPC"); - // We are implicitly trusting that these pointers will outlive the jukebox instance - let get_dolphin_volume = move || unsafe { get_dolphin_volume_fn() } as f32 / 100.0; - - // This channel is used for the `DiscordRPCMessageDispatcher` thread to send - // messages to the `DiscordRPCMusicPlayer` thread - let (melee_event_tx, melee_event_rx) = channel::(); - // These channels allow the jukebox instance to notify both child // threads when something important happens. Currently its only purpose // is to notify them that the instance is about to be dropped so they // should terminate - let (message_dispatcher_thread_tx, message_dispatcher_thread_rx) = channel::(); - let (music_thread_tx, music_thread_rx) = channel::(); + let (tx, rx) = channel::(); // Spawn message dispatcher thread std::thread::Builder::new() .name("DiscordRPCMessageDispatcher".to_string()) - .spawn(move || { - match Self::dispatch_messages(m_p_ram, get_dolphin_volume, message_dispatcher_thread_rx, melee_event_tx) { - Err(e) => tracing::error!( - target: Log::DiscordRPC, - error = ?e, - "DiscordRPCMessageDispatcher thread encountered an error: {e}" - ), - _ => (), - } + .spawn(move || match Self::dispatch_messages(m_p_ram, rx) { + Err(e) => tracing::error!( + target: Log::DiscordRPC, + error = ?e, + "DiscordRPCMessageDispatcher thread encountered an error: {e}" + ), + _ => (), }) .map_err(ThreadSpawn)?; - + Ok(Self { tx }) } /// This thread continuously reads select values from game memory as well /// as the current `volume` value in the dolphin configuration. If it /// notices anything change, it will dispatch a message to the /// `DiscordRPCMusicPlayer` thread. - fn dispatch_messages( - m_p_ram: usize, - get_dolphin_volume: impl Fn() -> f32, - message_dispatcher_thread_rx: Receiver, - melee_event_tx: Sender, - ) -> Result<()> { + fn dispatch_messages(m_p_ram: usize, rx: Receiver) -> Result<()> { // Initial "dolphin state" that will get updated over time let mut prev_state = DolphinGameState::default(); loop { // Stop the thread if the jukebox instance will be been dropped - if let Ok(event) = message_dispatcher_thread_rx.try_recv() { - if matches!(event, DiscordRPCEvent::Dropped) { + if let Ok(event) = rx.try_recv() { + if matches!(event, Message::Dropped) { return Ok(()); } } // Continuously check if the dolphin state has changed - let state = Self::read_dolphin_game_state(&m_p_ram, get_dolphin_volume())?; + let state = Self::read_dolphin_game_state(&m_p_ram)?; // If the state has changed, if prev_state != state { @@ -154,7 +120,8 @@ impl DiscordActivityHandler { let event = Self::produce_melee_event(&prev_state, &state); tracing::info!(target: Log::DiscordRPC, "{:?}", event); - melee_event_tx.send(event).ok(); + // TODO: Do something with the event + prev_state = state; } @@ -162,72 +129,6 @@ impl DiscordActivityHandler { } } - /// This thread listens for incoming messages from the - /// `DiscordRPCMessageDispatcher` thread and handles music playback - /// accordingly. - - - /// Handle a events received in the audio playback thread, by changing tracks, - /// adjusting volume etc. - fn handle_melee_event( - event: MeleeEvent, - sink: &Sink, - volume: &mut f32, - ) -> ControlFlow<()> { - use self::MeleeEvent::*; - - // TODO: - // - Intro movie - // - // - classic vs screen - // - classic victory screen - // - classic game over screen - // - classic credits - // - classic "congratulations movie" - // - Adventure mode field intro music - - match event { - TitleScreenEntered | GameEnd => { - - NoOp; - }, - MenuEntered => { - - NoOp; - }, - LotteryEntered => { - NoOp; - }, - VsOnlineOpponent => { - NoOp; - }, - RankedStageStrikeEntered => { - NoOp; - }, - GameStart(stage_id) => { - NoOp; - }, - Pause => { - sink.set_volume(*volume * 0.2); - return Continue(()); - }, - Unpause => { - sink.set_volume(*volume); - return Continue(()); - }, - SetVolume(received_volume) => { - sink.set_volume(received_volume); - *volume = received_volume; - return Continue(()); - }, - NoOp => { - return Continue(()); - }, - }; - - Break(()) - } - /// Given the previous dolphin state and current dolphin state, produce an event fn produce_melee_event(prev_state: &DolphinGameState, state: &DolphinGameState) -> MeleeEvent { let vs_screen_1 = state.scene_major == SCENE_VS_ONLINE @@ -253,8 +154,6 @@ impl DiscordActivityHandler { MeleeEvent::GameStart(state.stage_id) } else if prev_state.in_game && state.in_game && state.match_info == 1 { MeleeEvent::GameEnd - } else if prev_state.volume != state.volume { - MeleeEvent::SetVolume(state.volume) } else if !prev_state.is_paused && state.is_paused { MeleeEvent::Pause } else if prev_state.is_paused && !state.is_paused { @@ -265,13 +164,12 @@ impl DiscordActivityHandler { } /// Create a `DolphinGameState` by reading Dolphin's memory - fn read_dolphin_game_state(m_p_ram: &usize, dolphin_volume_percent: f32) -> Result { + fn read_dolphin_game_state(m_p_ram: &usize) -> Result { #[inline(always)] fn read(offset: usize) -> Result { Ok(unsafe { LocalMember::::new_offset(vec![offset]).read().map_err(DolphinMemoryRead)? }) } - // https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L8 - let melee_volume_percent = ((read::(m_p_ram + 0x45C384)? as f32 - 100.0) * -1.0) / 100.0; + // https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L16 let scene_major = read::(m_p_ram + 0x479D30)?; // https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L19 @@ -289,7 +187,6 @@ impl DiscordActivityHandler { in_menus: utils::is_in_menus(scene_major, scene_minor), scene_major, scene_minor, - volume: dolphin_volume_percent * melee_volume_percent * VOLUME_REDUCTION_MULTIPLIER, stage_id, is_paused, match_info, @@ -300,13 +197,11 @@ impl DiscordActivityHandler { impl Drop for DiscordActivityHandler { fn drop(&mut self) { tracing::info!(target: Log::DiscordRPC, "Dropping Slippi DiscordActivityHandler"); - for sender in &self.channel_senders { - if let Err(e) = sender.send(DiscordRPCEvent::Dropped) { - tracing::warn!( - target: Log::DiscordRPC, - "Failed to notify child thread that DiscordActivityHandler is dropping: {e}" - ); - } + if let Err(e) = self.tx.send(Message::Dropped) { + tracing::warn!( + target: Log::DiscordRPC, + "Failed to notify child thread that DiscordActivityHandler is dropping: {e}" + ); } } } diff --git a/discord-rpc/src/utils.rs b/discord-rpc/src/utils.rs index 78809c6..8cd79da 100644 --- a/discord-rpc/src/utils.rs +++ b/discord-rpc/src/utils.rs @@ -1,10 +1,4 @@ -use std::fs::File; -use std::io::{Read, Seek}; - use crate::scenes::scene_ids::*; -use crate::{DiscordRPCError::*, Result}; - - /// Returns true if the user is in an actual match /// Sourced from M'Overlay: https://github.com/bkacjios/m-overlay/blob/d8c629d/source/melee.lua#L1177 diff --git a/exi/src/lib.rs b/exi/src/lib.rs index 36a5cef..b120846 100644 --- a/exi/src/lib.rs +++ b/exi/src/lib.rs @@ -10,7 +10,7 @@ use std::time::Duration; use ureq::AgentBuilder; use dolphin_integrations::Log; -use slippi_discord_rpc::{Config as DiscordActivityHandlerConfig, DiscordActivityHandler}; +use slippi_discord_rpc::DiscordActivityHandler; use slippi_game_reporter::GameReporter; use slippi_jukebox::Jukebox; use slippi_user::UserManager; @@ -32,8 +32,7 @@ pub enum JukeboxConfiguration { /// Configuration instructions that the FFI layer uses to call over here. #[derive(Debug)] pub enum DiscordActivityHandlerConfiguration { - Start { m_p_ram: usize, config: DiscordActivityHandlerConfig }, - UpdateConfig { config: DiscordActivityHandlerConfig }, + Start { m_p_ram: usize }, Stop, } @@ -95,10 +94,7 @@ impl SlippiEXIDevice { /// check and read the offset for memory watching. This launches any background tasks that /// need access to that parameter. pub fn on_memory_initialized(&mut self, m_p_ram: usize) { - self.configure_discord_handler(DiscordActivityHandlerConfiguration::Start { - m_p_ram, - config: DiscordActivityHandlerConfig::default() - }); + self.configure_discord_handler(DiscordActivityHandlerConfiguration::Start { m_p_ram }); } /// Configures a new Jukebox, or ensures an existing one is dropped if it's being disabled. @@ -144,18 +140,18 @@ impl SlippiEXIDevice { return; } - if let Some(discord_handler) = &mut self.discord_handler { - if let DiscordActivityHandlerConfiguration::UpdateConfig { config } = config { - discord_handler.update_config(config); - return; - } + // if let Some(discord_handler) = &mut self.discord_handler { + // if let DiscordActivityHandlerConfiguration::UpdateConfig { config } = config { + // // discord_handler.update_config(config); + // return; + // } - tracing::warn!(target: Log::SlippiOnline, "Discord handler is already running."); - return; - } + // tracing::warn!(target: Log::SlippiOnline, "Discord handler is already running."); + // return; + // } - if let DiscordActivityHandlerConfiguration::Start { m_p_ram, config } = config { - match DiscordActivityHandler::new(m_p_ram, config) { + if let DiscordActivityHandlerConfiguration::Start { m_p_ram } = config { + match DiscordActivityHandler::new(m_p_ram) { Ok(handler) => { self.discord_handler = Some(handler); }, diff --git a/ffi/includes/SlippiRustExtensions.h b/ffi/includes/SlippiRustExtensions.h index b7e16bd..fdfc829 100644 --- a/ffi/includes/SlippiRustExtensions.h +++ b/ffi/includes/SlippiRustExtensions.h @@ -69,7 +69,7 @@ void slprs_exi_device_destroy(uintptr_t exi_device_instance_ptr); /// This method is for the C++ side to notify that the Memory system is initialized and ready /// for use; the EXI device can then initialize any systems it needs that rely on the offset. void slprs_exi_device_on_memory_initialized(uintptr_t exi_device_instance_ptr, - const uint8_t *m_pRAM); + const uint8_t *m_p_ram); /// This method should be called from the EXI device subclass shim that's registered on /// the Dolphin side, corresponding to: @@ -120,6 +120,8 @@ void slprs_exi_device_configure_jukebox(uintptr_t exi_device_instance_ptr, uint8_t initial_dolphin_system_volume, uint8_t initial_dolphin_music_volume); +void slprs_start_discord_rich_presence(uintptr_t exi_device_instance_ptr, const uint8_t *m_p_ram); + /// Creates a new Player Report and leaks it, returning the pointer. /// /// This should be passed on to a GameReport for processing. diff --git a/ffi/src/exi.rs b/ffi/src/exi.rs index 291c122..39bd12d 100644 --- a/ffi/src/exi.rs +++ b/ffi/src/exi.rs @@ -93,14 +93,10 @@ pub extern "C" fn slprs_exi_device_destroy(exi_device_instance_ptr: usize) { /// This method is for the C++ side to notify that the Memory system is initialized and ready /// for use; the EXI device can then initialize any systems it needs that rely on the offset. #[no_mangle] -pub extern "C" fn slprs_exi_device_on_memory_initialized(exi_device_instance_ptr: usize, m_pRAM: *const u8) { - let offset = m_pRAM as usize; +pub extern "C" fn slprs_exi_device_on_memory_initialized(exi_device_instance_ptr: usize, m_p_ram: *const u8) { + let offset = m_p_ram as usize; - tracing::warn!( - target: Log::SlippiOnline, - ptr = exi_device_instance_ptr, - m_pRAM = offset - ); + tracing::warn!(target: Log::SlippiOnline, ptr = exi_device_instance_ptr, m_pRAM = offset); // Coerce the instance back from the pointer. This is theoretically safe since we control // the C++ side and can guarantee that the `exi_device_instance_ptr` pointer is only owned @@ -265,3 +261,18 @@ pub extern "C" fn slprs_exi_device_configure_jukebox( // Fall back into a raw pointer so Rust doesn't obliterate the object. let _leak = Box::into_raw(device); } + +#[no_mangle] +pub extern "C" fn slprs_start_discord_rich_presence(exi_device_instance_ptr: usize, m_p_ram: *const u8) { + // Coerce the instance from the pointer. This is theoretically safe since we control + // the C++ side and can guarantee that the `exi_device_instance_ptr` is only owned + // by the C++ EXI device, and is created/destroyed with the corresponding lifetimes. + let mut device = unsafe { Box::from_raw(exi_device_instance_ptr as *mut SlippiEXIDevice) }; + + let m_p_ram = m_p_ram as usize; + let config = slippi_exi_device::DiscordActivityHandlerConfiguration::Start { m_p_ram }; + device.configure_discord_handler(config); + + // Fall back into a raw pointer so Rust doesn't obliterate the object. + let _leak = Box::into_raw(device); +} From 59e2e2373f3897e3f2fc3fb10de359ce5a71b6b3 Mon Sep 17 00:00:00 2001 From: Anders Madsen Date: Fri, 2 Feb 2024 01:10:19 +0100 Subject: [PATCH 13/13] Update --- discord-rpc/src/lib.rs | 85 +++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 55 deletions(-) diff --git a/discord-rpc/src/lib.rs b/discord-rpc/src/lib.rs index 2430eb1..fc31dfe 100644 --- a/discord-rpc/src/lib.rs +++ b/discord-rpc/src/lib.rs @@ -1,19 +1,23 @@ -use std::sync::mpsc::{channel, Receiver, Sender}; -use std::{thread::sleep, time::Duration}; +use std::{ + result::Result as StdResult, + sync::mpsc::{channel, Receiver, Sender}, + thread::{self, sleep}, + time::Duration, +}; use dolphin_integrations::Log; -use process_memory::{LocalMember, Memory}; +use process_memory::{DataMember, LocalMember, Memory}; mod errors; -pub use errors::DiscordRPCError; +use crate::errors::DiscordRPCError; use DiscordRPCError::*; mod scenes; -use scenes::scene_ids::*; +use crate::scenes::scene_ids::*; mod utils; -pub(crate) type Result = std::result::Result; +pub(crate) type Result = StdResult; const THREAD_LOOP_SLEEP_TIME_MS: u64 = 30; @@ -58,7 +62,7 @@ enum MeleeEvent { #[derive(Debug, Clone)] enum Message { - Dropped, + Exit, } #[derive(Debug)] @@ -67,70 +71,47 @@ pub struct DiscordActivityHandler { } impl DiscordActivityHandler { - /// Returns a DiscordRPC instance that will immediately spawn two child threads - /// to try and read game memory and play music. When the returned instance is - /// dropped, the child threads will terminate and the music will stop. + /// Initialize a new DiscordRPC instance, spawning threads for + /// message dispatching with game state monitoring. pub fn new(m_p_ram: usize) -> Result { - tracing::info!(target: Log::DiscordRPC, "Initializing Slippi Discord RPC"); - - // These channels allow the jukebox instance to notify both child - // threads when something important happens. Currently its only purpose - // is to notify them that the instance is about to be dropped so they - // should terminate let (tx, rx) = channel::(); // Spawn message dispatcher thread - std::thread::Builder::new() + let _ = thread::Builder::new() .name("DiscordRPCMessageDispatcher".to_string()) - .spawn(move || match Self::dispatch_messages(m_p_ram, rx) { - Err(e) => tracing::error!( - target: Log::DiscordRPC, - error = ?e, - "DiscordRPCMessageDispatcher thread encountered an error: {e}" - ), - _ => (), + .spawn(move || { + if let Err(e) = Self::message_dispatcher(m_p_ram, rx) { + eprintln!("Error in dispatcher: {}", e); + } }) - .map_err(ThreadSpawn)?; + .map_err(|_| ThreadSpawn); Ok(Self { tx }) } - /// This thread continuously reads select values from game memory as well - /// as the current `volume` value in the dolphin configuration. If it - /// notices anything change, it will dispatch a message to the - /// `DiscordRPCMusicPlayer` thread. - fn dispatch_messages(m_p_ram: usize, rx: Receiver) -> Result<()> { - // Initial "dolphin state" that will get updated over time + /// This thread dispatches messages based on game state changes. + fn message_dispatcher(m_p_ram: usize, rx: Receiver) -> Result<()> { let mut prev_state = DolphinGameState::default(); loop { - // Stop the thread if the jukebox instance will be been dropped - if let Ok(event) = rx.try_recv() { - if matches!(event, Message::Dropped) { - return Ok(()); - } + if let Ok(Message::Exit) = rx.try_recv() { + return Ok(()); } - // Continuously check if the dolphin state has changed let state = Self::read_dolphin_game_state(&m_p_ram)?; - - // If the state has changed, - if prev_state != state { - // dispatch a message to the music player thread + if state != prev_state { let event = Self::produce_melee_event(&prev_state, &state); tracing::info!(target: Log::DiscordRPC, "{:?}", event); - - // TODO: Do something with the event - prev_state = state; } - sleep(Duration::from_millis(THREAD_LOOP_SLEEP_TIME_MS)); } } - /// Given the previous dolphin state and current dolphin state, produce an event - fn produce_melee_event(prev_state: &DolphinGameState, state: &DolphinGameState) -> MeleeEvent { + /// Given the previous dolphin state and current dolphin state, produce an event + fn produce_melee_event(prev_state: &DolphinGameState, state: &DolphinGameState) -> MeleeEvent { + tracing::info!(target: Log::DiscordRPC, "Major: {:?}", state.scene_major); + tracing::info!(target: Log::DiscordRPC, "Minor: {:?}", state.scene_minor); let vs_screen_1 = state.scene_major == SCENE_VS_ONLINE && prev_state.scene_minor != SCENE_VS_ONLINE_VERSUS && state.scene_minor == SCENE_VS_ONLINE_VERSUS; @@ -162,8 +143,6 @@ impl DiscordActivityHandler { MeleeEvent::NoOp } } - - /// Create a `DolphinGameState` by reading Dolphin's memory fn read_dolphin_game_state(m_p_ram: &usize) -> Result { #[inline(always)] fn read(offset: usize) -> Result { @@ -196,12 +175,8 @@ impl DiscordActivityHandler { impl Drop for DiscordActivityHandler { fn drop(&mut self) { - tracing::info!(target: Log::DiscordRPC, "Dropping Slippi DiscordActivityHandler"); - if let Err(e) = self.tx.send(Message::Dropped) { - tracing::warn!( - target: Log::DiscordRPC, - "Failed to notify child thread that DiscordActivityHandler is dropping: {e}" - ); + if self.tx.send(Message::Exit).is_err() { + eprintln!("Error sending exit message to dispatcher"); } } }