add experimental discord rich presence support

This commit is contained in:
József Sallai 2023-02-18 20:42:00 +02:00
parent 1810bf6d5b
commit 3dbe56690a
18 changed files with 231 additions and 8 deletions

View File

@ -37,7 +37,7 @@ category = "Game"
osx_minimum_system_version = "10.12"
[features]
default = ["default-base", "backend-sdl", "render-opengl", "exe", "webbrowser"]
default = ["default-base", "backend-sdl", "render-opengl", "exe", "webbrowser", "discord-rpc"]
default-base = ["ogg-playback"]
ogg-playback = ["lewton"]
backend-sdl = ["sdl2", "sdl2-sys"]
@ -45,6 +45,7 @@ backend-glutin = ["winit", "glutin", "render-opengl"]
backend-horizon = []
render-opengl = []
scripting-lua = ["lua-ffi"]
discord-rpc = []
netplay = ["serde_cbor"]
editor = []
exe = []
@ -62,6 +63,7 @@ case_insensitive_hashmap = "1.0.0"
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
cpal = { git = "https://github.com/doukutsu-rs/cpal", rev = "9d269d8724102404e73a61e9def0c0cbc921b676" }
directories = "3"
discord-rich-presence = "0.2"
downcast = "0.11"
glutin = { git = "https://github.com/doukutsu-rs/glutin.git", rev = "2dd95f042e6e090d36f577cbea125560dd99bd27", optional = true, default_features = false, features = ["x11"] }
imgui = "0.8"

View File

@ -126,7 +126,8 @@
"entry": "Cutscene Skip:",
"hold": "Hold to Skip",
"fastforward": "Fast-Forward"
}
},
"discord_rpc": "Discord Rich Presence:"
},
"links": "Links...",
"advanced": "Advanced...",

View File

@ -126,7 +126,8 @@
"entry": "カットシーンをスキップ",
"hold": "を押し続け",
"fastforward": "はやおくり"
}
},
"discord_rpc": "Discord Rich Presence:"
},
"links": "リンク",
"advanced": "詳細設定",

121
src/discord/mod.rs Normal file
View File

@ -0,0 +1,121 @@
use discord_rich_presence::{
activity::{Activity, Assets, Button},
DiscordIpc, DiscordIpcClient,
};
use crate::framework::error::{GameError, GameResult};
use crate::game::{player::Player, stage::StageData};
pub enum DiscordRPCState {
Initializing,
Idling,
InGame,
Jukebox,
}
pub struct DiscordRPC {
pub enabled: bool,
pub ready: bool,
client: DiscordIpcClient,
state: DiscordRPCState,
life: u16,
max_life: u16,
stage_name: String,
}
impl DiscordRPC {
pub fn new(app_id: &str) -> Self {
Self {
enabled: false,
ready: false,
client: DiscordIpcClient::new(app_id).unwrap(),
state: DiscordRPCState::Idling,
life: 0,
max_life: 0,
stage_name: String::new(),
}
}
pub fn start(&mut self) -> GameResult {
log::info!("Starting Discord RPC client...");
match self.client.connect() {
Ok(_) => {
self.ready = true;
Ok(())
}
Err(e) => Err(GameError::DiscordRPCError(e.to_string())),
}
}
fn update(&mut self) -> GameResult {
if !self.enabled {
return Ok(());
}
let (state, details) = match self.state {
DiscordRPCState::Initializing => ("Initializing...".to_owned(), "Just started playing".to_owned()),
DiscordRPCState::Idling => ("In the menus".to_owned(), "Idling".to_owned()),
DiscordRPCState::InGame => {
(format!("Currently in: {}", self.stage_name), format!("HP: {} / {}", self.life, self.max_life))
}
DiscordRPCState::Jukebox => ("In the menus".to_owned(), "Listening to the soundtrack".to_owned()),
};
log::debug!("Updating Discord RPC state: {} - {}", state, details);
let activity = Activity::new()
.state(state.as_str())
.details(details.as_str())
.assets(Assets::new().large_image("drs"))
.buttons(vec![Button::new("doukutsu-rs on GitHub", "https://github.com/doukutsu-rs/doukutsu-rs")]);
match self.client.set_activity(activity) {
Ok(_) => Ok(()),
Err(e) => Err(GameError::DiscordRPCError(e.to_string())),
}
}
pub fn update_stage(&mut self, stage: &StageData) -> GameResult {
self.stage_name = stage.name.clone();
self.update()
}
pub fn update_hp(&mut self, player: &Player) -> GameResult {
self.life = player.life;
self.max_life = player.max_life;
self.update()
}
pub fn set_initializing(&mut self) -> GameResult {
self.set_state(DiscordRPCState::Initializing)
}
pub fn set_idling(&mut self) -> GameResult {
self.set_state(DiscordRPCState::Idling)
}
pub fn set_in_game(&mut self) -> GameResult {
self.set_state(DiscordRPCState::InGame)
}
pub fn set_in_jukebox(&mut self) -> GameResult {
self.set_state(DiscordRPCState::Jukebox)
}
pub fn set_state(&mut self, state: DiscordRPCState) -> GameResult {
self.state = state;
self.update()
}
pub fn clear(&mut self) -> GameResult {
let _ = self.client.clear_activity();
Ok(())
}
pub fn dispose(&mut self) {
let _ = self.client.close();
}
}

View File

@ -3,8 +3,8 @@
use std::error::Error;
use std::fmt;
use std::string::FromUtf8Error;
use std::sync::{Arc, PoisonError};
use std::sync::mpsc::SendError;
use std::sync::{Arc, PoisonError};
/// An enum containing all kinds of game framework errors.
#[derive(Debug, Clone)]
@ -44,6 +44,8 @@ pub enum GameError {
InvalidValue(String),
/// Something went wrong while executing a debug command line command.
CommandLineError(String),
/// Something went wrong while initializing or modifying Discord rich presence values.
DiscordRPCError(String),
}
impl fmt::Display for GameError {

View File

@ -237,6 +237,12 @@ pub fn init(options: LaunchOptions) -> GameResult {
game.state.get_mut().fs_container = Some(fs_container);
#[cfg(feature = "discord-rpc")]
if game.state.get_mut().settings.discord_rpc {
game.state.get_mut().discord_rpc.enabled = true;
game.state.get_mut().discord_rpc.start()?;
}
game.state.get_mut().next_scene = Some(Box::new(LoadingScene::new()));
log::info!("Starting main loop...");
context.run(game.as_mut().get_mut())?;

View File

@ -923,6 +923,9 @@ impl Player {
let _ = npc_list.spawn(0x100, npc.clone());
}
}
#[cfg(feature = "discord-rpc")]
let _ = state.discord_rpc.update_hp(&self);
}
pub fn update_teleport_counter(&mut self, state: &SharedGameState) {

View File

@ -319,6 +319,9 @@ impl Player {
npc.cond.set_alive(false);
state.sound_manager.play_sfx(20);
#[cfg(feature = "discord-rpc")]
let _ = state.discord_rpc.update_hp(&self);
}
_ => {}
}

View File

@ -11,8 +11,8 @@ use std::rc::Rc;
use num_traits::{clamp, FromPrimitive};
use crate::bitfield;
use crate::common::{Direction, FadeDirection, FadeState, Rect};
use crate::common::Direction::{Left, Right};
use crate::common::{Direction, FadeDirection, FadeState, Rect};
use crate::engine_constants::EngineConstants;
use crate::entity::GameEntity;
use crate::framework::context::Context;
@ -1032,6 +1032,9 @@ impl TextScriptVM {
game_scene.player2.life += life;
game_scene.player2.max_life += life;
#[cfg(feature = "discord-rpc")]
state.discord_rpc.update_hp(&game_scene.player1)?;
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
}
TSCOpCode::FAC => {
@ -1534,6 +1537,9 @@ impl TextScriptVM {
game_scene.player1.life = clamp(game_scene.player1.life + life, 0, game_scene.player1.max_life);
game_scene.player2.life = clamp(game_scene.player2.life + life, 0, game_scene.player2.max_life);
#[cfg(feature = "discord-rpc")]
state.discord_rpc.update_hp(&game_scene.player1)?;
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
}
TSCOpCode::ITp => {

View File

@ -81,6 +81,8 @@ pub struct Settings {
pub more_rust: bool,
#[serde(default = "default_cutscene_skip_mode")]
pub cutscene_skip_mode: CutsceneSkipMode,
#[serde(default = "default_true")]
pub discord_rpc: bool,
}
fn default_true() -> bool {
@ -89,7 +91,7 @@ fn default_true() -> bool {
#[inline(always)]
fn current_version() -> u32 {
21
22
}
#[inline(always)]
@ -327,6 +329,11 @@ impl Settings {
};
}
if self.version == 21 {
self.version = 22;
self.discord_rpc = true;
}
if self.version != initial_version {
log::info!("Upgraded configuration file from version {} to {}.", initial_version, self.version);
}
@ -432,6 +439,7 @@ impl Default for Settings {
noclip: false,
more_rust: false,
cutscene_skip_mode: CutsceneSkipMode::Hold,
discord_rpc: true,
}
}
}

View File

@ -5,6 +5,8 @@ use chrono::{Datelike, Local};
use crate::common::{ControlFlags, Direction, FadeState};
use crate::components::draw_common::{draw_number, Alignment};
use crate::data::vanilla::VanillaExtractor;
#[cfg(feature = "discord-rpc")]
use crate::discord::DiscordRPC;
use crate::engine_constants::EngineConstants;
use crate::framework::backend::BackendTexture;
use crate::framework::context::Context;
@ -334,6 +336,8 @@ pub struct SharedGameState {
pub loc: Locale,
pub tutorial_counter: u16,
pub more_rust: bool,
#[cfg(feature = "discord-rpc")]
pub discord_rpc: DiscordRPC,
pub shutdown: bool,
}
@ -434,6 +438,11 @@ impl SharedGameState {
let more_rust = (current_time.month() == 7 && current_time.day() == 7) || settings.more_rust;
let seed = chrono::Local::now().timestamp() as i32;
let discord_rpc_app_id = match option_env!("DISCORD_RPC_APP_ID") {
Some(app_id) => app_id,
None => "1076523467337367622",
};
Ok(SharedGameState {
control_flags: ControlFlags(0),
game_flags: BitVec::with_size(8000),
@ -489,6 +498,8 @@ impl SharedGameState {
loc: locale,
tutorial_counter: 0,
more_rust,
#[cfg(feature = "discord-rpc")]
discord_rpc: DiscordRPC::new(discord_rpc_app_id),
shutdown: false,
})
}
@ -709,6 +720,9 @@ impl SharedGameState {
pub fn shutdown(&mut self) {
self.shutdown = true;
#[cfg(feature = "discord-rpc")]
self.discord_rpc.dispose();
}
// Stops SFX 40/41/58 (CPS and CSS)

View File

@ -9,6 +9,8 @@ extern crate strum_macros;
mod common;
mod components;
mod data;
#[cfg(feature = "discord-rpc")]
pub mod discord;
#[cfg(feature = "editor")]
mod editor;
mod engine_constants;

View File

@ -1,11 +1,11 @@
use num_traits::FromPrimitive;
use crate::framework::error::{GameError::CommandLineError, GameResult};
use crate::game::shared_game_state::SharedGameState;
use crate::game::npc::NPC;
use crate::scene::game_scene::GameScene;
use crate::game::scripting::tsc::text_script::{ScriptMode, TextScript, TextScriptEncoding};
use crate::game::shared_game_state::SharedGameState;
use crate::game::weapon::WeaponType;
use crate::scene::game_scene::GameScene;
#[derive(Clone)]
pub enum CommandLineCommand {
@ -230,6 +230,9 @@ impl CommandLineCommand {
CommandLineCommand::SetMaxHP(hp_count) => {
game_scene.player1.max_life = hp_count;
game_scene.player1.life = hp_count;
#[cfg(feature = "discord-rpc")]
state.discord_rpc.update_hp(&game_scene.player1)?;
}
CommandLineCommand::SpawnNPC(id) => {
let mut npc = NPC::create(id, &state.npc_table);

View File

@ -268,6 +268,9 @@ impl LiveDebugger {
if scene.player1.life == 0 {
scene.player1.life = scene.player1.max_life;
#[cfg(feature = "discord-rpc")]
let _ = state.discord_rpc.update_hp(&scene.player1);
}
scene.player2 = game_scene.player2.clone();

View File

@ -114,6 +114,8 @@ enum BehaviorMenuEntry {
GameTiming,
PauseOnFocusLoss,
CutsceneSkipMode,
#[cfg(feature = "discord-rpc")]
DiscordRPC,
Back,
}
@ -500,6 +502,15 @@ impl SettingsMenu {
),
);
#[cfg(feature = "discord-rpc")]
self.behavior.push_entry(
BehaviorMenuEntry::DiscordRPC,
MenuEntry::Toggle(
state.loc.t("menus.options_menu.behavior_menu.discord_rpc").to_owned(),
state.settings.discord_rpc,
),
);
self.behavior.push_entry(BehaviorMenuEntry::Back, MenuEntry::Active(state.loc.t("common.back").to_owned()));
self.links.push_entry(LinksMenuEntry::Back, MenuEntry::Active(state.loc.t("common.back").to_owned()));
@ -899,6 +910,26 @@ impl SettingsMenu {
let _ = state.settings.save(ctx);
}
}
#[cfg(feature = "discord-rpc")]
MenuSelectionResult::Selected(BehaviorMenuEntry::DiscordRPC, toggle) => {
if let MenuEntry::Toggle(_, value) = toggle {
state.settings.discord_rpc = !state.settings.discord_rpc;
let _ = state.settings.save(ctx);
*value = state.settings.discord_rpc;
state.discord_rpc.enabled = state.settings.discord_rpc;
if state.discord_rpc.enabled {
if !state.discord_rpc.ready {
state.discord_rpc.start()?;
}
state.discord_rpc.set_idling()?;
} else {
state.discord_rpc.clear()?;
}
}
}
MenuSelectionResult::Selected(BehaviorMenuEntry::Back, _) | MenuSelectionResult::Canceled => {
self.current = CurrentMenu::MainMenu;
}

View File

@ -1739,6 +1739,17 @@ impl Scene for GameScene {
self.pause_menu.init(state, ctx)?;
self.whimsical_star.init(&self.player1);
#[cfg(feature = "discord-rpc")]
{
if self.stage.data.map == state.stages[state.constants.game.intro_stage as usize].map {
state.discord_rpc.set_initializing()?;
} else {
state.discord_rpc.update_hp(&self.player1)?;
state.discord_rpc.update_stage(&self.stage.data)?;
state.discord_rpc.set_in_game()?;
}
}
Ok(())
}

View File

@ -104,6 +104,9 @@ impl Scene for JukeboxScene {
self.previous_pause_on_focus_loss_setting = state.settings.pause_on_focus_loss;
state.settings.pause_on_focus_loss = false;
#[cfg(feature = "discord-rpc")]
state.discord_rpc.set_in_jukebox()?;
Ok(())
}

View File

@ -277,6 +277,9 @@ impl Scene for TitleScene {
state.replay_state = ReplayState::None;
state.textscript_vm.flags.set_cutscene_skip(false);
#[cfg(feature = "discord-rpc")]
state.discord_rpc.set_idling()?;
Ok(())
}