diff --git a/CHANGELOG.md b/CHANGELOG.md index ab55163cb4..bad552cbf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Setting for disabling flashing lights +- Spectate mode for moderators. - Currently playing music track and artist now shows in the debug menu. - Added a setting to influence the gap between music track plays. - Added a Craft All button. - Server: Vacuum database on startup -- SeaChapel, greek/latin inspired dungeon for ocean biome coasts +- SeaChapel, greek/latin inspired dungeon for ocean biome coasts +- Entity view distance setting added (shown in graphics and network tabs). This setting controls + the distance at which entities are synced to the client and which entities are displayed in. + This is clamped to be no more than the current overall view distance setting. +- View distance settings that are lowered by the server limit (or other factors) now display an + extra ghost slider cursor when set above the limit (instead of snapping back to the limit). + Limits on the view distance by the server no longer affect the settings saved on the client. ### Changed - Use fluent for translations @@ -21,15 +29,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - /kill_npcs no longer leaves drops behind and also has bug causing it to not destroy entities fixed. - Default present mode changed to Fifo (aka 'Vsync capped'). +- Old "Entity View Distance" setting renamed to "Entity Detail Distance" (since this controls the + distance at which lower detail models are used for entities). +- Present mode options renamed for clarity: Fifo -> 'Vsync capped', Mailbox -> 'Vsync uncapped', + Immediate -> 'Vsync off'. ### Removed ### Fixed +- Fixed npc not handling interactions while fighting (especially merchants in trade) +- Fixed bug where you would still be burning after dying in lava. +- Workaround for rayon bug that caused lag spikes in slowjobs - Fixed crash due to zooming out very far - Client properly knows trade was cancelled when exiting to the character screen (and no longer tries to display the trade window when rejoining) - Cancel trades for an entity when it is deleted (note this doesn't effect trades between players since their entities are not removed). +- Fixed bug where the view distance selection was not immediately applied to entity syncing when + first joining a server and when changing the view distance (previously this required moving to a + new chunk for the initial setting or subsequent change to apply). ## [0.13.0] - 2022-07-23 @@ -68,8 +86,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - More varied ambient birdcalls - Cave biomes - Updated the Polish translation -- Setting for disabling flashing lights -- Spectate mode for moderators. ### Changed @@ -113,9 +129,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Combat music now loops and ends properly - Modular weapons now have a selling price - Closing a subwindow now only regrabs the cursor if no other subwindow requires it. -- Fixed npc not handling interactions while fighting (especially merchants in trade) -- Fixed bug where you would still be burning after dying in lava. -- Workaround for rayon bug that caused lag spikes in slowjobs ## [0.12.0] - 2022-02-19 diff --git a/assets/voxygen/i18n/en/hud/settings.ftl b/assets/voxygen/i18n/en/hud/settings.ftl index 97d5df9064..cb895bebe5 100644 --- a/assets/voxygen/i18n/en/hud/settings.ftl +++ b/assets/voxygen/i18n/en/hud/settings.ftl @@ -54,15 +54,16 @@ hud-settings-auto_camera = Auto camera hud-settings-bow_zoom = Zoom in when charging bow hud-settings-reset_gameplay = Reset to Defaults hud-settings-view_distance = View Distance +hud-settings-entity_view_distance = Entities View Distance hud-settings-lod_distance = LoD Distance hud-settings-sprites_view_distance = Sprites View Distance -hud-settings-figures_view_distance = Entities View Distance +hud-settings-entities_detail_distance = Entities Detail Distance hud-settings-maximum_fps = Maximum FPS hud-settings-background_fps = Background FPS hud-settings-present_mode = Present Mode -hud-settings-present_mode-fifo = Fifo -hud-settings-present_mode-mailbox = Mailbox -hud-settings-present_mode-immediate = Immediate +hud-settings-present_mode-vsync_capped = Vsync capped +hud-settings-present_mode-vsync_uncapped = Vsync uncapped +hud-settings-present_mode-vsync_off = Vsync off hud-settings-fov = Field of View (deg) hud-settings-gamma = Gamma hud-settings-exposure = Exposure diff --git a/client/src/bin/bot/main.rs b/client/src/bin/bot/main.rs index d4529199f2..ce4bacff73 100644 --- a/client/src/bin/bot/main.rs +++ b/client/src/bin/bot/main.rs @@ -221,7 +221,10 @@ impl BotClient { let c = list.characters.get(0).unwrap(); if let Some(id) = c.character.id { - client.request_character(id); + client.request_character(id, common::ViewDistances { + terrain: 5, + entity: 5, + }); } } info!("ingame done"); diff --git a/client/src/bin/swarm/main.rs b/client/src/bin/swarm/main.rs index c776f8186f..90e345f6ad 100644 --- a/client/src/bin/swarm/main.rs +++ b/client/src/bin/swarm/main.rs @@ -111,7 +111,6 @@ fn run_client( let mut client = runtime .block_on(Client::new(addr, runtime_clone, &mut None)) .expect("Failed to connect to the server"); - client.set_view_distance(opt.vd); // Login // NOTE: use a no-auth server @@ -159,6 +158,10 @@ fn run_client( .character .id .expect("Why is this an option?"), + common::ViewDistances { + terrain: opt.vd, + entity: opt.vd, + }, ); // If this is the admin client then adminify the other swarm members diff --git a/client/src/lib.rs b/client/src/lib.rs index 51cc93fd67..1138e9ab4f 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -81,6 +81,8 @@ use tokio::runtime::Runtime; use tracing::{debug, error, trace, warn}; use vek::*; +pub const MAX_SELECTABLE_VIEW_DISTANCE: u32 = 65; + const PING_ROLLING_AVERAGE_SECS: usize = 10; #[derive(Debug)] @@ -248,6 +250,8 @@ pub struct Client { tick: u64, state: State, + server_view_distance_limit: Option, + /// Terrrain view distance view_distance: Option, lod_distance: f32, // TODO: move into voxygen @@ -707,6 +711,7 @@ impl Client { tick: 0, state, + server_view_distance_limit: None, view_distance: None, lod_distance: 4.0, loaded_distance: 0.0, @@ -803,8 +808,8 @@ impl Client { | ClientGeneral::CreateCharacter { .. } | ClientGeneral::EditCharacter { .. } | ClientGeneral::DeleteCharacter(_) - | ClientGeneral::Character(_) - | ClientGeneral::Spectate => &mut self.character_screen_stream, + | ClientGeneral::Character(_, _) + | ClientGeneral::Spectate(_) => &mut self.character_screen_stream, //Only in game ClientGeneral::ControllerInputs(_) | ClientGeneral::ControlEvent(_) @@ -879,16 +884,22 @@ impl Client { } /// Request a state transition to `ClientState::Character`. - pub fn request_character(&mut self, character_id: CharacterId) { - self.send_msg(ClientGeneral::Character(character_id)); + pub fn request_character( + &mut self, + character_id: CharacterId, + view_distances: common::ViewDistances, + ) { + let view_distances = self.set_view_distances_local(view_distances); + self.send_msg(ClientGeneral::Character(character_id, view_distances)); // Assume we are in_game unless server tells us otherwise self.presence = Some(PresenceKind::Character(character_id)); } /// Request a state transition to `ClientState::Spectate`. - pub fn request_spectate(&mut self) { - self.send_msg(ClientGeneral::Spectate); + pub fn request_spectate(&mut self, view_distances: common::ViewDistances) { + let view_distances = self.set_view_distances_local(view_distances); + self.send_msg(ClientGeneral::Spectate(view_distances)); self.presence = Some(PresenceKind::Spectator); } @@ -942,10 +953,27 @@ impl Client { self.send_msg(ClientGeneral::ExitInGame); } - pub fn set_view_distance(&mut self, view_distance: u32) { - let view_distance = view_distance.max(1).min(65); - self.view_distance = Some(view_distance); - self.send_msg(ClientGeneral::SetViewDistance(view_distance)); + pub fn set_view_distances(&mut self, view_distances: common::ViewDistances) { + let view_distances = self.set_view_distances_local(view_distances); + self.send_msg(ClientGeneral::SetViewDistance(view_distances)); + } + + /// Clamps provided view distances, locally sets the terrain view distance + /// in the client's properties and returns the clamped values for the + /// caller to send to the server. + fn set_view_distances_local( + &mut self, + view_distances: common::ViewDistances, + ) -> common::ViewDistances { + let view_distances = common::ViewDistances { + terrain: view_distances + .terrain + .max(1) + .min(MAX_SELECTABLE_VIEW_DISTANCE), + entity: view_distances.entity.max(1), + }; + self.view_distance = Some(view_distances.terrain); + view_distances } pub fn set_lod_distance(&mut self, lod_distance: u32) { @@ -1486,6 +1514,8 @@ impl Client { pub fn view_distance(&self) -> Option { self.view_distance } + pub fn server_view_distance_limit(&self) -> Option { self.server_view_distance_limit } + pub fn loaded_distance(&self) -> f32 { self.loaded_distance } pub fn position(&self) -> Option> { @@ -2254,6 +2284,9 @@ impl Client { ServerGeneral::SetViewDistance(vd) => { self.view_distance = Some(vd); frontend_events.push(Event::SetViewDistance(vd)); + // If the server is correcting client vd selection we assume this is the max + // allowed view distance. + self.server_view_distance_limit = Some(vd); }, ServerGeneral::Outcomes(outcomes) => { frontend_events.extend(outcomes.into_iter().map(Event::Outcome)) @@ -2348,18 +2381,10 @@ impl Client { ServerGeneral::CharacterEdited(character_id) => { events.push(Event::CharacterEdited(character_id)); }, - ServerGeneral::CharacterSuccess => { - debug!("client is now in ingame state on server"); - if let Some(vd) = self.view_distance { - self.set_view_distance(vd); - } - }, + ServerGeneral::CharacterSuccess => debug!("client is now in ingame state on server"), ServerGeneral::SpectatorSuccess(spawn_point) => { - if let Some(vd) = self.view_distance { - events.push(Event::StartSpectate(spawn_point)); - debug!("client is now in ingame state on server"); - self.set_view_distance(vd); - } + events.push(Event::StartSpectate(spawn_point)); + debug!("client is now in ingame state on server"); }, _ => unreachable!("Not a character_screen msg"), } diff --git a/common/net/src/msg/client.rs b/common/net/src/msg/client.rs index f373593b5c..18ad9e52d5 100644 --- a/common/net/src/msg/client.rs +++ b/common/net/src/msg/client.rs @@ -4,6 +4,7 @@ use common::{ comp, comp::{Skill, SkillGroupKind}, terrain::block::Block, + ViewDistances, }; use serde::{Deserialize, Serialize}; use vek::*; @@ -60,13 +61,13 @@ pub enum ClientGeneral { alias: String, body: comp::Body, }, - Character(CharacterId), - Spectate, + Character(CharacterId, ViewDistances), + Spectate(ViewDistances), //Only in game ControllerInputs(Box), ControlEvent(comp::ControlEvent), ControlAction(comp::ControlAction), - SetViewDistance(u32), + SetViewDistance(ViewDistances), BreakBlock(Vec3), PlaceBlock(Vec3, Block), ExitInGame, @@ -121,7 +122,7 @@ impl ClientMsg { | ClientGeneral::DeleteCharacter(_) => { c_type != ClientType::ChatOnly && presence.is_none() }, - ClientGeneral::Character(_) | ClientGeneral::Spectate => { + ClientGeneral::Character(_, _) | ClientGeneral::Spectate(_) => { c_type == ClientType::Game && presence.is_none() }, //Only in game diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index 9fecc4361b..948f9a80a2 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -166,6 +166,10 @@ pub enum ServerGeneral { /// from an ingame state ExitInGameSuccess, InventoryUpdate(comp::Inventory, comp::InventoryUpdateEvent), + /// NOTE: The client can infer that entity view distance will be at most the + /// terrain view distance that we send here (and if lower it won't be + /// modified). So we just need to send the terrain VD back to the client + /// if corrections are made. SetViewDistance(u32), Outcomes(Vec), Knockback(Vec3), diff --git a/common/src/event.rs b/common/src/event.rs index d540b80b5d..4455484ace 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -107,8 +107,9 @@ pub enum ServerEvent { InitCharacterData { entity: EcsEntity, character_id: CharacterId, + requested_view_distances: crate::ViewDistances, }, - InitSpectator(EcsEntity), + InitSpectator(EcsEntity, crate::ViewDistances), UpdateCharacterData { entity: EcsEntity, components: ( diff --git a/common/src/lib.rs b/common/src/lib.rs index 88ae0bdefb..6cf1b69302 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -18,83 +18,82 @@ )] #![feature(hash_drain_filter)] -/// Re-exported crates -#[cfg(not(target_arch = "wasm32"))] -pub use uuid; +macro_rules! cfg_not_wasm { + ($($item:item)*) => { + $( + #[cfg(not(target_arch = "wasm32"))] + $item + )* + } +} + +// Re-exported crates +cfg_not_wasm! { + pub use common_assets as assets; + pub use uuid; +} + +// Modules -// modules -#[cfg(not(target_arch = "wasm32"))] -pub use common_assets as assets; -#[cfg(not(target_arch = "wasm32"))] pub mod astar; -#[cfg(not(target_arch = "wasm32"))] -mod cached_spatial_grid; -#[cfg(not(target_arch = "wasm32"))] -pub mod calendar; -#[cfg(not(target_arch = "wasm32"))] -pub mod character; -#[cfg(not(target_arch = "wasm32"))] pub mod clock; -#[cfg(not(target_arch = "wasm32"))] pub mod cmd; pub mod combat; pub mod comp; pub mod consts; -#[cfg(not(target_arch = "wasm32"))] pub mod depot; -#[cfg(not(target_arch = "wasm32"))] -pub mod effect; -#[cfg(not(target_arch = "wasm32"))] pub mod event; -#[cfg(not(target_arch = "wasm32"))] -pub mod explosion; -#[cfg(not(target_arch = "wasm32"))] -pub mod figure; -#[cfg(not(target_arch = "wasm32"))] -pub mod generation; -#[cfg(not(target_arch = "wasm32"))] pub mod grid; -#[cfg(not(target_arch = "wasm32"))] pub mod link; -#[cfg(not(target_arch = "wasm32"))] pub mod lod; -#[cfg(not(target_arch = "wasm32"))] -pub mod lottery; -#[cfg(not(target_arch = "wasm32"))] -pub mod mounting; -#[cfg(not(target_arch = "wasm32"))] pub mod npc; -#[cfg(not(target_arch = "wasm32"))] -pub mod outcome; -#[cfg(not(target_arch = "wasm32"))] pub mod path; -#[cfg(not(target_arch = "wasm32"))] pub mod ray; -#[cfg(not(target_arch = "wasm32"))] -pub mod recipe; -#[cfg(not(target_arch = "wasm32"))] -pub mod region; pub mod resources; -#[cfg(not(target_arch = "wasm32"))] pub mod rtsim; -#[cfg(not(target_arch = "wasm32"))] -pub mod skillset_builder; -#[cfg(not(target_arch = "wasm32"))] -pub mod slowjob; -#[cfg(not(target_arch = "wasm32"))] -pub mod spiral; -#[cfg(not(target_arch = "wasm32"))] -pub mod states; -#[cfg(not(target_arch = "wasm32"))] pub mod store; -#[cfg(not(target_arch = "wasm32"))] -pub mod terrain; -#[cfg(not(target_arch = "wasm32"))] pub mod time; -#[cfg(not(target_arch = "wasm32"))] pub mod trade; -#[cfg(not(target_arch = "wasm32"))] pub mod typed; pub mod uid; -#[cfg(not(target_arch = "wasm32"))] pub mod util; -#[cfg(not(target_arch = "wasm32"))] pub mod vol; -#[cfg(not(target_arch = "wasm32"))] -pub mod volumes; -#[cfg(not(target_arch = "wasm32"))] -pub mod weather; -#[cfg(not(target_arch = "wasm32"))] -pub use cached_spatial_grid::CachedSpatialGrid; -#[cfg(not(target_arch = "wasm32"))] -pub use combat::{Damage, GroupTarget, Knockback, KnockbackDir}; +// NOTE: Comment out macro to get rustfmt to re-order these as needed. +cfg_not_wasm! { + pub mod astar; + pub mod calendar; + pub mod character; + pub mod clock; + pub mod cmd; + pub mod depot; + pub mod effect; + pub mod event; + pub mod explosion; + pub mod figure; + pub mod generation; + pub mod grid; + pub mod link; + pub mod lod; + pub mod lottery; + pub mod mounting; + pub mod npc; + pub mod outcome; + pub mod path; + pub mod ray; + pub mod recipe; + pub mod region; + pub mod rtsim; + pub mod skillset_builder; + pub mod slowjob; + pub mod spiral; + pub mod states; + pub mod store; + pub mod terrain; + pub mod time; + pub mod trade; + pub mod util; + pub mod vol; + pub mod volumes; + pub mod weather; + + mod cached_spatial_grid; + mod view_distances; +} + +// We declare a macro in this module so there are issues referring to it by path +// within this crate if typed module is declared in macro expansion. +#[cfg(not(target_arch = "wasm32"))] pub mod typed; + pub use combat::{DamageKind, DamageSource}; -#[cfg(not(target_arch = "wasm32"))] -pub use comp::inventory::loadout_builder::LoadoutBuilder; -#[cfg(not(target_arch = "wasm32"))] -pub use explosion::{Explosion, RadiusEffect}; -#[cfg(not(target_arch = "wasm32"))] -pub use skillset_builder::SkillSetBuilder; + +cfg_not_wasm! { + pub use cached_spatial_grid::CachedSpatialGrid; + pub use combat::{Damage, GroupTarget, Knockback, KnockbackDir}; + pub use comp::inventory::loadout_builder::LoadoutBuilder; + pub use explosion::{Explosion, RadiusEffect}; + pub use skillset_builder::SkillSetBuilder; + pub use view_distances::ViewDistances; +} diff --git a/common/src/view_distances.rs b/common/src/view_distances.rs new file mode 100644 index 0000000000..484ab496c0 --- /dev/null +++ b/common/src/view_distances.rs @@ -0,0 +1,25 @@ +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] +pub struct ViewDistances { + pub terrain: u32, + /// Server will clamp this to `terrain` if it is larger. + /// + /// NOTE: Importantly, the server still loads entities in the `terrain` view + /// distance (at least currently, please update this if you change it!), + /// but the syncing to the client is done based on the entity view + /// distance. + pub entity: u32, +} + +impl ViewDistances { + /// Clamps the terrain view distance to an optional max and clamps the + /// entity view distance to the resulting terrain view distance. + /// + /// Also ensures both are at a minimum of 1 (unless the provided max is 0). + pub fn clamp(self, max: Option) -> Self { + let terrain = self.terrain.max(1).min(max.unwrap_or(u32::MAX)); + Self { + terrain, + entity: self.entity.max(1).min(terrain), + } + } +} diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 4afac192c5..3e0e9f390e 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -17,6 +17,7 @@ use common::{ rtsim::RtSimEntity, uid::Uid, util::Dir, + ViewDistances, }; use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use specs::{Builder, Entity as EcsEntity, WorldExt}; @@ -29,12 +30,29 @@ pub fn handle_initialize_character( server: &mut Server, entity: EcsEntity, character_id: CharacterId, + requested_view_distances: ViewDistances, ) { - server.state.initialize_character_data(entity, character_id); + let clamped_vds = requested_view_distances.clamp(server.settings().max_view_distance); + server + .state + .initialize_character_data(entity, character_id, clamped_vds); + // Correct client if its requested VD is too high. + if requested_view_distances.terrain != clamped_vds.terrain { + server.notify_client(entity, ServerGeneral::SetViewDistance(clamped_vds.terrain)); + } } -pub fn handle_initialize_spectator(server: &mut Server, entity: EcsEntity) { - server.state.initialize_spectator_data(entity); +pub fn handle_initialize_spectator( + server: &mut Server, + entity: EcsEntity, + requested_view_distances: ViewDistances, +) { + let clamped_vds = requested_view_distances.clamp(server.settings().max_view_distance); + server.state.initialize_spectator_data(entity, clamped_vds); + // Correct client if its requested VD is too high. + if requested_view_distances.terrain != clamped_vds.terrain { + server.notify_client(entity, ServerGeneral::SetViewDistance(clamped_vds.terrain)); + } sys::subscription::initialize_region_subscription(server.state.ecs(), entity); } diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index d8fb23f2d7..9b496c77b0 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -140,8 +140,16 @@ impl Server { ServerEvent::InitCharacterData { entity, character_id, - } => handle_initialize_character(self, entity, character_id), - ServerEvent::InitSpectator(entity) => handle_initialize_spectator(self, entity), + requested_view_distances, + } => handle_initialize_character( + self, + entity, + character_id, + requested_view_distances, + ), + ServerEvent::InitSpectator(entity, requested_view_distances) => { + handle_initialize_spectator(self, entity, requested_view_distances) + }, ServerEvent::UpdateCharacterData { entity, components } => { let ( body, diff --git a/server/src/presence.rs b/server/src/presence.rs index fea7730441..7db43d1cca 100644 --- a/server/src/presence.rs +++ b/server/src/presence.rs @@ -1,20 +1,24 @@ use common_net::msg::PresenceKind; use hashbrown::HashSet; use serde::{Deserialize, Serialize}; -use specs::{Component, DenseVecStorage, DerefFlaggedStorage, NullStorage, VecStorage}; +use specs::{Component, NullStorage}; +use std::time::{Duration, Instant}; use vek::*; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Debug)] pub struct Presence { - pub view_distance: u32, + pub terrain_view_distance: ViewDistance, + pub entity_view_distance: ViewDistance, pub kind: PresenceKind, pub lossy_terrain_compression: bool, } impl Presence { - pub fn new(view_distance: u32, kind: PresenceKind) -> Self { + pub fn new(view_distances: common::ViewDistances, kind: PresenceKind) -> Self { + let now = Instant::now(); Self { - view_distance, + terrain_view_distance: ViewDistance::new(view_distances.terrain, now), + entity_view_distance: ViewDistance::new(view_distances.entity, now), kind, lossy_terrain_compression: false, } @@ -22,8 +26,7 @@ impl Presence { } impl Component for Presence { - // Presence seems <= 64 bits, so it isn't worth using DenseVecStorage. - type Storage = DerefFlaggedStorage>; + type Storage = specs::DenseVecStorage; } // Distance from fuzzy_chunk before snapping to current chunk @@ -34,11 +37,12 @@ pub const REGION_FUZZ: u32 = 16; #[derive(Clone, Debug)] pub struct RegionSubscription { pub fuzzy_chunk: Vec2, + pub last_entity_view_distance: u32, pub regions: HashSet>, } impl Component for RegionSubscription { - type Storage = DerefFlaggedStorage>; + type Storage = specs::DenseVecStorage; } #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)] @@ -47,3 +51,88 @@ pub struct RepositionOnChunkLoad; impl Component for RepositionOnChunkLoad { type Storage = NullStorage; } + +#[derive(PartialEq, Debug, Clone, Copy)] +enum Direction { + Up, + Down, +} + +/// Distance from the [Presence] from which the world is loaded and information +/// is synced to clients. +/// +/// We limit the frequency that changes in the view distance change direction +/// (e.g. shifting from increasing the value to decreasing it). This is useful +/// since we want to avoid rapid cycles of shrinking and expanding of the view +/// distance. +#[derive(Debug)] +pub struct ViewDistance { + direction: Direction, + last_direction_change_time: Instant, + target: Option, + current: u32, +} + +impl ViewDistance { + /// Minimum time allowed between changes in direction of value adjustments. + const TIME_PER_DIR_CHANGE: Duration = Duration::from_millis(300); + + pub fn new(start_value: u32, now: Instant) -> Self { + Self { + direction: Direction::Up, + last_direction_change_time: now - Self::TIME_PER_DIR_CHANGE, + target: None, + current: start_value, + } + } + + /// Returns the current value. + pub fn current(&self) -> u32 { self.current } + + /// Applies deferred change based on the whether the time to apply it has + /// been reached. + pub fn update(&mut self, now: Instant) { + if let Some(target_val) = self.target { + if now.saturating_duration_since(self.last_direction_change_time) + > Self::TIME_PER_DIR_CHANGE + { + self.last_direction_change_time = now; + self.current = target_val; + self.target = None; + } + } + } + + /// Sets the target value. + /// + /// If this hasn't been changed recently or it is in the same direction as + /// the previous change it will be applied immediately. Otherwise, it + /// will be deferred to a later time (limiting the frequency of changes + /// in the change direction). + pub fn set_target(&mut self, new_target: u32, now: Instant) { + use core::cmp::Ordering; + let new_direction = match new_target.cmp(&self.current) { + Ordering::Equal => return, // No change needed. + Ordering::Less => Direction::Down, + Ordering::Greater => Direction::Up, + }; + + // Change is in the same direction as before so we can just apply it. + if new_direction == self.direction { + self.current = new_target; + self.target = None; + // If it has already been a while since the last direction change we can + // directly apply the request and switch the direction. + } else if now.saturating_duration_since(self.last_direction_change_time) + > Self::TIME_PER_DIR_CHANGE + { + self.direction = new_direction; + self.last_direction_change_time = now; + self.current = new_target; + self.target = None; + // Otherwise, we need to defer the request. + } else { + self.target = Some(new_target); + } + } +} diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index dd1241f5b8..5a271b5131 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -26,6 +26,7 @@ use common::{ resources::{Time, TimeOfDay}, slowjob::SlowJobPool, uid::{Uid, UidAllocator}, + ViewDistances, }; use common_net::{ msg::{CharacterInfo, PlayerListUpdate, PresenceKind, ServerGeneral}, @@ -107,9 +108,14 @@ pub trait StateExt { index: &world::IndexOwned, ) -> EcsEntityBuilder; /// Insert common/default components for a new character joining the server - fn initialize_character_data(&mut self, entity: EcsEntity, character_id: CharacterId); + fn initialize_character_data( + &mut self, + entity: EcsEntity, + character_id: CharacterId, + view_distances: ViewDistances, + ); /// Insert common/default components for a new spectator joining the server - fn initialize_spectator_data(&mut self, entity: EcsEntity); + fn initialize_spectator_data(&mut self, entity: EcsEntity, view_distances: ViewDistances); /// Update the components associated with the entity's current character. /// Performed after loading component data from the database fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents); @@ -488,10 +494,21 @@ impl StateExt for State { self.ecs_mut() .create_entity_synced() .with(pos) - .with(Presence::new(view_distance, PresenceKind::Spectator)) + .with(Presence::new( + ViewDistances { + terrain: view_distance, + entity: view_distance, + }, + PresenceKind::Spectator, + )) } - fn initialize_character_data(&mut self, entity: EcsEntity, character_id: CharacterId) { + fn initialize_character_data( + &mut self, + entity: EcsEntity, + character_id: CharacterId, + view_distances: ViewDistances, + ) { let spawn_point = self.ecs().read_resource::().0; if let Some(player_uid) = self.read_component_copied::(entity) { @@ -519,10 +536,9 @@ impl StateExt for State { // Make sure physics components are updated self.write_component_ignore_entity_dead(entity, comp::ForceUpdate::forced()); - const INITIAL_VD: u32 = 5; //will be changed after login self.write_component_ignore_entity_dead( entity, - Presence::new(INITIAL_VD, PresenceKind::Character(character_id)), + Presence::new(view_distances, PresenceKind::Character(character_id)), ); // Tell the client its request was successful. @@ -532,7 +548,7 @@ impl StateExt for State { } } - fn initialize_spectator_data(&mut self, entity: EcsEntity) { + fn initialize_spectator_data(&mut self, entity: EcsEntity, view_distances: ViewDistances) { let spawn_point = self.ecs().read_resource::().0; if self.read_component_copied::(entity).is_some() { @@ -545,10 +561,9 @@ impl StateExt for State { // Make sure physics components are updated self.write_component_ignore_entity_dead(entity, comp::ForceUpdate::forced()); - const INITIAL_VD: u32 = 5; //will be changed after login self.write_component_ignore_entity_dead( entity, - Presence::new(INITIAL_VD, PresenceKind::Spectator), + Presence::new(view_distances, PresenceKind::Spectator), ); // Tell the client its request was successful. diff --git a/server/src/sys/entity_sync.rs b/server/src/sys/entity_sync.rs index ceff297429..dc303e732f 100644 --- a/server/src/sys/entity_sync.rs +++ b/server/src/sys/entity_sync.rs @@ -384,7 +384,8 @@ impl<'a> System<'a> for Sys { let is_near = |o_pos: Vec3| { pos.zip_with(presence, |pos, presence| { pos.0.xy().distance_squared(o_pos.xy()) - < (presence.view_distance as f32 * TerrainChunkSize::RECT_SIZE.x as f32) + < (presence.entity_view_distance.current() as f32 + * TerrainChunkSize::RECT_SIZE.x as f32) .powi(2) }) }; diff --git a/server/src/sys/msg/character_screen.rs b/server/src/sys/msg/character_screen.rs index d9a12f58f0..0e6ce84b28 100644 --- a/server/src/sys/msg/character_screen.rs +++ b/server/src/sys/msg/character_screen.rs @@ -64,17 +64,17 @@ impl Sys { }; match msg { // Request spectator state - ClientGeneral::Spectate => { + ClientGeneral::Spectate(requested_view_distances) => { if let Some(admin) = admins.get(entity) && admin.0 >= AdminRole::Moderator { send_join_messages()?; - server_emitter.emit(ServerEvent::InitSpectator(entity)); + server_emitter.emit(ServerEvent::InitSpectator(entity, requested_view_distances)); } else { debug!("dropped Spectate msg from unprivileged client") } }, - ClientGeneral::Character(character_id) => { + ClientGeneral::Character(character_id, requested_view_distances) => { if let Some(player) = players.get(entity) { if presences.contains(entity) { debug!("player already ingame, aborting"); @@ -117,6 +117,7 @@ impl Sys { server_emitter.emit(ServerEvent::InitCharacterData { entity, character_id, + requested_view_distances, }); } } else { diff --git a/server/src/sys/msg/in_game.rs b/server/src/sys/msg/in_game.rs index 474d16a902..95d92bee8b 100644 --- a/server/src/sys/msg/in_game.rs +++ b/server/src/sys/msg/in_game.rs @@ -17,6 +17,7 @@ use common_ecs::{Job, Origin, Phase, System}; use common_net::msg::{ClientGeneral, PresenceKind, ServerGeneral}; use common_state::{BlockChange, BuildAreas}; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteStorage}; +use std::time::Instant; use tracing::{debug, trace, warn}; use vek::*; @@ -49,9 +50,10 @@ impl Sys { _terrain_persistence: &mut TerrainPersistenceData<'_>, maybe_player: &Option<&Player>, maybe_admin: &Option<&Admin>, + time_for_vd_changes: Instant, msg: ClientGeneral, ) -> Result<(), crate::error::Error> { - let presence = match maybe_presence { + let presence = match maybe_presence.as_deref_mut() { Some(g) => g, None => { debug!(?entity, "client is not in_game, ignoring msg"); @@ -66,21 +68,15 @@ impl Sys { client.send(ServerGeneral::ExitInGameSuccess)?; *maybe_presence = None; }, - ClientGeneral::SetViewDistance(view_distance) => { - presence.view_distance = settings - .max_view_distance - .map(|max| view_distance.min(max)) - .unwrap_or(view_distance); + ClientGeneral::SetViewDistance(view_distances) => { + let clamped_vds = view_distances.clamp(settings.max_view_distance); - //correct client if its VD is to high - if settings - .max_view_distance - .map(|max| view_distance > max) - .unwrap_or(false) - { - client.send(ServerGeneral::SetViewDistance( - settings.max_view_distance.unwrap_or(0), - ))?; + presence.terrain_view_distance.set_target(clamped_vds.terrain, time_for_vd_changes); + presence.entity_view_distance.set_target(clamped_vds.entity, time_for_vd_changes); + + // Correct client if its requested VD is too high. + if view_distances.terrain != clamped_vds.terrain { + client.send(ServerGeneral::SetViewDistance(clamped_vds.terrain))?; } }, ClientGeneral::ControllerInputs(inputs) => { @@ -299,8 +295,8 @@ impl Sys { | ClientGeneral::CreateCharacter { .. } | ClientGeneral::EditCharacter { .. } | ClientGeneral::DeleteCharacter(_) - | ClientGeneral::Character(_) - | ClientGeneral::Spectate + | ClientGeneral::Character(_, _) + | ClientGeneral::Spectate(_) | ClientGeneral::TerrainChunkRequest { .. } | ClientGeneral::LodZoneRequest { .. } | ClientGeneral::ChatMsg(_) @@ -378,6 +374,8 @@ impl<'a> System<'a> for Sys { ) { let mut server_emitter = server_event_bus.emitter(); + let time_for_vd_changes = Instant::now(); + for (entity, client, mut maybe_presence, player, maybe_admin) in ( &entities, &mut clients, @@ -387,12 +385,15 @@ impl<'a> System<'a> for Sys { ) .join() { + // If an `ExitInGame` message is received this is set to `None` allowing further + // ingame messages to be ignored. + let mut clearable_maybe_presence = maybe_presence.as_deref_mut(); let _ = super::try_recv_all(client, 2, |client, msg| { Self::handle_client_in_game_msg( &mut server_emitter, entity, client, - &mut maybe_presence.as_deref_mut(), + &mut clearable_maybe_presence, &terrain, &can_build, &is_rider, @@ -410,9 +411,17 @@ impl<'a> System<'a> for Sys { &mut terrain_persistence, &player, &maybe_admin, + time_for_vd_changes, msg, ) }); + + // Ensure deferred view distance changes are applied (if the + // requsite time has elapsed). + if let Some(presence) = maybe_presence { + presence.terrain_view_distance.update(time_for_vd_changes); + presence.entity_view_distance.update(time_for_vd_changes); + } } } } diff --git a/server/src/sys/msg/terrain.rs b/server/src/sys/msg/terrain.rs index af91135db6..c60ea357fb 100644 --- a/server/src/sys/msg/terrain.rs +++ b/server/src/sys/msg/terrain.rs @@ -76,7 +76,7 @@ impl<'a> System<'a> for Sys { pos.0.xy().map(|e| e as f64).distance_squared( key.map(|e| e as f64 + 0.5) * TerrainChunkSize::RECT_SIZE.map(|e| e as f64), - ) < ((presence.view_distance as f64 - 1.0 + ) < ((presence.terrain_view_distance.current() as f64 - 1.0 + 2.5 * 2.0_f64.sqrt()) * TerrainChunkSize::RECT_SIZE.x as f64) .powi(2) diff --git a/server/src/sys/subscription.rs b/server/src/sys/subscription.rs index a2748ac8ea..ea3521ddbc 100644 --- a/server/src/sys/subscription.rs +++ b/server/src/sys/subscription.rs @@ -60,7 +60,8 @@ impl<'a> System<'a> for Sys { // To update subscriptions // 1. Iterate through clients // 2. Calculate current chunk position - // 3. If chunk is the same return, otherwise continue (use fuzziness) + // 3. If chunk is different (use fuzziness) or the client view distance + // has changed continue, otherwise return // 4. Iterate through subscribed regions // 5. Check if region is still in range (use fuzziness) // 6. If not in range @@ -78,13 +79,15 @@ impl<'a> System<'a> for Sys { ) .join() { - let vd = presence.view_distance; + let vd = presence.entity_view_distance.current(); // Calculate current chunk let chunk = (Vec2::::from(pos.0)) .map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32); - // Only update regions when moving to a new chunk - // uses a fuzzy border to prevent rapid triggering when moving along chunk - // boundaries + // Only update regions when moving to a new chunk or if view distance has + // changed. + // + // Uses a fuzzy border to prevent rapid triggering when moving along chunk + // boundaries. if chunk != subscription.fuzzy_chunk && (subscription .fuzzy_chunk @@ -96,7 +99,10 @@ impl<'a> System<'a> for Sys { e.abs() > (sz / 2 + presence::CHUNK_FUZZ) as f32 }) .reduce_or() + || subscription.last_entity_view_distance != vd { + // Update the view distance + subscription.last_entity_view_distance = vd; // Update current chunk subscription.fuzzy_chunk = Vec2::::from(pos.0) .map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32); @@ -216,7 +222,7 @@ pub fn initialize_region_subscription(world: &World, entity: specs::Entity) { let chunk_size = TerrainChunkSize::RECT_SIZE.reduce_max() as f32; let regions = regions_in_vd( client_pos.0, - (presence.view_distance as f32 * chunk_size) as f32 + (presence.entity_view_distance.current() as f32 * chunk_size) as f32 + (presence::CHUNK_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(), ); @@ -261,6 +267,7 @@ pub fn initialize_region_subscription(world: &World, entity: specs::Entity) { if let Err(e) = world.write_storage().insert(entity, RegionSubscription { fuzzy_chunk, + last_entity_view_distance: presence.entity_view_distance.current(), regions, }) { error!(?e, "Failed to insert region subscription component"); diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 9bf2ecf288..369852785b 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -269,7 +269,7 @@ impl<'a> System<'a> for Sys { .map(|e: i32| (e.unsigned_abs()).saturating_sub(2)) .magnitude_squared(); - if adjusted_dist_sqr <= presence.view_distance.pow(2) { + if adjusted_dist_sqr <= presence.terrain_view_distance.current().pow(2) { chunk_send_emitter.emit(ChunkSendEntry { entity, chunk_key: key, @@ -293,7 +293,7 @@ impl<'a> System<'a> for Sys { // For each player with a position, calculate the distance. for (presence, pos) in (&presences, &positions).join() { - if chunk_in_vd(pos.0, chunk_key, &terrain, presence.view_distance) { + if chunk_in_vd(pos.0, chunk_key, &terrain, presence.terrain_view_distance.current()) { should_drop = false; break; } diff --git a/server/src/sys/terrain_sync.rs b/server/src/sys/terrain_sync.rs index a12302b393..4212261a15 100644 --- a/server/src/sys/terrain_sync.rs +++ b/server/src/sys/terrain_sync.rs @@ -33,8 +33,12 @@ impl<'a> System<'a> for Sys { // Sync changed chunks for chunk_key in &terrain_changes.modified_chunks { for (entity, presence, pos) in (&entities, &presences, &positions).join() { - if super::terrain::chunk_in_vd(pos.0, *chunk_key, &terrain, presence.view_distance) - { + if super::terrain::chunk_in_vd( + pos.0, + *chunk_key, + &terrain, + presence.terrain_view_distance.current(), + ) { chunk_send_emitter.emit(ChunkSendEntry { entity, chunk_key: *chunk_key, diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 8acb48ce9c..5d950626d1 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -3063,6 +3063,7 @@ impl Hud { &self.imgs, &self.fonts, i18n, + client.server_view_distance_limit(), fps as f32, ) .set(self.ids.settings_window, ui_widgets) diff --git a/voxygen/src/hud/settings_window/mod.rs b/voxygen/src/hud/settings_window/mod.rs index 6b045ef933..6e55347e52 100644 --- a/voxygen/src/hud/settings_window/mod.rs +++ b/voxygen/src/hud/settings_window/mod.rs @@ -93,6 +93,7 @@ pub struct SettingsWindow<'a> { imgs: &'a Imgs, fonts: &'a Fonts, localized_strings: &'a Localization, + server_view_distance_limit: Option, fps: f32, #[conrod(common_builder)] common: widget::CommonBuilder, @@ -105,6 +106,7 @@ impl<'a> SettingsWindow<'a> { imgs: &'a Imgs, fonts: &'a Fonts, localized_strings: &'a Localization, + server_view_distance_limit: Option, fps: f32, ) -> Self { Self { @@ -113,6 +115,7 @@ impl<'a> SettingsWindow<'a> { imgs, fonts, localized_strings, + server_view_distance_limit, fps, common: widget::CommonBuilder::default(), } @@ -299,11 +302,17 @@ impl<'a> Widget for SettingsWindow<'a> { } }, SettingsTab::Video => { - for change in - video::Video::new(global_state, imgs, fonts, localized_strings, self.fps) - .top_left_with_margins_on(state.ids.settings_content_align, 0.0, 0.0) - .wh_of(state.ids.settings_content_align) - .set(state.ids.video, ui) + for change in video::Video::new( + global_state, + imgs, + fonts, + localized_strings, + self.server_view_distance_limit, + self.fps, + ) + .top_left_with_margins_on(state.ids.settings_content_align, 0.0, 0.0) + .wh_of(state.ids.settings_content_align) + .set(state.ids.video, ui) { events.push(Event::SettingsChange(change.into())); } @@ -327,11 +336,16 @@ impl<'a> Widget for SettingsWindow<'a> { } }, 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) + for change in networking::Networking::new( + global_state, + imgs, + fonts, + localized_strings, + self.server_view_distance_limit, + ) + .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())); } diff --git a/voxygen/src/hud/settings_window/networking.rs b/voxygen/src/hud/settings_window/networking.rs index 8a7904e55e..b892bbf122 100644 --- a/voxygen/src/hud/settings_window/networking.rs +++ b/voxygen/src/hud/settings_window/networking.rs @@ -15,9 +15,12 @@ widget_ids! { struct Ids { window, window_r, - vd_text, - vd_slider, - vd_value, + terrain_vd_text, + terrain_vd_slider, + terrain_vd_value, + entity_vd_text, + entity_vd_slider, + entity_vd_value, player_physics_behavior_text, player_physics_behavior_list, lossy_terrain_compression_button, @@ -34,6 +37,7 @@ pub struct Networking<'a> { imgs: &'a Imgs, fonts: &'a Fonts, localized_strings: &'a Localization, + server_view_distance_limit: Option, #[conrod(common_builder)] common: widget::CommonBuilder, } @@ -43,12 +47,14 @@ impl<'a> Networking<'a> { imgs: &'a Imgs, fonts: &'a Fonts, localized_strings: &'a Localization, + server_view_distance_limit: Option, ) -> Self { Self { global_state, imgs, fonts, localized_strings, + server_view_distance_limit, common: widget::CommonBuilder::default(), } } @@ -94,34 +100,84 @@ impl<'a> Widget for Networking<'a> { .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) - .set(state.ids.vd_text, ui); + .set(state.ids.terrain_vd_text, ui); + let terrain_view_distance = self.global_state.settings.graphics.terrain_view_distance; + let server_view_distance_limit = self.server_view_distance_limit.unwrap_or(u32::MAX); if let Some(new_val) = ImageSlider::discrete( - self.global_state.settings.graphics.view_distance, + terrain_view_distance, 1, - 65, + client::MAX_SELECTABLE_VIEW_DISTANCE, self.imgs.slider_indicator, self.imgs.slider, ) .w_h(104.0, 22.0) - .down_from(state.ids.vd_text, 8.0) + .down_from(state.ids.terrain_vd_text, 8.0) .track_breadth(12.0) .slider_length(10.0) + .soft_max(server_view_distance_limit) .pad_track((5.0, 5.0)) - .set(state.ids.vd_slider, ui) + .set(state.ids.terrain_vd_slider, ui) { - events.push(NetworkingChange::AdjustViewDistance(new_val)); + events.push(NetworkingChange::AdjustTerrainViewDistance(new_val)); } - Text::new(&format!( - "{}", - self.global_state.settings.graphics.view_distance - )) - .right_from(state.ids.vd_slider, 8.0) + Text::new(&if terrain_view_distance <= server_view_distance_limit { + format!("{terrain_view_distance}") + } else { + format!("{terrain_view_distance} ({server_view_distance_limit})") + }) + .right_from(state.ids.terrain_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); + .set(state.ids.terrain_vd_value, ui); + + // Entity View Distance + Text::new( + &self + .localized_strings + .get_msg("hud-settings-entity_view_distance"), + ) + .down_from(state.ids.terrain_vd_slider, 10.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.entity_vd_text, ui); + + let soft_entity_vd_max = self + .server_view_distance_limit + .unwrap_or(u32::MAX) + .min(terrain_view_distance); + let entity_view_distance = self.global_state.settings.graphics.entity_view_distance; + if let Some(new_val) = ImageSlider::discrete( + entity_view_distance, + 1, + client::MAX_SELECTABLE_VIEW_DISTANCE, + self.imgs.slider_indicator, + self.imgs.slider, + ) + .w_h(104.0, 22.0) + .down_from(state.ids.entity_vd_text, 8.0) + .track_breadth(12.0) + .slider_length(10.0) + .soft_max(soft_entity_vd_max) + .pad_track((5.0, 5.0)) + .set(state.ids.entity_vd_slider, ui) + { + events.push(NetworkingChange::AdjustEntityViewDistance(new_val)); + } + + Text::new(&if entity_view_distance <= soft_entity_vd_max { + format!("{entity_view_distance}") + } else { + format!("{entity_view_distance} ({soft_entity_vd_max})") + }) + .right_from(state.ids.entity_vd_slider, 8.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.entity_vd_value, ui); // Player physics behavior Text::new( @@ -129,7 +185,7 @@ impl<'a> Widget for Networking<'a> { .localized_strings .get_msg("hud-settings-player_physics_behavior"), ) - .down_from(state.ids.vd_slider, 8.0) + .down_from(state.ids.entity_vd_slider, 8.0) .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) diff --git a/voxygen/src/hud/settings_window/video.rs b/voxygen/src/hud/settings_window/video.rs index 2d4d9cd1a8..6b445957a5 100644 --- a/voxygen/src/hud/settings_window/video.rs +++ b/voxygen/src/hud/settings_window/video.rs @@ -36,9 +36,12 @@ widget_ids! { reset_graphics_button, fps_counter, pipeline_recreation_text, - vd_slider, - vd_text, - vd_value, + terrain_vd_slider, + terrain_vd_text, + terrain_vd_value, + entity_vd_slider, + entity_vd_text, + entity_vd_value, ld_slider, ld_text, ld_value, @@ -131,6 +134,7 @@ pub struct Video<'a> { imgs: &'a Imgs, fonts: &'a Fonts, localized_strings: &'a Localization, + server_view_distance_limit: Option, fps: f32, #[conrod(common_builder)] common: widget::CommonBuilder, @@ -141,6 +145,7 @@ impl<'a> Video<'a> { imgs: &'a Imgs, fonts: &'a Fonts, localized_strings: &'a Localization, + server_view_distance_limit: Option, fps: f32, ) -> Self { Self { @@ -148,6 +153,7 @@ impl<'a> Video<'a> { imgs, fonts, localized_strings, + server_view_distance_limit, fps, common: widget::CommonBuilder::default(), } @@ -282,38 +288,126 @@ impl<'a> Widget for Video<'a> { .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) - .set(state.ids.vd_text, ui); + .set(state.ids.terrain_vd_text, ui); + let terrain_view_distance = self.global_state.settings.graphics.terrain_view_distance; + let server_view_distance_limit = self.server_view_distance_limit.unwrap_or(u32::MAX); if let Some(new_val) = ImageSlider::discrete( - self.global_state.settings.graphics.view_distance, + terrain_view_distance, 1, - 65, + client::MAX_SELECTABLE_VIEW_DISTANCE, self.imgs.slider_indicator, self.imgs.slider, ) .w_h(104.0, 22.0) - .down_from(state.ids.vd_text, 8.0) + .down_from(state.ids.terrain_vd_text, 8.0) .track_breadth(12.0) .slider_length(10.0) + .soft_max(server_view_distance_limit) .pad_track((5.0, 5.0)) - .set(state.ids.vd_slider, ui) + .set(state.ids.terrain_vd_slider, ui) { - events.push(GraphicsChange::AdjustViewDistance(new_val)); + events.push(GraphicsChange::AdjustTerrainViewDistance(new_val)); } - Text::new(&format!( - "{}", - self.global_state.settings.graphics.view_distance - )) - .right_from(state.ids.vd_slider, 8.0) + Text::new(&if terrain_view_distance <= server_view_distance_limit { + format!("{terrain_view_distance}") + } else { + format!("{terrain_view_distance} ({server_view_distance_limit})") + }) + .right_from(state.ids.terrain_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); + .set(state.ids.terrain_vd_value, ui); + + // Entity View Distance + let soft_entity_vd_max = self + .server_view_distance_limit + .unwrap_or(u32::MAX) + .min(terrain_view_distance); + let entity_view_distance = self.global_state.settings.graphics.entity_view_distance; + if let Some(new_val) = ImageSlider::discrete( + entity_view_distance, + 1, + client::MAX_SELECTABLE_VIEW_DISTANCE, + self.imgs.slider_indicator, + self.imgs.slider, + ) + .w_h(104.0, 22.0) + .right_from(state.ids.terrain_vd_slider, 70.0) + .track_breadth(12.0) + .slider_length(10.0) + .soft_max(soft_entity_vd_max) + .pad_track((5.0, 5.0)) + .set(state.ids.entity_vd_slider, ui) + { + events.push(GraphicsChange::AdjustEntityViewDistance(new_val)); + } + + Text::new( + &self + .localized_strings + .get_msg("hud-settings-entity_view_distance"), + ) + .up_from(state.ids.entity_vd_slider, 10.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.entity_vd_text, ui); + + Text::new(&if entity_view_distance <= soft_entity_vd_max { + format!("{entity_view_distance}") + } else { + format!("{entity_view_distance} ({soft_entity_vd_max})") + }) + .right_from(state.ids.entity_vd_slider, 8.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.entity_vd_value, ui); + + // Sprites VD + if let Some(new_val) = ImageSlider::discrete( + self.global_state.settings.graphics.sprite_render_distance, + 50, + 500, + self.imgs.slider_indicator, + self.imgs.slider, + ) + .w_h(104.0, 22.0) + .right_from(state.ids.entity_vd_slider, 70.0) + .track_breadth(12.0) + .slider_length(10.0) + .pad_track((5.0, 5.0)) + .set(state.ids.sprite_dist_slider, ui) + { + events.push(GraphicsChange::AdjustSpriteRenderDistance(new_val)); + } + Text::new( + &self + .localized_strings + .get_msg("hud-settings-sprites_view_distance"), + ) + .up_from(state.ids.sprite_dist_slider, 10.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.sprite_dist_text, ui); + + Text::new(&format!( + "{}", + self.global_state.settings.graphics.sprite_render_distance + )) + .right_from(state.ids.sprite_dist_slider, 8.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.sprite_dist_value, ui); // LoD Distance Text::new(&self.localized_strings.get_msg("hud-settings-lod_distance")) - .down_from(state.ids.vd_slider, 10.0) + .down_from(state.ids.terrain_vd_slider, 10.0) .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) @@ -346,6 +440,50 @@ impl<'a> Widget for Video<'a> { .color(TEXT_COLOR) .set(state.ids.ld_value, ui); + // Figure LOD distance + if let Some(new_val) = ImageSlider::discrete( + self.global_state + .settings + .graphics + .figure_lod_render_distance, + 50, + 500, + self.imgs.slider_indicator, + self.imgs.slider, + ) + .w_h(104.0, 22.0) + .right_from(state.ids.ld_slider, 70.0) + .track_breadth(12.0) + .slider_length(10.0) + .pad_track((5.0, 5.0)) + .set(state.ids.figure_dist_slider, ui) + { + events.push(GraphicsChange::AdjustFigureLoDRenderDistance(new_val)); + } + Text::new( + &self + .localized_strings + .get_msg("hud-settings-entities_detail_distance"), + ) + .up_from(state.ids.figure_dist_slider, 8.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.figure_dist_text, ui); + + Text::new(&format!( + "{}", + self.global_state + .settings + .graphics + .figure_lod_render_distance + )) + .right_from(state.ids.figure_dist_slider, 8.0) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.figure_dist_value, ui); + // Max FPS Text::new(&self.localized_strings.get_msg("hud-settings-maximum_fps")) .down_from(state.ids.ld_slider, 10.0) @@ -388,7 +526,7 @@ impl<'a> Widget for Video<'a> { .get_msg("hud-settings-background_fps"), ) .down_from(state.ids.ld_slider, 10.0) - .right_from(state.ids.max_fps_value, 30.0) + .right_from(state.ids.max_fps_value, 44.0) .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) @@ -436,7 +574,7 @@ impl<'a> Widget for Video<'a> { // Present Mode Text::new(&self.localized_strings.get_msg("hud-settings-present_mode")) .down_from(state.ids.ld_slider, 10.0) - .right_from(state.ids.max_background_fps_value, 30.0) + .right_from(state.ids.max_background_fps_value, 40.0) .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) @@ -448,16 +586,11 @@ impl<'a> Widget for Video<'a> { PresentMode::Immediate, ]; let mode_label_list = [ - &self - .localized_strings - .get_msg("hud-settings-present_mode-fifo"), - &self - .localized_strings - .get_msg("hud-settings-present_mode-mailbox"), - &self - .localized_strings - .get_msg("hud-settings-present_mode-immediate"), - ]; + "hud-settings-present_mode-vsync_capped", + "hud-settings-present_mode-vsync_uncapped", + "hud-settings-present_mode-vsync_off", + ] + .map(|k| self.localized_strings.get_msg(k)); // Get which present mode is currently active let selected = mode_list @@ -465,7 +598,7 @@ impl<'a> Widget for Video<'a> { .position(|x| *x == render_mode.present_mode); if let Some(clicked) = DropDownList::new(&mode_label_list, selected) - .w_h(120.0, 22.0) + .w_h(150.0, 26.0) .color(MENU_BG) .label_color(TEXT_COLOR) .label_font_id(self.fonts.cyri.conrod_id) @@ -651,87 +784,6 @@ impl<'a> Widget for Video<'a> { .color(TEXT_COLOR) .set(state.ids.ambiance_value, ui); - // Sprites VD - if let Some(new_val) = ImageSlider::discrete( - self.global_state.settings.graphics.sprite_render_distance, - 50, - 500, - self.imgs.slider_indicator, - self.imgs.slider, - ) - .w_h(104.0, 22.0) - .right_from(state.ids.vd_slider, 50.0) - .track_breadth(12.0) - .slider_length(10.0) - .pad_track((5.0, 5.0)) - .set(state.ids.sprite_dist_slider, ui) - { - events.push(GraphicsChange::AdjustSpriteRenderDistance(new_val)); - } - Text::new( - &self - .localized_strings - .get_msg("hud-settings-sprites_view_distance"), - ) - .up_from(state.ids.sprite_dist_slider, 8.0) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.sprite_dist_text, ui); - - Text::new(&format!( - "{}", - self.global_state.settings.graphics.sprite_render_distance - )) - .right_from(state.ids.sprite_dist_slider, 8.0) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.sprite_dist_value, ui); - // Figure VD - if let Some(new_val) = ImageSlider::discrete( - self.global_state - .settings - .graphics - .figure_lod_render_distance, - 50, - 500, - self.imgs.slider_indicator, - self.imgs.slider, - ) - .w_h(104.0, 22.0) - .right_from(state.ids.sprite_dist_slider, 50.0) - .track_breadth(12.0) - .slider_length(10.0) - .pad_track((5.0, 5.0)) - .set(state.ids.figure_dist_slider, ui) - { - events.push(GraphicsChange::AdjustFigureLoDRenderDistance(new_val)); - } - Text::new( - &self - .localized_strings - .get_msg("hud-settings-figures_view_distance"), - ) - .up_from(state.ids.figure_dist_slider, 8.0) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.figure_dist_text, ui); - - Text::new(&format!( - "{}", - self.global_state - .settings - .graphics - .figure_lod_render_distance - )) - .right_from(state.ids.figure_dist_slider, 8.0) - .font_size(self.fonts.cyri.scale(14)) - .font_id(self.fonts.cyri.conrod_id) - .color(TEXT_COLOR) - .set(state.ids.figure_dist_value, ui); - // AaMode Text::new( &self diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs index 3fbbf7e212..a345ad17f0 100644 --- a/voxygen/src/menu/char_selection/mod.rs +++ b/voxygen/src/menu/char_selection/mod.rs @@ -137,10 +137,11 @@ impl PlayState for CharSelectionState { ui::Event::Play(character_id) => { { let mut c = self.client.borrow_mut(); - c.request_character(character_id); - //Send our ViewDistance and LoD distance - c.set_view_distance(global_state.settings.graphics.view_distance); - c.set_lod_distance(global_state.settings.graphics.lod_distance); + let graphics = &global_state.settings.graphics; + c.request_character(character_id, common::ViewDistances { + terrain: graphics.terrain_view_distance, + entity: graphics.entity_view_distance, + }); } return PlayStateResult::Switch(Box::new(SessionState::new( global_state, @@ -150,9 +151,11 @@ impl PlayState for CharSelectionState { ui::Event::Spectate => { { let mut c = self.client.borrow_mut(); - c.request_spectate(); - c.set_view_distance(global_state.settings.graphics.view_distance); - c.set_lod_distance(global_state.settings.graphics.lod_distance); + let graphics = &global_state.settings.graphics; + c.request_spectate(common::ViewDistances { + terrain: graphics.terrain_view_distance, + entity: graphics.entity_view_distance, + }); } return PlayStateResult::Switch(Box::new(SessionState::new( global_state, @@ -215,12 +218,7 @@ impl PlayState for CharSelectionState { Ok(events) => { for event in events { match event { - client::Event::SetViewDistance(vd) => { - global_state.settings.graphics.view_distance = vd; - global_state - .settings - .save_to_file_warn(&global_state.config_dir); - }, + client::Event::SetViewDistance(_vd) => {}, client::Event::Disconnect => { global_state.info_message = Some( localized_strings diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index 88246035b1..7b7d3cfa1e 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -231,12 +231,7 @@ impl PlayState for MainMenuState { Ok(events) => { for event in events { match event { - client::Event::SetViewDistance(vd) => { - global_state.settings.graphics.view_distance = vd; - global_state - .settings - .save_to_file_warn(&global_state.config_dir); - }, + client::Event::SetViewDistance(_vd) => {}, client::Event::Disconnect => { global_state.info_message = Some( localized_strings diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 4d1684dbc6..9d79d9c1c0 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -629,7 +629,7 @@ impl FigureMgr { let time = state.get_time() as f32; let tick = scene_data.tick; let ecs = state.ecs(); - let view_distance = scene_data.view_distance; + let view_distance = scene_data.entity_view_distance; let dt = state.get_delta_time(); let dt_lerp = (15.0 * dt).min(1.0); let frustum = camera.frustum(); diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index a059025a47..47a50d8e40 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -120,7 +120,8 @@ pub struct SceneData<'a> { pub mutable_viewpoint: bool, pub target_entity: Option, pub loaded_distance: f32, - pub view_distance: u32, + pub terrain_view_distance: u32, // not used currently + pub entity_view_distance: u32, pub tick: u64, pub gamma: f32, pub exposure: f32, diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index f3ab46bdff..237c89da82 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -351,12 +351,7 @@ impl SessionState { client::Event::Notification(n) => { self.hud.new_notification(n); }, - client::Event::SetViewDistance(vd) => { - global_state.settings.graphics.view_distance = vd; - global_state - .settings - .save_to_file_warn(&global_state.config_dir); - }, + client::Event::SetViewDistance(_vd) => {}, client::Event::Outcome(outcome) => outcomes.push(outcome), client::Event::CharacterCreated(_) => {}, client::Event::CharacterEdited(_) => {}, @@ -1702,7 +1697,11 @@ impl PlayState for SessionState { // Only highlight if interactable target_entity: self.interactable.and_then(Interactable::entity), loaded_distance: client.loaded_distance(), - view_distance: client.view_distance().unwrap_or(1), + terrain_view_distance: client.view_distance().unwrap_or(1), + entity_view_distance: client + .view_distance() + .unwrap_or(1) + .min(global_state.settings.graphics.entity_view_distance), tick: client.get_tick(), gamma: global_state.settings.graphics.gamma, exposure: global_state.settings.graphics.exposure, @@ -1788,7 +1787,11 @@ impl PlayState for SessionState { // Only highlight if interactable target_entity: self.interactable.and_then(Interactable::entity), loaded_distance: client.loaded_distance(), - view_distance: client.view_distance().unwrap_or(1), + terrain_view_distance: client.view_distance().unwrap_or(1), + entity_view_distance: client + .view_distance() + .unwrap_or(1) + .min(settings.graphics.entity_view_distance), tick: client.get_tick(), gamma: settings.graphics.gamma, exposure: settings.graphics.exposure, diff --git a/voxygen/src/session/settings_change.rs b/voxygen/src/session/settings_change.rs index 4870820688..4c82431be8 100644 --- a/voxygen/src/session/settings_change.rs +++ b/voxygen/src/session/settings_change.rs @@ -70,7 +70,8 @@ pub enum Gameplay { } #[derive(Clone)] pub enum Graphics { - AdjustViewDistance(u32), + AdjustTerrainViewDistance(u32), + AdjustEntityViewDistance(u32), AdjustLodDistance(u32), AdjustLodDetail(u32), AdjustSpriteRenderDistance(u32), @@ -147,7 +148,8 @@ pub enum Language { } #[derive(Clone)] pub enum Networking { - AdjustViewDistance(u32), + AdjustTerrainViewDistance(u32), + AdjustEntityViewDistance(u32), ChangePlayerPhysicsBehavior { server_authoritative: bool, }, @@ -155,6 +157,8 @@ pub enum Networking { #[cfg(feature = "discord")] ToggleDiscordIntegration(bool), + // TODO: reset option (ensure it handles the entity/terrain vd the same as graphics reset + // option) } #[derive(Clone)] @@ -354,13 +358,11 @@ impl SettingsChange { }, SettingsChange::Graphics(graphics_change) => { match graphics_change { - Graphics::AdjustViewDistance(view_distance) => { - session_state - .client - .borrow_mut() - .set_view_distance(view_distance); - - settings.graphics.view_distance = view_distance; + Graphics::AdjustTerrainViewDistance(terrain_vd) => { + adjust_terrain_view_distance(terrain_vd, settings, session_state) + }, + Graphics::AdjustEntityViewDistance(entity_vd) => { + adjust_entity_view_distance(entity_vd, settings, session_state) }, Graphics::AdjustLodDistance(lod_distance) => { session_state @@ -433,10 +435,7 @@ impl SettingsChange { settings.graphics = GraphicsSettings::default(); let graphics = &settings.graphics; // View distance - session_state - .client - .borrow_mut() - .set_view_distance(graphics.view_distance); + client_set_view_distance(settings, session_state); // FOV session_state.scene.camera_mut().set_fov_deg(graphics.fov); session_state @@ -604,12 +603,11 @@ impl SettingsChange { }, }, 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::AdjustTerrainViewDistance(terrain_vd) => { + adjust_terrain_view_distance(terrain_vd, settings, session_state) + }, + Networking::AdjustEntityViewDistance(entity_vd) => { + adjust_entity_view_distance(entity_vd, settings, session_state) }, Networking::ChangePlayerPhysicsBehavior { server_authoritative, @@ -654,6 +652,39 @@ impl SettingsChange { }, }, } - settings.save_to_file_warn(&global_state.config_dir); + global_state + .settings + .save_to_file_warn(&global_state.config_dir); } } + +use crate::settings::Settings; + +fn adjust_terrain_view_distance( + terrain_vd: u32, + settings: &mut Settings, + session_state: &mut SessionState, +) { + settings.graphics.terrain_view_distance = terrain_vd; + client_set_view_distance(settings, session_state); +} + +fn adjust_entity_view_distance( + entity_vd: u32, + settings: &mut Settings, + session_state: &mut SessionState, +) { + settings.graphics.entity_view_distance = entity_vd; + client_set_view_distance(settings, session_state); +} + +fn client_set_view_distance(settings: &Settings, session_state: &mut SessionState) { + let view_distances = common::ViewDistances { + terrain: settings.graphics.terrain_view_distance, + entity: settings.graphics.entity_view_distance, + }; + session_state + .client + .borrow_mut() + .set_view_distances(view_distances); +} diff --git a/voxygen/src/settings/graphics.rs b/voxygen/src/settings/graphics.rs index b2a1b4858c..303bf77f49 100644 --- a/voxygen/src/settings/graphics.rs +++ b/voxygen/src/settings/graphics.rs @@ -29,7 +29,8 @@ impl fmt::Display for Fps { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] pub struct GraphicsSettings { - pub view_distance: u32, + pub terrain_view_distance: u32, + pub entity_view_distance: u32, pub lod_distance: u32, pub sprite_render_distance: u32, pub particles_enabled: bool, @@ -50,7 +51,8 @@ pub struct GraphicsSettings { impl Default for GraphicsSettings { fn default() -> Self { Self { - view_distance: 10, + terrain_view_distance: 10, + entity_view_distance: client::MAX_SELECTABLE_VIEW_DISTANCE, lod_distance: 200, sprite_render_distance: 100, particles_enabled: true, diff --git a/voxygen/src/ui/widgets/image_slider.rs b/voxygen/src/ui/widgets/image_slider.rs index dbff4e518e..cc273b8f15 100644 --- a/voxygen/src/ui/widgets/image_slider.rs +++ b/voxygen/src/ui/widgets/image_slider.rs @@ -29,6 +29,10 @@ pub struct ImageSlider { value: T, min: T, max: T, + // If `value > soft_max` we will display the slider at `soft_max` along with a faded ghost + // slider at `value`. The slider displayed at `soft_max` is purely a visual indicator and has + // no effect on the values produced by this slider. + soft_max: T, /// The amount in which the slider's display should be skewed. /// /// Higher skew amounts (above 1.0) will weigh lower values. @@ -65,6 +69,7 @@ widget_ids! { struct Ids { track, slider, + soft_max_slider, } } @@ -76,6 +81,7 @@ pub struct State { impl ImageSlider { builder_methods! { pub skew { skew = f32 } + pub soft_max { soft_max = T } pub pad_track { track.padding = (f32, f32) } pub hover_image { slider.hover_image_id = Some(image::Id) } pub press_image { slider.press_image_id = Some(image::Id) } @@ -85,18 +91,16 @@ impl ImageSlider { pub slider_color { slider.color = Some(Color) } } - fn new( - value: T, - min: T, - max: T, - slider_image_id: image::Id, - track_image_id: image::Id, - ) -> Self { + fn new(value: T, min: T, max: T, slider_image_id: image::Id, track_image_id: image::Id) -> Self + where + T: Copy, + { Self { common: widget::CommonBuilder::default(), value, min, max, + soft_max: max, skew: 1.0, track: Track { image_id: track_image_id, @@ -133,7 +137,7 @@ where impl ImageSlider where - T: Integer, + T: Integer + Copy, { pub fn discrete( value: T, @@ -266,45 +270,67 @@ where .unwrap_or(slider.image_id); // A rectangle for positioning and sizing the slider. - let value_perc = utils::map_range(new_value, min, max, 0.0, 1.0); - let unskewed_perc = value_perc.powf(1.0 / skew as f64); - let slider_rect = if is_horizontal { - let pos = utils::map_range( - unskewed_perc, - 0.0, - 1.0, - rect.x.start + start_pad, - rect.x.end - end_pad, - ); - let w = slider.length.map_or(rect.w() / 10.0, |w| w as f64); - Rect { - x: Range::from_pos_and_len(pos, w), - ..rect - } - } else { - let pos = utils::map_range( - unskewed_perc, - 0.0, - 1.0, - rect.y.start + start_pad, - rect.y.end - end_pad, - ); - let h = slider.length.map_or(rect.h() / 10.0, |h| h as f64); - Rect { - y: Range::from_pos_and_len(pos, h), - ..rect + let slider_rect = |slider_value| { + let value_perc = utils::map_range(slider_value, min, max, 0.0, 1.0); + let unskewed_perc = value_perc.powf(1.0 / skew as f64); + if is_horizontal { + let pos = utils::map_range( + unskewed_perc, + 0.0, + 1.0, + rect.x.start + start_pad, + rect.x.end - end_pad, + ); + let w = slider.length.map_or(rect.w() / 10.0, |w| w as f64); + Rect { + x: Range::from_pos_and_len(pos, w), + ..rect + } + } else { + let pos = utils::map_range( + unskewed_perc, + 0.0, + 1.0, + rect.y.start + start_pad, + rect.y.end - end_pad, + ); + let h = slider.length.map_or(rect.h() / 10.0, |h| h as f64); + Rect { + y: Range::from_pos_and_len(pos, h), + ..rect + } } }; - let (x, y, w, h) = slider_rect.x_y_w_h(); + // Whether soft max slider needs to be displayed and main slider faded to look + // like a ghost. + let over_soft_max = new_value > self.soft_max; + + let (x, y, w, h) = slider_rect(new_value).x_y_w_h(); + let fade = if over_soft_max { 0.5 } else { 1.0 }; Image::new(slider_image) .x_y(x, y) .w_h(w, h) .parent(id) .graphics_for(id) - .color(slider.color) + .color(Some( + slider + .color + .map_or(Color::Rgba(1.0, 1.0, 1.0, fade), |c: Color| c.alpha(fade)), + )) .set(state.ids.slider, ui); + if over_soft_max { + let (x, y, w, h) = slider_rect(self.soft_max).x_y_w_h(); + Image::new(slider_image) + .x_y(x, y) + .w_h(w, h) + .parent(id) + .graphics_for(id) + .color(slider.color) + .set(state.ids.soft_max_slider, ui); + } + // If the value has just changed, return the new value. if value != new_value { Some(new_value)