Merge branch 'AldanTanneo/discord-presence' into 'master'

Discord Activity

See merge request veloren/veloren!3529
This commit is contained in:
Joshua Barretto 2022-08-15 15:58:38 +00:00
commit dfe2e1c085
20 changed files with 767 additions and 110 deletions

53
Cargo.lock generated
View File

@ -128,6 +128,24 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "anyhow"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "508b352bb5c066aac251f6daf6b36eccd03e8a88e8081cd44959ea277a3af9a8"
[[package]]
name = "app_dirs2"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3564c46eaa62f4a4e50c98e2c698cbb95f95c08501bd2e3baf6ea73773ab9fc"
dependencies = [
"jni",
"ndk-context",
"winapi 0.3.9",
"xdg",
]
[[package]]
name = "approx"
version = "0.3.2"
@ -1567,6 +1585,31 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
[[package]]
name = "discord-sdk"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e314b59ba110cb4fad80073f5276eb62cda4cc3caabe998f5d664b19fd6a7be0"
dependencies = [
"anyhow",
"app_dirs2",
"async-trait",
"base64",
"bitflags",
"crossbeam-channel",
"num-traits",
"parking_lot 0.12.0",
"serde",
"serde_json",
"serde_repr",
"thiserror",
"time 0.3.9",
"tokio",
"tracing",
"url",
"winreg",
]
[[package]]
name = "dispatch"
version = "0.1.4"
@ -6806,6 +6849,7 @@ dependencies = [
"crossbeam-channel",
"crossbeam-utils 0.8.8",
"directories-next",
"discord-sdk",
"dispatch 0.1.4",
"dot_vox",
"egui",
@ -7797,6 +7841,15 @@ dependencies = [
"x11-dl",
]
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "winres"
version = "0.1.12"

View File

@ -11,6 +11,7 @@ common-controls = Controls
common-video = Graphics
common-sound = Sound
common-chat = Chat
common-networking = Networking
common-resume = Resume
common-characters = Characters
common-close = Close
@ -40,6 +41,7 @@ common-video_settings = Graphics Settings
common-sound_settings = Sound Settings
common-language_settings = Language Settings
common-chat_settings = Chat Settings
common-networking_settings = Networking Settings
common-connection_lost =
Connection lost!
Did the server restart?
@ -105,4 +107,4 @@ common-material-wood = Wood
common-material-stone = Stone
common-material-cloth = Cloth
common-material-hide = Hide
common-sprite-chest = Chest
common-sprite-chest = Chest

View File

@ -132,4 +132,6 @@ hud-settings-region = Region
hud-settings-say = Say
hud-settings-all = All
hud-settings-group_only = Group only
hud-settings-reset_chat = Reset to Defaults
hud-settings-reset_chat = Reset to Defaults
hud-settings-third_party_integrations = Third-party Integrations
hud-settings-enable_discord_integration = Enable Discord Integration

View File

@ -201,7 +201,7 @@ impl Default for Settings {
metrics_address: SocketAddr::from((Ipv4Addr::LOCALHOST, 14005)),
auth_server_address: Some("https://auth.veloren.net".into()),
world_seed: DEFAULT_WORLD_SEED,
server_name: "Veloren Alpha".into(),
server_name: "Veloren Server".into(),
max_players: 100,
start_time: 9.0 * 3600.0,
map_file: None,

View File

@ -32,9 +32,10 @@ tracy-memory = ["tracy"] # enables heap profiling with tracy
plugins = ["client/plugins"]
egui-ui = ["voxygen-egui", "egui", "egui_wgpu_backend", "egui_winit_platform"]
shaderc-from-source = ["shaderc/build-from-source"]
discord = ["discord-sdk"]
# We don't ship egui with published release builds so a separate feature is required that excludes it.
default-publish = ["singleplayer", "native-dialog", "plugins", "simd"]
default-publish = ["singleplayer", "native-dialog", "plugins", "simd", "discord"]
# Temp for bug on current wgpu version that has access violation in vulkan when constructing egui pipeline
default-no-egui = ["default-publish", "hot-reloading", "shaderc-from-source"]
default = ["default-no-egui", "egui-ui"]
@ -131,6 +132,9 @@ itertools = "0.10.0"
tracing = "0.1"
profiling = { version = "1.0.6", default-features = false, optional = true }
# Discord RPC
discord-sdk = { version = "0.3.0", optional = true }
[target.'cfg(target_os = "macos")'.dependencies]
dispatch = "0.1.4"

304
voxygen/src/discord.rs Normal file
View File

@ -0,0 +1,304 @@
use std::time::{Duration, SystemTime};
use common::terrain::SiteKindMeta;
use discord_sdk::{
self as ds, activity,
activity::{ActivityArgs, ActivityBuilder},
};
use tokio::{
sync::mpsc::{unbounded_channel, UnboundedSender},
time::{interval, MissedTickBehavior},
};
use tracing::{debug, info, warn};
/// Discord app id
///
/// **Note:** currently a private app created for testing purposes, can be
/// shared to a team or replaced entirely later on
const DISCORD_APP_ID: ds::AppId = 1006661232465563698;
/// Discord presence update command
#[derive(Debug, Clone)]
pub enum ActivityUpdate {
/// Clear the current Discord activity and exit the activity task
Clear,
/// Set the activity to "In Main Menu"
MainMenu,
/// Set the activity to "In Character Selection"
CharacterSelection,
/// Set the activity to "Playing Singleplayer"
JoinSingleplayer,
/// Set the activity to "Playing Multiplayer"
JoinServer(String),
/// Set the large asset text to the location name
NewLocation {
chunk_name: String,
site: SiteKindMeta,
},
}
impl ActivityUpdate {
/// Rich Presence asset keys: the backgrounds used in the main menu and
/// loading screen
///
/// TODO: randomize images? use them according to the current biome?
const ASSETS: [&'static str; 15] = [
"bg_main", "bg_1", "bg_2", "bg_3", "bg_4", "bg_5", "bg_6", "bg_7", "bg_8", "bg_9", "bg_10",
"bg_11", "bg_12", "bg_13", "bg_14",
];
/// Rich Presence character screen asset key
const CHARACTER_SCREEN_ASSET: &'static str = "character_screen";
/// Rich Presence logo asset key
const LOGO_ASSET: &'static str = "logo";
/// Edit the current activity args according to the command in `self`.
///
/// - For `MainMenu`, `CharacterSelection`, `JoinSingleplayer` and
/// `JoinServer(name)`: create a new activity and discard the previous one
/// - For `NewLocation` and `LevelUp`: update the current activity
fn edit_activity(self, args: &mut ActivityArgs) {
use ActivityUpdate::*;
match self {
Clear => (),
MainMenu => {
*args = ActivityBuilder::default()
.start_timestamp(SystemTime::now())
.state("Idle")
.details("In Main Menu")
.assets(
activity::Assets::default().large(Self::LOGO_ASSET, Option::<&str>::None),
)
.into();
},
CharacterSelection => {
*args = ActivityBuilder::default()
.start_timestamp(SystemTime::now())
.state("Idle")
.details("In Character Selection")
.assets(
activity::Assets::default()
.large(Self::CHARACTER_SCREEN_ASSET, Option::<&str>::None)
.small(Self::LOGO_ASSET, Option::<&str>::None),
)
.into();
},
JoinSingleplayer => {
*args = ActivityBuilder::default()
.start_timestamp(SystemTime::now())
.details("Playing Singleplayer")
.assets(
activity::Assets::default()
.large(Self::ASSETS[9], Option::<&str>::None)
.small(Self::LOGO_ASSET, Option::<&str>::None),
)
.into();
},
JoinServer(server_name) => {
*args = ActivityBuilder::default()
.start_timestamp(SystemTime::now())
.state(format!("On {server_name}"))
.details("Playing Multiplayer")
.assets(
activity::Assets::default()
.large(Self::ASSETS[1], Option::<&str>::None)
.small(Self::LOGO_ASSET, Option::<&str>::None),
)
.into();
},
NewLocation { chunk_name, site } => {
use common::terrain::site::{
DungeonKindMeta::*, SettlementKindMeta::*, SiteKindMeta::*,
};
let location = match site {
Dungeon(Old) => format!("Battling evil in {chunk_name}"),
Dungeon(Gnarling) => format!("Hunting Gnarlings in {chunk_name}"),
Cave => "In a Cave".to_string(),
Settlement(Default) => format!("Visiting {chunk_name}"),
Settlement(Cliff) => format!("Climbing the towers of {chunk_name}"),
Settlement(Desert) => format!("Hiding from the sun in {chunk_name}"),
_ => format!("In {chunk_name}"),
};
args.activity.as_mut().map(|a| {
a.assets.as_mut().map(|assets| {
assets.large_text = Some(location);
})
});
},
}
}
}
/// A channel to the background task that updates the Discord activity.
pub enum Discord {
/// Active state, receiving updates
Active {
/// The channel to communicate with the tokio task
channel: UnboundedSender<ActivityUpdate>,
/// Current chunk name, cached to check for updates
current_chunk_name: Option<String>,
/// Current site, cached to check for updates
current_site: SiteKindMeta,
},
/// Inactive state: either the Discord app could not be contacted, is not
/// installed, or was disconnected
Inactive,
}
impl Discord {
/// Start a background [tokio task](tokio::task) that will update the
/// Discord activity every 4 seconds (due to rate limits) if it has
/// changed.
///
/// The [`update`](Discord::update) method can be used on the returned
/// struct to update the Discord activity via a channel command
pub fn start(rt: &tokio::runtime::Runtime) -> Self {
let (sender, mut receiver) = unbounded_channel::<ActivityUpdate>();
rt.spawn(async move {
let (wheel, handler) = ds::wheel::Wheel::new(Box::new(|err| {
warn!(error = ?err, "Encountered an error while connecting to Discord");
}));
let mut user = wheel.user();
let discord = match ds::Discord::new(
ds::DiscordApp::PlainId(DISCORD_APP_ID),
ds::Subscriptions::ACTIVITY,
Box::new(handler),
) {
Ok(ds) => {
if let Err(err) = user.0.changed().await {
warn!(err = ?err, "Could not execute handshake to Discord");
// If no handshake is received, exit the task immediately
return;
}
info!("Connected to Discord");
ds
},
Err(err) => {
info!(err = ?err, "Could not connect to Discord app");
// If no Discord app was found, exit the task immediately
return;
},
};
let mut args = ActivityArgs::default();
let mut has_changed = false;
let mut interval = interval(Duration::from_secs(4));
interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
loop {
// Check every four seconds if the activity needs to change
tokio::select! {
biased; // to save the CPU cost of selecting a random branch
_ = interval.tick(), if has_changed => {
has_changed = false;
let activity = args.activity.clone();
match discord.update_activity(args).await {
Err(err) => {
warn!(error = ?err, "Could not update Discord activity");
}
Ok(Some(new_activity)) => {
debug!(new_activity = ?new_activity, "Updated Discord activity");
},
Ok(None) => ()
}
args = ActivityArgs::default();
args.activity = activity;
}
update = receiver.recv() => match update {
None | Some(ActivityUpdate::Clear) => {
match discord.clear_activity().await {
Ok(_) => {
info!("Cleared Discord activity");
},
Err(err) => {
warn!(error = ?err, "Failed to clear Discord activity")
}
}
return;
},
Some(update) => {
update.edit_activity(&mut args);
has_changed = true;
},
}
}
}
});
Self::Active {
channel: sender,
current_chunk_name: None,
current_site: SiteKindMeta::Void,
}
}
/// Send an activity update to the background task
#[inline]
fn update(&mut self, update: ActivityUpdate) {
if let Self::Active { channel, .. } = self {
// On error, turn itself into inactive to avoid sending unecessary updates
if channel.send(update).is_err() {
*self = Self::Inactive;
}
}
}
/// Clear the Discord activity
#[inline]
pub fn clear_activity(&mut self) {
self.update(ActivityUpdate::Clear);
*self = Discord::Inactive;
}
/// Sets the current Discord activity to Main Menu
#[inline]
pub fn enter_main_menu(&mut self) { self.update(ActivityUpdate::MainMenu); }
/// Sets the current Discord activity to Character Selection
#[inline]
pub fn enter_character_selection(&mut self) { self.update(ActivityUpdate::CharacterSelection); }
/// Sets the current Discord activity to Singleplayer
#[inline]
pub fn join_singleplayer(&mut self) { self.update(ActivityUpdate::JoinSingleplayer); }
/// Sets the current Discord activity to Multiplayer with the corresponding
/// server name
#[inline]
pub fn join_server(&mut self, server_name: String) {
self.update(ActivityUpdate::JoinServer(server_name));
}
/// Check the current location name and update it if it has changed
#[inline]
pub fn update_location(&mut self, chunk_name: &str, site: SiteKindMeta) {
if let Self::Active {
current_chunk_name,
current_site,
..
} = self
{
let different_name = current_chunk_name.as_deref() != Some(chunk_name);
if different_name || *current_site != site {
if different_name {
*current_chunk_name = Some(chunk_name.to_string());
}
*current_site = site;
self.update(ActivityUpdate::NewLocation {
chunk_name: chunk_name.to_string(),
site,
});
}
}
}
/// Check wether the Discord activity is active and receiving updates
#[inline]
pub fn is_active(&self) -> bool { matches!(self, Self::Active { .. }) }
}

View File

@ -44,8 +44,6 @@ widget_ids! {
auto_walk_behavior_list,
camera_clamp_behavior_text,
camera_clamp_behavior_list,
player_physics_behavior_text,
player_physics_behavior_list,
stop_auto_walk_on_input_button,
stop_auto_walk_on_input_label,
auto_camera_button,
@ -440,43 +438,6 @@ impl<'a> Widget for Gameplay<'a> {
}
}
// Player physics behavior
Text::new(
&self
.localized_strings
.get("hud.settings.player_physics_behavior"),
)
.down_from(state.ids.auto_walk_behavior_list, 10.0)
.right_from(state.ids.camera_clamp_behavior_text, 118.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.player_physics_behavior_text, ui);
let player_physics_selected =
self.global_state.settings.gameplay.player_physics_behavior as usize;
if let Some(clicked) = DropDownList::new(
&["Client-authoritative", "Server-authoritative"],
Some(player_physics_selected),
)
.w_h(200.0, 30.0)
.color(MENU_BG)
.label_color(TEXT_COLOR)
.label_font_id(self.fonts.cyri.conrod_id)
.down_from(state.ids.player_physics_behavior_text, 8.0)
.set(state.ids.player_physics_behavior_list, ui)
{
match clicked {
0 => events.push(ChangePlayerPhysicsBehavior {
server_authoritative: false,
}),
_ => events.push(ChangePlayerPhysicsBehavior {
server_authoritative: true,
}),
}
}
// Stop autowalk on input toggle
let stop_auto_walk_on_input_toggle = ToggleButton::new(
self.global_state.settings.gameplay.stop_auto_walk_on_input,

View File

@ -3,6 +3,7 @@ mod controls;
mod gameplay;
mod interface;
mod language;
mod networking;
mod sound;
mod video;
@ -39,6 +40,7 @@ widget_ids! {
sound,
language,
chat,
networking,
}
}
@ -54,6 +56,7 @@ pub enum SettingsTab {
Gameplay,
Controls,
Lang,
Networking,
}
impl SettingsTab {
fn name_key(&self) -> &str {
@ -65,6 +68,7 @@ impl SettingsTab {
SettingsTab::Video => "common.video",
SettingsTab::Sound => "common.sound",
SettingsTab::Lang => "common.languages",
SettingsTab::Networking => "common.networking",
}
}
@ -77,6 +81,7 @@ impl SettingsTab {
SettingsTab::Video => "common.video_settings",
SettingsTab::Sound => "common.sound_settings",
SettingsTab::Lang => "common.language_settings",
SettingsTab::Networking => "common.networking_settings",
}
}
}
@ -321,6 +326,16 @@ impl<'a> Widget for SettingsWindow<'a> {
events.push(Event::SettingsChange(change.into()));
}
},
SettingsTab::Networking => {
for change in
networking::Networking::new(global_state, imgs, fonts, localized_strings)
.top_left_with_margins_on(state.ids.settings_content_align, 0.0, 0.0)
.wh_of(state.ids.settings_content_align)
.set(state.ids.networking, ui)
{
events.push(Event::SettingsChange(change.into()));
}
},
}
events

View File

@ -0,0 +1,263 @@
use crate::{
hud::{img_ids::Imgs, MENU_BG, TEXT_COLOR},
session::settings_change::{Networking as NetworkingChange, Networking::*},
ui::{fonts::Fonts, ImageSlider, ToggleButton},
GlobalState,
};
use conrod_core::{
color,
widget::{self, DropDownList, Rectangle, Text},
widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
};
use i18n::Localization;
widget_ids! {
struct Ids {
window,
window_r,
vd_text,
vd_slider,
vd_value,
player_physics_behavior_text,
player_physics_behavior_list,
lossy_terrain_compression_button,
lossy_terrain_compression_label,
third_party_integrations_title,
enable_discord_integration_text,
enable_discord_integration_button
}
}
#[derive(WidgetCommon)]
pub struct Networking<'a> {
global_state: &'a GlobalState,
imgs: &'a Imgs,
fonts: &'a Fonts,
localized_strings: &'a Localization,
#[conrod(common_builder)]
common: widget::CommonBuilder,
}
impl<'a> Networking<'a> {
pub fn new(
global_state: &'a GlobalState,
imgs: &'a Imgs,
fonts: &'a Fonts,
localized_strings: &'a Localization,
) -> Self {
Self {
global_state,
imgs,
fonts,
localized_strings,
common: widget::CommonBuilder::default(),
}
}
}
pub struct State {
ids: Ids,
}
impl<'a> Widget for Networking<'a> {
type Event = Vec<NetworkingChange>;
type State = State;
type Style = ();
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
State {
ids: Ids::new(id_gen),
}
}
fn style(&self) -> Self::Style {}
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
common_base::prof_span!("Networking::update");
let widget::UpdateArgs { state, ui, .. } = args;
let mut events = Vec::new();
Rectangle::fill_with(args.rect.dim(), color::TRANSPARENT)
.xy(args.rect.xy())
.graphics_for(args.id)
.scroll_kids()
.scroll_kids_vertically()
.set(state.ids.window, ui);
Rectangle::fill_with([args.rect.w() / 2.0, args.rect.h()], color::TRANSPARENT)
.top_right()
.parent(state.ids.window)
.set(state.ids.window_r, ui);
// View Distance
Text::new(&self.localized_strings.get("hud.settings.view_distance"))
.top_left_with_margins_on(state.ids.window, 10.0, 10.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.vd_text, ui);
if let Some(new_val) = ImageSlider::discrete(
self.global_state.settings.graphics.view_distance,
1,
65,
self.imgs.slider_indicator,
self.imgs.slider,
)
.w_h(104.0, 22.0)
.down_from(state.ids.vd_text, 8.0)
.track_breadth(12.0)
.slider_length(10.0)
.pad_track((5.0, 5.0))
.set(state.ids.vd_slider, ui)
{
events.push(NetworkingChange::AdjustViewDistance(new_val));
}
Text::new(&format!(
"{}",
self.global_state.settings.graphics.view_distance
))
.right_from(state.ids.vd_slider, 8.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.vd_value, ui);
// Player physics behavior
Text::new(
&self
.localized_strings
.get("hud.settings.player_physics_behavior"),
)
.down_from(state.ids.vd_slider, 8.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.player_physics_behavior_text, ui);
let player_physics_selected = self
.global_state
.settings
.networking
.player_physics_behavior as usize;
if let Some(clicked) = DropDownList::new(
&["Client-authoritative", "Server-authoritative"],
Some(player_physics_selected),
)
.w_h(200.0, 30.0)
.color(MENU_BG)
.label_color(TEXT_COLOR)
.label_font_id(self.fonts.cyri.conrod_id)
.down_from(state.ids.player_physics_behavior_text, 8.0)
.set(state.ids.player_physics_behavior_list, ui)
{
match clicked {
0 => events.push(ChangePlayerPhysicsBehavior {
server_authoritative: false,
}),
_ => events.push(ChangePlayerPhysicsBehavior {
server_authoritative: true,
}),
}
}
// Lossy terrain compression
Text::new(
&self
.localized_strings
.get("hud.settings.lossy_terrain_compression"),
)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.right_from(state.ids.player_physics_behavior_text, 64.0)
.color(TEXT_COLOR)
.set(state.ids.lossy_terrain_compression_label, ui);
let lossy_terrain_compression = ToggleButton::new(
self.global_state
.settings
.networking
.lossy_terrain_compression,
self.imgs.checkbox,
self.imgs.checkbox_checked,
)
.w_h(18.0, 18.0)
.right_from(state.ids.lossy_terrain_compression_label, 10.0)
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
.set(state.ids.lossy_terrain_compression_button, ui);
if self
.global_state
.settings
.networking
.lossy_terrain_compression
!= lossy_terrain_compression
{
events.push(NetworkingChange::ToggleLossyTerrainCompression(
lossy_terrain_compression,
));
}
#[cfg(feature = "discord")]
{
// Third party integrations
Text::new(
&self
.localized_strings
.get("hud.settings.third_party_integrations"),
)
.down_from(state.ids.player_physics_behavior_list, 16.0)
.font_size(self.fonts.cyri.scale(18))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.third_party_integrations_title, ui);
// Toggle Discord integration
let enable_discord_integration = ToggleButton::new(
self.global_state
.settings
.networking
.enable_discord_integration,
self.imgs.checkbox,
self.imgs.checkbox_checked,
)
.w_h(18.0, 18.0)
.down_from(state.ids.third_party_integrations_title, 8.0)
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
.set(state.ids.enable_discord_integration_button, ui);
if self
.global_state
.settings
.networking
.enable_discord_integration
!= enable_discord_integration
{
events.push(ToggleDiscordIntegration(
!self
.global_state
.settings
.networking
.enable_discord_integration,
));
}
Text::new(
&self
.localized_strings
.get("hud.settings.enable_discord_integration"),
)
.right_from(state.ids.enable_discord_integration_button, 10.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.graphics_for(state.ids.enable_discord_integration_button)
.color(TEXT_COLOR)
.set(state.ids.enable_discord_integration_text, ui);
}
events
}
}

View File

@ -102,8 +102,6 @@ widget_ids! {
//
particles_button,
particles_label,
lossy_terrain_compression_button,
lossy_terrain_compression_label,
weapon_trails_button,
weapon_trails_label,
flashing_lights_button,
@ -1219,49 +1217,11 @@ impl<'a> Widget for Video<'a> {
events.push(GraphicsChange::ToggleParticlesEnabled(particles_enabled));
}
// Lossy terrain compression
Text::new(
&self
.localized_strings
.get("hud.settings.lossy_terrain_compression"),
)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.right_from(state.ids.particles_label, 64.0)
.color(TEXT_COLOR)
.set(state.ids.lossy_terrain_compression_label, ui);
let lossy_terrain_compression = ToggleButton::new(
self.global_state
.settings
.graphics
.lossy_terrain_compression,
self.imgs.checkbox,
self.imgs.checkbox_checked,
)
.w_h(18.0, 18.0)
.right_from(state.ids.lossy_terrain_compression_label, 10.0)
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
.set(state.ids.lossy_terrain_compression_button, ui);
if self
.global_state
.settings
.graphics
.lossy_terrain_compression
!= lossy_terrain_compression
{
events.push(GraphicsChange::ToggleLossyTerrainCompression(
lossy_terrain_compression,
));
}
// Weapon trails
Text::new(&self.localized_strings.get("hud.settings.weapon_trails"))
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.right_from(state.ids.lossy_terrain_compression_label, 64.0)
.right_from(state.ids.particles_label, 64.0)
.color(TEXT_COLOR)
.set(state.ids.weapon_trails_label, ui);

View File

@ -22,6 +22,7 @@ pub mod audio;
pub mod cmd;
pub mod controller;
mod credits;
#[cfg(feature = "discord")] pub mod discord;
mod ecs;
pub mod error;
pub mod game_input;
@ -83,6 +84,9 @@ pub struct GlobalState {
pub client_error: Option<String>,
// Used to clear the shadow textures when entering a PlayState that doesn't utilise shadows
pub clear_shadows_next_frame: bool,
/// A channel that sends Discord activity updates to a background task
#[cfg(feature = "discord")]
pub discord: crate::discord::Discord,
}
impl GlobalState {

View File

@ -285,6 +285,13 @@ fn main() {
#[cfg(feature = "egui-ui")]
let egui_state = EguiState::new(&window);
#[cfg(feature = "discord")]
let discord = if settings.networking.enable_discord_integration {
veloren_voxygen::discord::Discord::start(&tokio_runtime)
} else {
veloren_voxygen::discord::Discord::Inactive
};
let global_state = GlobalState {
userdata_dir,
config_dir,
@ -306,6 +313,8 @@ fn main() {
clipboard,
client_error: None,
clear_shadows_next_frame: false,
#[cfg(feature = "discord")]
discord,
};
run::run(global_state, event_loop);

View File

@ -74,6 +74,9 @@ impl PlayState for CharSelectionState {
// Clear shadow textures since we don't render to them here
global_state.clear_shadows_next_frame = true;
#[cfg(feature = "discord")]
global_state.discord.enter_character_selection();
}
fn tick(&mut self, global_state: &mut GlobalState, events: Vec<WinEvent>) -> PlayStateResult {

View File

@ -82,6 +82,9 @@ impl PlayState for MainMenuState {
// Set scale mode in case it was change
self.main_menu_ui
.set_scale_mode(global_state.settings.interface.ui_scale);
#[cfg(feature = "discord")]
global_state.discord.enter_main_menu();
}
#[allow(clippy::single_match)] // TODO: remove when event match has multiple arms

View File

@ -151,6 +151,10 @@ fn handle_main_events_cleared(
break;
},
PlayStateResult::Shutdown => {
// Clear the Discord activity before shutting down
#[cfg(feature = "discord")]
global_state.discord.clear_activity();
debug!("Shutting down all states...");
while states.last().is_some() {
states.pop().map(|old_state| {

View File

@ -113,9 +113,9 @@ impl SessionState {
let mut mumble_link = SharedLink::new("veloren", "veloren-voxygen");
{
let mut client = client.borrow_mut();
client.request_player_physics(global_state.settings.gameplay.player_physics_behavior);
client.request_player_physics(global_state.settings.networking.player_physics_behavior);
client.request_lossy_terrain_compression(
global_state.settings.graphics.lossy_terrain_compression,
global_state.settings.networking.lossy_terrain_compression,
);
#[cfg(not(target_os = "macos"))]
if let Some(uid) = client.uid() {
@ -408,6 +408,23 @@ impl PlayState for SessionState {
self.client.borrow_mut().send_chat(cmd.to_string());
}
}
#[cfg(feature = "discord")]
{
// Update the Discord activity on client initialization
#[cfg(feature = "singleplayer")]
let singleplayer = global_state.singleplayer.is_some();
#[cfg(not(feature = "singleplayer"))]
let singleplayer = false;
if singleplayer {
global_state.discord.join_singleplayer();
} else {
global_state
.discord
.join_server(self.client.borrow().server_info().name.clone());
}
}
}
fn tick(&mut self, global_state: &mut GlobalState, events: Vec<Event>) -> PlayStateResult {
@ -434,6 +451,17 @@ impl PlayState for SessionState {
let client = self.client.borrow();
let player_entity = client.entity();
#[cfg(feature = "discord")]
if global_state.discord.is_active() {
if let Some(chunk) = client.current_chunk() {
if let Some(location_name) = chunk.meta().name() {
global_state
.discord
.update_location(location_name, client.current_site());
}
}
}
if global_state.settings.gameplay.bow_zoom {
let mut fov_scaling = 1.0;
if let Some(comp::CharacterState::ChargedRanged(cr)) = client

View File

@ -62,7 +62,6 @@ pub enum Gameplay {
ChangeFreeLookBehavior(PressBehavior),
ChangeAutoWalkBehavior(PressBehavior),
ChangeCameraClampBehavior(PressBehavior),
ChangePlayerPhysicsBehavior { server_authoritative: bool },
ChangeStopAutoWalkOnInput(bool),
ChangeAutoCamera(bool),
ChangeBowZoom(bool),
@ -89,7 +88,6 @@ pub enum Graphics {
ChangeFullscreenMode(FullScreenSettings),
ToggleParticlesEnabled(bool),
ToggleLossyTerrainCompression(bool),
ToggleWeaponTrailsEnabled(bool),
AdjustWindowSize([u16; 2]),
@ -148,7 +146,16 @@ pub enum Language {
ToggleEnglishFallback(bool),
}
#[derive(Clone)]
pub enum Networking {}
pub enum Networking {
AdjustViewDistance(u32),
ChangePlayerPhysicsBehavior {
server_authoritative: bool,
},
ToggleLossyTerrainCompression(bool),
#[cfg(feature = "discord")]
ToggleDiscordIntegration(bool),
}
#[derive(Clone)]
pub enum SettingsChange {
@ -319,15 +326,6 @@ impl SettingsChange {
Gameplay::ChangeCameraClampBehavior(behavior) => {
settings.gameplay.camera_clamp_behavior = behavior;
},
Gameplay::ChangePlayerPhysicsBehavior {
server_authoritative,
} => {
settings.gameplay.player_physics_behavior = server_authoritative;
session_state
.client
.borrow_mut()
.request_player_physics(server_authoritative);
},
Gameplay::ChangeStopAutoWalkOnInput(state) => {
settings.gameplay.stop_auto_walk_on_input = state;
},
@ -424,13 +422,6 @@ impl SettingsChange {
Graphics::ToggleParticlesEnabled(particles_enabled) => {
settings.graphics.particles_enabled = particles_enabled;
},
Graphics::ToggleLossyTerrainCompression(lossy_terrain_compression) => {
settings.graphics.lossy_terrain_compression = lossy_terrain_compression;
session_state
.client
.borrow_mut()
.request_lossy_terrain_compression(lossy_terrain_compression);
},
Graphics::ToggleWeaponTrailsEnabled(weapon_trails_enabled) => {
settings.graphics.weapon_trails_enabled = weapon_trails_enabled;
},
@ -612,7 +603,56 @@ impl SettingsChange {
.set_english_fallback(settings.language.use_english_fallback);
},
},
SettingsChange::Networking(networking_change) => match networking_change {},
SettingsChange::Networking(networking_change) => match networking_change {
Networking::AdjustViewDistance(view_distance) => {
session_state
.client
.borrow_mut()
.set_view_distance(view_distance);
settings.graphics.view_distance = view_distance;
},
Networking::ChangePlayerPhysicsBehavior {
server_authoritative,
} => {
settings.networking.player_physics_behavior = server_authoritative;
session_state
.client
.borrow_mut()
.request_player_physics(server_authoritative);
},
Networking::ToggleLossyTerrainCompression(lossy_terrain_compression) => {
settings.networking.lossy_terrain_compression = lossy_terrain_compression;
session_state
.client
.borrow_mut()
.request_lossy_terrain_compression(lossy_terrain_compression);
},
#[cfg(feature = "discord")]
Networking::ToggleDiscordIntegration(enabled) => {
use crate::discord::Discord;
settings.networking.enable_discord_integration = enabled;
if enabled {
global_state.discord = Discord::start(&global_state.tokio_runtime);
#[cfg(feature = "singleplayer")]
let singleplayer = global_state.singleplayer.is_some();
#[cfg(not(feature = "singleplayer"))]
let singleplayer = false;
if singleplayer {
global_state.discord.join_singleplayer();
} else {
global_state.discord.join_server(
session_state.client.borrow().server_info().name.clone(),
);
}
} else {
global_state.discord.clear_activity();
global_state.discord = Discord::Inactive;
}
},
},
}
settings.save_to_file_warn(&global_state.config_dir);
}

View File

@ -14,7 +14,6 @@ pub struct GameplaySettings {
pub free_look_behavior: PressBehavior,
pub auto_walk_behavior: PressBehavior,
pub camera_clamp_behavior: PressBehavior,
pub player_physics_behavior: bool,
pub stop_auto_walk_on_input: bool,
pub auto_camera: bool,
pub bow_zoom: bool,
@ -32,7 +31,6 @@ impl Default for GameplaySettings {
free_look_behavior: PressBehavior::Toggle,
auto_walk_behavior: PressBehavior::Toggle,
camera_clamp_behavior: PressBehavior::Toggle,
player_physics_behavior: false,
stop_auto_walk_on_input: true,
auto_camera: false,
bow_zoom: true,

View File

@ -33,7 +33,6 @@ pub struct GraphicsSettings {
pub lod_distance: u32,
pub sprite_render_distance: u32,
pub particles_enabled: bool,
pub lossy_terrain_compression: bool,
pub weapon_trails_enabled: bool,
pub figure_lod_render_distance: u32,
pub max_fps: Fps,
@ -55,7 +54,6 @@ impl Default for GraphicsSettings {
lod_distance: 200,
sprite_render_distance: 100,
particles_enabled: true,
lossy_terrain_compression: false,
weapon_trails_enabled: true,
figure_lod_render_distance: 300,
max_fps: Fps::Max(60),

View File

@ -10,6 +10,9 @@ pub struct NetworkingSettings {
pub default_server: String,
pub trusted_auth_servers: HashSet<String>,
pub use_quic: bool,
pub player_physics_behavior: bool,
pub lossy_terrain_compression: bool,
pub enable_discord_integration: bool,
}
impl Default for NetworkingSettings {
@ -23,6 +26,9 @@ impl Default for NetworkingSettings {
.map(|s| s.to_string())
.collect(),
use_quic: false,
player_physics_behavior: false,
lossy_terrain_compression: false,
enable_discord_integration: true,
}
}
}