Add Discord RPC support
This commit is contained in:
parent
fc7ddb78b7
commit
f070a34d73
80
game/game_logic/src/discord/ipc.rs
Normal file
80
game/game_logic/src/discord/ipc.rs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
//! Discord Rich Presence utilities
|
||||||
|
|
||||||
|
use discord_sdk::{
|
||||||
|
activity::{Activity, ActivityBuilder},
|
||||||
|
user::User,
|
||||||
|
wheel::Wheel,
|
||||||
|
Discord, DiscordApp, Subscriptions,
|
||||||
|
};
|
||||||
|
use tokio::time::error::Elapsed;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum DiscordError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Sdk(#[from] discord_sdk::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
AwaitConnection(#[from] tokio::sync::watch::error::RecvError),
|
||||||
|
#[error("Could not connect")]
|
||||||
|
Connection,
|
||||||
|
#[error(transparent)]
|
||||||
|
ConnectionTimeout(#[from] Elapsed),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The client wrapper for Discord RPC
|
||||||
|
pub struct DiscordRpcClient {
|
||||||
|
pub discord: Discord,
|
||||||
|
pub user: User,
|
||||||
|
pub wheel: Wheel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiscordRpcClient {
|
||||||
|
/// Creates a new `DiscordRpcClient`
|
||||||
|
pub async fn new(app_id: i64, subscriptions: Subscriptions) -> Result<Self, DiscordError> {
|
||||||
|
// Create a new wheel
|
||||||
|
let (wheel, handler) = Wheel::new(Box::new(|err| {
|
||||||
|
error!("Encountered an error: {}", err);
|
||||||
|
}));
|
||||||
|
let mut user = wheel.user();
|
||||||
|
|
||||||
|
// Create the client
|
||||||
|
let discord = Discord::new(
|
||||||
|
DiscordApp::PlainId(app_id),
|
||||||
|
subscriptions,
|
||||||
|
Box::new(handler),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Wait for the discord handshake
|
||||||
|
info!("Waiting for Discord client handshake");
|
||||||
|
user.0.changed().await?;
|
||||||
|
info!("Discord handshake success");
|
||||||
|
|
||||||
|
// Fetch the final user object
|
||||||
|
let user = match &*user.0.borrow() {
|
||||||
|
discord_sdk::wheel::UserState::Connected(u) => Ok(u.clone()),
|
||||||
|
discord_sdk::wheel::UserState::Disconnected(_) => Err(DiscordError::Connection),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
discord,
|
||||||
|
user,
|
||||||
|
wheel,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the user rich presence
|
||||||
|
#[profiling::function]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn clear_rich_presence(&self) -> Result<Option<Activity>, discord_sdk::Error> {
|
||||||
|
self.discord
|
||||||
|
.update_activity(ActivityBuilder::default())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the user rich presence
|
||||||
|
pub async fn set_rich_presence(
|
||||||
|
&self,
|
||||||
|
activity: ActivityBuilder,
|
||||||
|
) -> Result<Option<Activity>, discord_sdk::Error> {
|
||||||
|
self.discord.update_activity(activity).await
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,87 @@
|
|||||||
//! This module contains code needed for interacting with a local Discord instance.
|
//! This module contains code needed for interacting with a local Discord instance.
|
||||||
|
|
||||||
mod signal;
|
mod signal;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
pub use signal::DiscordRpcSignal;
|
pub use signal::DiscordRpcSignal;
|
||||||
|
use tokio::{
|
||||||
|
sync::{mpsc::Receiver, mpsc::Sender},
|
||||||
|
task::JoinHandle,
|
||||||
|
};
|
||||||
|
|
||||||
|
use self::{ipc::DiscordRpcClient, signal::StatefulDiscordRpcSignalHandler};
|
||||||
|
mod ipc;
|
||||||
|
pub use ipc::DiscordError;
|
||||||
|
|
||||||
|
/// How long to wait before we give up on connecting to Discord.
|
||||||
|
const DISCORD_CONNECT_TIMEOUT_SECONDS: u64 = 5;
|
||||||
|
|
||||||
|
/// A cross-thread communication channel for sending Discord RPC events.
|
||||||
|
pub type DiscordChannel = Sender<DiscordRpcSignal>;
|
||||||
|
|
||||||
|
pub struct DiscordRpcThreadHandle {
|
||||||
|
tx_chan: DiscordChannel,
|
||||||
|
rx_chan: Receiver<DiscordRpcSignal>,
|
||||||
|
internal_client: Option<DiscordRpcClient>,
|
||||||
|
state: StatefulDiscordRpcSignalHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiscordRpcThreadHandle {
|
||||||
|
/// Construct a new `DiscordRpcThreadHandle`
|
||||||
|
pub async fn new(app_id: i64) -> Result<Self, DiscordError> {
|
||||||
|
// Create the Discord client
|
||||||
|
info!("Trying to locate and connect to a local Discord process for RPC. Will wait up to {} seconds before timing out", DISCORD_CONNECT_TIMEOUT_SECONDS);
|
||||||
|
let rpc_client = match tokio::time::timeout(
|
||||||
|
Duration::from_secs(DISCORD_CONNECT_TIMEOUT_SECONDS),
|
||||||
|
DiscordRpcClient::new(app_id, discord_sdk::Subscriptions::ACTIVITY),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(client) => Some(client?),
|
||||||
|
Err(t) => {
|
||||||
|
error!(
|
||||||
|
"Timed out trying to connect to Discord RPC. Duration: {}",
|
||||||
|
t
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!("Successfully connected to Discord");
|
||||||
|
|
||||||
|
// Set up channels
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::channel(5);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
tx_chan: tx,
|
||||||
|
rx_chan: rx,
|
||||||
|
internal_client: rpc_client,
|
||||||
|
state: StatefulDiscordRpcSignalHandler::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get access to the inter-thread channel for communicating to discord
|
||||||
|
pub fn get_channel(&self) -> DiscordChannel {
|
||||||
|
self.tx_chan.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the inner communication task in an async context
|
||||||
|
pub fn begin_thread_non_blocking(mut self) -> JoinHandle<()> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
// Handle any possible incoming events
|
||||||
|
match self.rx_chan.try_recv() {
|
||||||
|
Ok(signal) => match self.internal_client {
|
||||||
|
Some(ref client) => {
|
||||||
|
client
|
||||||
|
.set_rich_presence(self.state.apply(signal))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
None => { /* The client could not connect */ }
|
||||||
|
},
|
||||||
|
Err(_) => { /* Do Nothing */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,9 +6,10 @@
|
|||||||
//! The game thread can then send `DiscordRpcSignal` values through an `mpsc` sender, which will be received by the Discord RPC client thread.
|
//! The game thread can then send `DiscordRpcSignal` values through an `mpsc` sender, which will be received by the Discord RPC client thread.
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use discord_sdk::activity::{ActivityBuilder, Assets, IntoTimestamp};
|
use discord_sdk::activity::{ActivityBuilder, Assets};
|
||||||
|
|
||||||
/// Definitions of signals that can be sent to the Discord RPC thread to control how discord displays game status.
|
/// Definitions of signals that can be sent to the Discord RPC thread to control how discord displays game status.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub enum DiscordRpcSignal {
|
pub enum DiscordRpcSignal {
|
||||||
/// Signal to begin a game timer (Discord will display `XX:XX elapsed`)
|
/// Signal to begin a game timer (Discord will display `XX:XX elapsed`)
|
||||||
BeginGameTimer,
|
BeginGameTimer,
|
||||||
@ -50,7 +51,6 @@ pub struct StatefulDiscordRpcSignalHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StatefulDiscordRpcSignalHandler {
|
impl StatefulDiscordRpcSignalHandler {
|
||||||
|
|
||||||
/// Apply a signal to generate a new activity
|
/// Apply a signal to generate a new activity
|
||||||
pub fn apply(&mut self, signal: DiscordRpcSignal) -> ActivityBuilder {
|
pub fn apply(&mut self, signal: DiscordRpcSignal) -> ActivityBuilder {
|
||||||
// Fill in the data based on the contents of the signal
|
// Fill in the data based on the contents of the signal
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
//! This file is the main entry point for the game logic.
|
//! This file is the main entry point for the game logic.
|
||||||
|
|
||||||
use crate::{asset_manager::json::load_json_structure, project_constants::ProjectConstants};
|
use crate::{
|
||||||
|
asset_manager::json::load_json_structure,
|
||||||
|
discord::{DiscordRpcSignal, DiscordRpcThreadHandle},
|
||||||
|
project_constants::ProjectConstants,
|
||||||
|
};
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate approx; // For the macro `relative_eq!`
|
extern crate approx; // For the macro `relative_eq!`
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log; // For the `info!`, `warn!`, etc. macros
|
||||||
|
|
||||||
pub mod asset_manager;
|
pub mod asset_manager;
|
||||||
pub mod discord;
|
pub mod discord;
|
||||||
@ -32,6 +38,24 @@ pub async fn entrypoint(force_recreate_savefiles: bool) {
|
|||||||
persistent::save_state::GameSaveState::load_or_create(force_recreate_savefiles)
|
persistent::save_state::GameSaveState::load_or_create(force_recreate_savefiles)
|
||||||
.expect("Failed to parse game save state from disk. Possibly corrupt file?");
|
.expect("Failed to parse game save state from disk. Possibly corrupt file?");
|
||||||
|
|
||||||
|
// Connect to Discord
|
||||||
|
let discord = DiscordRpcThreadHandle::new(project_constants.discord_app_id)
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect to Discord RPC");
|
||||||
|
let event_loop_discord_tx = discord.get_channel();
|
||||||
|
let _discord_task_handle = discord.begin_thread_non_blocking();
|
||||||
|
|
||||||
|
// Set a base activity to show in Discord
|
||||||
|
{
|
||||||
|
event_loop_discord_tx
|
||||||
|
.send(DiscordRpcSignal::ChangeDetails {
|
||||||
|
details: "Probably loading something IDK.".to_string(),
|
||||||
|
party_status: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Failed to send Discord RPC event");
|
||||||
|
}
|
||||||
|
|
||||||
// Blocking call to the graphics rendering loop.
|
// Blocking call to the graphics rendering loop.
|
||||||
rendering::event_loop::handle_graphics_blocking(
|
rendering::event_loop::handle_graphics_blocking(
|
||||||
|builder| {
|
|builder| {
|
||||||
@ -43,6 +67,7 @@ pub async fn entrypoint(force_recreate_savefiles: bool) {
|
|||||||
.width(project_constants.base_window_size.0 as i32);
|
.width(project_constants.base_window_size.0 as i32);
|
||||||
},
|
},
|
||||||
settings.target_fps,
|
settings.target_fps,
|
||||||
|
event_loop_discord_tx,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clean up any resources
|
// Clean up any resources
|
||||||
|
@ -10,7 +10,7 @@ pub struct ProjectConstants {
|
|||||||
pub base_window_size: (u32, u32),
|
pub base_window_size: (u32, u32),
|
||||||
|
|
||||||
/// The Discord application ID
|
/// The Discord application ID
|
||||||
pub discord_app_id: u64,
|
pub discord_app_id: i64,
|
||||||
|
|
||||||
/// The target framerate of the game
|
/// The target framerate of the game
|
||||||
pub target_fps: u32,
|
pub target_fps: u32,
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
use raylib::RaylibBuilder;
|
use raylib::RaylibBuilder;
|
||||||
|
|
||||||
|
use crate::discord::DiscordChannel;
|
||||||
|
|
||||||
/// Will begin rendering graphics. Returns when the window closes
|
/// Will begin rendering graphics. Returns when the window closes
|
||||||
pub fn handle_graphics_blocking<ConfigBuilder>(config: ConfigBuilder, target_frames_per_second: u32)
|
pub fn handle_graphics_blocking<ConfigBuilder>(config: ConfigBuilder, target_frames_per_second: u32, discord_signaling: DiscordChannel)
|
||||||
where
|
where
|
||||||
ConfigBuilder: FnOnce(&mut RaylibBuilder),
|
ConfigBuilder: FnOnce(&mut RaylibBuilder),
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user