diff --git a/Cargo.lock b/Cargo.lock index 39fa9af..d28979d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,12 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "atty" version = "0.2.14" @@ -106,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" @@ -218,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" @@ -353,6 +377,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" @@ -444,6 +477,22 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "hps_decode" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a265f4bd80aa3aee69875279ebd2e1ab3dd976fae2ead395b22c7ae8e5c7247" +dependencies = [ + "rayon", + "thiserror", +] + [[package]] name = "hps_decode" version = "0.2.1" @@ -586,11 +635,22 @@ 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.148" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libloading" @@ -624,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" @@ -796,6 +865,15 @@ dependencies = [ "cc", ] +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -897,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" @@ -991,7 +1080,11 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdf1d4dea18dff2e9eb6dca123724f8b60ef44ad74a9ad283cdfe025df7e73fa" dependencies = [ + "claxon", "cpal", + "hound", + "lewton", + "symphonia", ] [[package]] @@ -1123,11 +1216,24 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +[[package]] +name = "slippi-discord-rpc" +version = "0.1.0" +dependencies = [ + "dolphin-integrations", + "hps_decode 0.1.1", + "process-memory", + "rodio", + "thiserror", + "tracing", +] + [[package]] name = "slippi-exi-device" version = "0.1.0" dependencies = [ "dolphin-integrations", + "slippi-discord-rpc", "slippi-game-reporter", "slippi-jukebox", "slippi-user", @@ -1155,7 +1261,7 @@ name = "slippi-jukebox" version = "0.1.0" dependencies = [ "dolphin-integrations", - "hps_decode", + "hps_decode 0.2.1", "rodio", "thiserror", "tracing", @@ -1204,6 +1310,56 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "symphonia" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941" +dependencies = [ + "lazy_static", + "symphonia-bundle-mp3", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142" +dependencies = [ + "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]] name = "syn" version = "1.0.109" 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 new file mode 100644 index 0000000..6f3be31 --- /dev/null +++ b/discord-rpc/Cargo.toml @@ -0,0 +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 + +[features] +default = [] +ishiiruka = [] +mainline = [] + +[dependencies] +dolphin-integrations = { path = "../dolphin" } +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/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 new file mode 100644 index 0000000..fc31dfe --- /dev/null +++ b/discord-rpc/src/lib.rs @@ -0,0 +1,182 @@ +use std::{ + result::Result as StdResult, + sync::mpsc::{channel, Receiver, Sender}, + thread::{self, sleep}, + time::Duration, +}; + +use dolphin_integrations::Log; +use process_memory::{DataMember, LocalMember, Memory}; + +mod errors; +use crate::errors::DiscordRPCError; +use DiscordRPCError::*; + +mod scenes; +use crate::scenes::scene_ids::*; + +mod utils; + +pub(crate) type Result = StdResult; + +const THREAD_LOOP_SLEEP_TIME_MS: u64 = 30; + +#[derive(Debug, PartialEq)] +struct DolphinGameState { + in_game: bool, + in_menus: bool, + scene_major: u8, + scene_minor: u8, + stage_id: u8, + 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, + is_paused: false, + match_info: 0, + } + } +} + +#[derive(Debug)] +enum MeleeEvent { + TitleScreenEntered, + MenuEntered, + LotteryEntered, + GameStart(u8), // stage id + GameEnd, + RankedStageStrikeEntered, + VsOnlineOpponent, + Pause, + Unpause, + NoOp, +} + +#[derive(Debug, Clone)] +enum Message { + Exit, +} + +#[derive(Debug)] +pub struct DiscordActivityHandler { + tx: Sender, +} + +impl DiscordActivityHandler { + /// Initialize a new DiscordRPC instance, spawning threads for + /// message dispatching with game state monitoring. + pub fn new(m_p_ram: usize) -> Result { + let (tx, rx) = channel::(); + + // Spawn message dispatcher thread + let _ = thread::Builder::new() + .name("DiscordRPCMessageDispatcher".to_string()) + .spawn(move || { + if let Err(e) = Self::message_dispatcher(m_p_ram, rx) { + eprintln!("Error in dispatcher: {}", e); + } + }) + .map_err(|_| ThreadSpawn); + + Ok(Self { tx }) + } + + /// 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 { + if let Ok(Message::Exit) = rx.try_recv() { + return Ok(()); + } + + let state = Self::read_dolphin_game_state(&m_p_ram)?; + if state != prev_state { + let event = Self::produce_melee_event(&prev_state, &state); + tracing::info!(target: Log::DiscordRPC, "{:?}", 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 { + 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; + 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.is_paused && state.is_paused { + MeleeEvent::Pause + } else if prev_state.is_paused && !state.is_paused { + MeleeEvent::Unpause + } else { + MeleeEvent::NoOp + } + } + 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#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, + stage_id, + is_paused, + match_info, + }) + } +} + +impl Drop for DiscordActivityHandler { + fn drop(&mut self) { + if self.tx.send(Message::Exit).is_err() { + eprintln!("Error sending exit message to dispatcher"); + } + } +} 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/utils.rs b/discord-rpc/src/utils.rs new file mode 100644 index 0000000..8cd79da --- /dev/null +++ b/discord-rpc/src/utils.rs @@ -0,0 +1,92 @@ +use crate::scenes::scene_ids::*; + +/// 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 +} diff --git a/dolphin/src/logger/mod.rs b/dolphin/src/logger/mod.rs index 72e0f04..796d971 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_RUST_DISCORD_RPC"; } /// Represents a `LogContainer` on the Dolphin side. diff --git a/exi/Cargo.toml b/exi/Cargo.toml index 9710da9..f4ac02c 100644 --- a/exi/Cargo.toml +++ b/exi/Cargo.toml @@ -16,6 +16,7 @@ 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" } diff --git a/exi/src/lib.rs b/exi/src/lib.rs index 5f1885c..b120846 100644 --- a/exi/src/lib.rs +++ b/exi/src/lib.rs @@ -10,6 +10,7 @@ use std::time::Duration; use ureq::AgentBuilder; use dolphin_integrations::Log; +use slippi_discord_rpc::DiscordActivityHandler; use slippi_game_reporter::GameReporter; use slippi_jukebox::Jukebox; use slippi_user::UserManager; @@ -17,23 +18,34 @@ use slippi_user::UserManager; 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, -} - 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 DiscordActivityHandlerConfiguration { + Start { m_p_ram: usize }, + 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. /// @@ -68,6 +80,7 @@ impl SlippiEXIDevice { game_reporter, user_manager, jukebox: None, + discord_handler: None, } } @@ -77,6 +90,13 @@ 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 }); + } + /// 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 { @@ -111,4 +131,39 @@ 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: DiscordActivityHandlerConfiguration) { + if let DiscordActivityHandlerConfiguration::Stop = config { + self.discord_handler = None; + 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; + // } + + if let DiscordActivityHandlerConfiguration::Start { m_p_ram } = config { + match DiscordActivityHandler::new(m_p_ram) { + Ok(handler) => { + self.discord_handler = Some(handler); + }, + + Err(e) => { + tracing::error!( + target: Log::SlippiOnline, + error = ?e, + "Failed to start Discord handler" + ); + }, + } + } + } } diff --git a/ffi/includes/SlippiRustExtensions.h b/ffi/includes/SlippiRustExtensions.h index 6e50fb8..fdfc829 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_p_ram); + /// This method should be called from the EXI device subclass shim that's registered on /// the Dolphin side, corresponding to: /// @@ -115,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 1206964..39bd12d 100644 --- a/ffi/src/exi.rs +++ b/ffi/src/exi.rs @@ -90,6 +90,25 @@ 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_p_ram: *const u8) { + let offset = m_p_ram 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: /// @@ -242,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); +} 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 }