mirror of
https://github.com/doukutsu-rs/doukutsu-rs
synced 2025-01-28 13:26:48 +00:00
add experimental discord rich presence support
This commit is contained in:
parent
1810bf6d5b
commit
3dbe56690a
|
@ -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"
|
||||
|
|
|
@ -126,7 +126,8 @@
|
|||
"entry": "Cutscene Skip:",
|
||||
"hold": "Hold to Skip",
|
||||
"fastforward": "Fast-Forward"
|
||||
}
|
||||
},
|
||||
"discord_rpc": "Discord Rich Presence:"
|
||||
},
|
||||
"links": "Links...",
|
||||
"advanced": "Advanced...",
|
||||
|
|
|
@ -126,7 +126,8 @@
|
|||
"entry": "カットシーンをスキップ",
|
||||
"hold": "を押し続け",
|
||||
"fastforward": "はやおくり"
|
||||
}
|
||||
},
|
||||
"discord_rpc": "Discord Rich Presence:"
|
||||
},
|
||||
"links": "リンク",
|
||||
"advanced": "詳細設定",
|
||||
|
|
121
src/discord/mod.rs
Normal file
121
src/discord/mod.rs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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())?;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue