Skip to content

Commit

Permalink
fix the discord rpc stub so that it logs events
Browse files Browse the repository at this point in the history
  • Loading branch information
JLaferri committed Feb 1, 2024
1 parent 62e3abf commit de96765
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 162 deletions.
157 changes: 26 additions & 131 deletions discord-rpc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,22 +11,11 @@ use DiscordRPCError::*;
mod scenes;
use scenes::scene_ids::*;


mod utils;

pub(crate) type Result<T> = std::result::Result<T, DiscordRPCError>;

/// 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 {
Expand All @@ -40,7 +24,6 @@ struct DolphinGameState {
scene_major: u8,
scene_minor: u8,
stage_id: u8,
volume: f32,
is_paused: bool,
match_info: u8,
}
Expand All @@ -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,
}
Expand All @@ -71,163 +53,82 @@ enum MeleeEvent {
VsOnlineOpponent,
Pause,
Unpause,
SetVolume(f32),
NoOp,
}

#[derive(Debug, Clone)]
enum DiscordRPCEvent {
enum Message {
Dropped,
}

#[derive(Debug)]
pub struct DiscordActivityHandler {
channel_senders: [Sender<DiscordRPCEvent>; CHILD_THREAD_COUNT],
tx: Sender<Message>,
}

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<Self> {
pub fn new(m_p_ram: usize) -> Result<Self> {
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::<MeleeEvent>();

// 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::<DiscordRPCEvent>();
let (music_thread_tx, music_thread_rx) = channel::<DiscordRPCEvent>();
let (tx, rx) = channel::<Message>();

// 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<DiscordRPCEvent>,
melee_event_tx: Sender<MeleeEvent>,
) -> Result<()> {
fn dispatch_messages(m_p_ram: usize, rx: Receiver<Message>) -> 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 {
// 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();
// TODO: Do something with the event

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.

/// 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
Expand All @@ -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 {
Expand All @@ -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<DolphinGameState> {
fn read_dolphin_game_state(m_p_ram: &usize) -> Result<DolphinGameState> {
#[inline(always)]
fn read<T: Copy>(offset: usize) -> Result<T> {
Ok(unsafe { LocalMember::<T>::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::<i8>(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::<u8>(m_p_ram + 0x479D30)?;
// https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L19
Expand All @@ -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,
Expand All @@ -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}"
);
}
}
}
6 changes: 0 additions & 6 deletions discord-rpc/src/utils.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
30 changes: 13 additions & 17 deletions exi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
},
Expand Down
4 changes: 3 additions & 1 deletion ffi/includes/SlippiRustExtensions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit de96765

Please sign in to comment.