diff --git a/CHANGELOG.md b/CHANGELOG.md index 734b6c53a7..c5458fa2ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Moved hammer leap attack to skillbar - Reworked fire staff - Overhauled cloud shaders to add mist, light attenuation, an approximation of rayleigh scattering, etc. +- Fixed a bug where a nearby item would also be collected when collecting collectible blocks +- Allowed collecting nearby blocks without aiming at them +- Made voxygen wait until singleplayer server is initialized before attempting to connect, removing the chance for it to give up on connecting if the server takes a while to start +- Log where userdata folder is located ### Removed diff --git a/Cargo.lock b/Cargo.lock index fb8f8f1e2e..7bdaf0910a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2963,6 +2963,15 @@ dependencies = [ "num-traits 0.2.12", ] +[[package]] +name = "ordered-float" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fe9037165d7023b1228bc4ae9a2fa1a2b0095eca6c2998c624723dfd01314a5" +dependencies = [ + "num-traits 0.2.12", +] + [[package]] name = "osascript" version = "0.3.0" @@ -3691,7 +3700,7 @@ dependencies = [ "crossbeam-utils 0.7.2", "linked-hash-map", "num_cpus", - "ordered-float", + "ordered-float 1.1.0", "rustc-hash", "stb_truetype", ] @@ -4962,6 +4971,7 @@ dependencies = [ "native-dialog", "num 0.2.1", "old_school_gfx_glutin_ext", + "ordered-float 2.0.0", "rand 0.7.3", "rodio", "ron", @@ -5014,7 +5024,7 @@ dependencies = [ "minifb", "noise", "num 0.2.1", - "ordered-float", + "ordered-float 1.1.0", "packed_simd_2", "rand 0.7.3", "rand_chacha 0.2.2", diff --git a/common/src/clock.rs b/common/src/clock.rs index 02d4946e5f..5fc354622f 100644 --- a/common/src/clock.rs +++ b/common/src/clock.rs @@ -9,7 +9,7 @@ const CLOCK_SMOOTHING: f64 = 0.9; pub struct Clock { last_sys_time: Instant, last_delta: Option<Duration>, - running_tps_average: f64, + running_average_delta: f64, compensation: f64, } @@ -18,18 +18,18 @@ impl Clock { Self { last_sys_time: Instant::now(), last_delta: None, - running_tps_average: 0.0, + running_average_delta: 0.0, compensation: 1.0, } } - pub fn get_tps(&self) -> f64 { 1.0 / self.running_tps_average } + pub fn get_tps(&self) -> f64 { 1.0 / self.running_average_delta } pub fn get_last_delta(&self) -> Duration { self.last_delta.unwrap_or_else(|| Duration::new(0, 0)) } - pub fn get_avg_delta(&self) -> Duration { Duration::from_secs_f64(self.running_tps_average) } + pub fn get_avg_delta(&self) -> Duration { Duration::from_secs_f64(self.running_average_delta) } pub fn tick(&mut self, tgt: Duration) { span!(_guard, "tick", "Clock::tick"); @@ -37,9 +37,9 @@ impl Clock { // Attempt to sleep to fill the gap. if let Some(sleep_dur) = tgt.checked_sub(delta) { - if self.running_tps_average != 0.0 { + if self.running_average_delta != 0.0 { self.compensation = - (self.compensation + (tgt.as_secs_f64() / self.running_tps_average) - 1.0) + (self.compensation + (tgt.as_secs_f64() / self.running_average_delta) - 1.0) .max(0.0) } @@ -53,10 +53,10 @@ impl Clock { self.last_sys_time = Instant::now(); self.last_delta = Some(delta); - self.running_tps_average = if self.running_tps_average == 0.0 { + self.running_average_delta = if self.running_average_delta == 0.0 { delta.as_secs_f64() } else { - CLOCK_SMOOTHING * self.running_tps_average + CLOCK_SMOOTHING * self.running_average_delta + (1.0 - CLOCK_SMOOTHING) * delta.as_secs_f64() }; } diff --git a/common/src/comp/inventory/mod.rs b/common/src/comp/inventory/mod.rs index d57f268d4a..f24fcc3ae1 100644 --- a/common/src/comp/inventory/mod.rs +++ b/common/src/comp/inventory/mod.rs @@ -8,9 +8,6 @@ use serde::{Deserialize, Serialize}; use specs::{Component, FlaggedStorage, HashMapStorage}; use specs_idvs::IdvStorage; -// The limit on distance between the entity and a collectible (squared) -pub const MAX_PICKUP_RANGE_SQR: f32 = 64.0; - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Inventory { slots: Vec<Option<Item>>, diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 3aa9556d99..f2016bcb88 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -49,13 +49,13 @@ pub use inputs::CanBuild; pub use inventory::{ item, item::{Item, ItemDrop}, - slot, Inventory, InventoryUpdate, InventoryUpdateEvent, MAX_PICKUP_RANGE_SQR, + slot, Inventory, InventoryUpdate, InventoryUpdateEvent, }; pub use last::Last; pub use location::{Waypoint, WaypointArea}; pub use misc::Object; pub use phys::{Collider, ForceUpdate, Gravity, Mass, Ori, PhysicsState, Pos, Scale, Sticky, Vel}; -pub use player::{Player, MAX_MOUNT_RANGE_SQR}; +pub use player::Player; pub use projectile::Projectile; pub use shockwave::{Shockwave, ShockwaveHitEntities}; pub use skills::{Skill, SkillGroup, SkillGroupType, SkillSet}; diff --git a/common/src/comp/player.rs b/common/src/comp/player.rs index a6818af377..7b12b8741a 100644 --- a/common/src/comp/player.rs +++ b/common/src/comp/player.rs @@ -5,7 +5,6 @@ use specs::{Component, FlaggedStorage, NullStorage}; use specs_idvs::IdvStorage; const MAX_ALIAS_LEN: usize = 32; -pub const MAX_MOUNT_RANGE_SQR: i32 = 20000; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Player { diff --git a/common/src/consts.rs b/common/src/consts.rs new file mode 100644 index 0000000000..c82a9f4cd3 --- /dev/null +++ b/common/src/consts.rs @@ -0,0 +1,3 @@ +// The limit on distance between the entity and a collectible (squared) +pub const MAX_PICKUP_RANGE: f32 = 8.0; +pub const MAX_MOUNT_RANGE: f32 = 14.0; diff --git a/common/src/lib.rs b/common/src/lib.rs index 01b986408a..b35122fa67 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -24,6 +24,7 @@ pub mod clock; pub mod cmd; pub mod combat; pub mod comp; +pub mod consts; pub mod effect; pub mod event; pub mod explosion; diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index 73bdc649a4..1707656261 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -85,6 +85,7 @@ fn main() -> io::Result<()> { // Determine folder to save server data in let server_data_dir = { let mut path = common::userdata_dir_workspace!(); + info!("Using userdata folder at {}", path.display()); path.push(server::DEFAULT_DATA_DIR_NAME); path }; diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index fff06d9e99..5dce6f7aab 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -3,8 +3,9 @@ use common::{ comp::{ self, item, slot::{self, Slot}, - Pos, MAX_PICKUP_RANGE_SQR, + Pos, }, + consts::MAX_PICKUP_RANGE, msg::ServerGeneral, recipe::default_recipe_book, sync::{Uid, WorldSyncExt}, @@ -512,7 +513,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv fn within_pickup_range(player_position: Option<&Pos>, item_position: Option<&Pos>) -> bool { match (player_position, item_position) { - (Some(ppos), Some(ipos)) => ppos.0.distance_squared(ipos.0) < MAX_PICKUP_RANGE_SQR, + (Some(ppos), Some(ipos)) => ppos.0.distance_squared(ipos.0) < MAX_PICKUP_RANGE.powi(2), _ => false, } } diff --git a/server/src/settings.rs b/server/src/settings.rs index 379ca1d15e..759773b62a 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -72,13 +72,15 @@ impl Settings { match ron::de::from_reader(file) { Ok(x) => x, Err(e) => { + let default_settings = Self::default(); + let template_path = path.with_extension("template.ron"); warn!( ?e, "Failed to parse setting file! Falling back to default settings and \ - creating a template file for you to migrate your current settings file" + creating a template file for you to migrate your current settings file: \ + {}", + template_path.display() ); - let default_settings = Self::default(); - let template_path = path.with_extension("template.ron"); if let Err(e) = default_settings.save_to_file(&template_path) { error!(?e, "Failed to create template settings file") } diff --git a/server/src/settings/editable.rs b/server/src/settings/editable.rs index ce8ff36431..9b1c451fd1 100644 --- a/server/src/settings/editable.rs +++ b/server/src/settings/editable.rs @@ -37,7 +37,7 @@ pub trait EditableSetting: Serialize + DeserializeOwned + Default { new_path = path.with_extension(format!("invalid{}.ron", i)); } - warn!("Renaming invalid settings file to: {}", path.display()); + warn!("Renaming invalid settings file to: {}", new_path.display()); if let Err(e) = fs::rename(&path, &new_path) { warn!(?e, ?path, ?new_path, "Failed to rename settings file."); } diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index 439574c43f..3711dbb952 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -66,6 +66,7 @@ hashbrown = {version = "0.7.2", features = ["rayon", "serde", "nightly"]} image = {version = "0.23.8", default-features = false, features = ["ico", "png"]} native-dialog = { version = "0.4.2", default-features = false, optional = true } num = "0.2" +ordered-float = { version = "2.0.0", default-features = false } rand = "0.7" rodio = {version = "0.11", default-features = false, features = ["wav", "vorbis"]} ron = {version = "0.6", default-features = false} diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 907a04857f..a0496cadf5 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -1123,7 +1123,7 @@ impl Hud { for (pos, item, distance) in (&entities, &pos, &items) .join() .map(|(_, pos, item)| (pos, item, pos.0.distance_squared(player_pos))) - .filter(|(_, _, distance)| distance < &common::comp::MAX_PICKUP_RANGE_SQR) + .filter(|(_, _, distance)| distance < &common::consts::MAX_PICKUP_RANGE.powi(2)) { let overitem_id = overitem_walker.next( &mut self.ids.overitems, @@ -2455,6 +2455,28 @@ impl Hud { self.force_ungrab = !self.force_ungrab; true }, + + // If not showing the ui don't allow keys that change the ui state but do listen for + // hotbar keys + event if !self.show.ui => { + if let WinEvent::InputUpdate(key, state) = event { + if let Some(slot) = try_hotbar_slot_from_input(key) { + handle_slot( + slot, + state, + &mut self.events, + &mut self.slot_manager, + &mut self.hotbar, + ); + true + } else { + false + } + } else { + false + } + }, + WinEvent::Zoom(_) => !cursor_grabbed && !self.ui.no_widget_capturing_mouse(), WinEvent::InputUpdate(GameInput::Chat, true) => { @@ -2521,107 +2543,20 @@ impl Hud { true }, // Skillbar - GameInput::Slot1 => { - handle_slot( - hotbar::Slot::One, - state, - &mut self.events, - &mut self.slot_manager, - &mut self.hotbar, - ); - true + input => { + if let Some(slot) = try_hotbar_slot_from_input(input) { + handle_slot( + slot, + state, + &mut self.events, + &mut self.slot_manager, + &mut self.hotbar, + ); + true + } else { + false + } }, - GameInput::Slot2 => { - handle_slot( - hotbar::Slot::Two, - state, - &mut self.events, - &mut self.slot_manager, - &mut self.hotbar, - ); - true - }, - GameInput::Slot3 => { - handle_slot( - hotbar::Slot::Three, - state, - &mut self.events, - &mut self.slot_manager, - &mut self.hotbar, - ); - true - }, - GameInput::Slot4 => { - handle_slot( - hotbar::Slot::Four, - state, - &mut self.events, - &mut self.slot_manager, - &mut self.hotbar, - ); - true - }, - GameInput::Slot5 => { - handle_slot( - hotbar::Slot::Five, - state, - &mut self.events, - &mut self.slot_manager, - &mut self.hotbar, - ); - true - }, - GameInput::Slot6 => { - handle_slot( - hotbar::Slot::Six, - state, - &mut self.events, - &mut self.slot_manager, - &mut self.hotbar, - ); - true - }, - GameInput::Slot7 => { - handle_slot( - hotbar::Slot::Seven, - state, - &mut self.events, - &mut self.slot_manager, - &mut self.hotbar, - ); - true - }, - GameInput::Slot8 => { - handle_slot( - hotbar::Slot::Eight, - state, - &mut self.events, - &mut self.slot_manager, - &mut self.hotbar, - ); - true - }, - GameInput::Slot9 => { - handle_slot( - hotbar::Slot::Nine, - state, - &mut self.events, - &mut self.slot_manager, - &mut self.hotbar, - ); - true - }, - GameInput::Slot10 => { - handle_slot( - hotbar::Slot::Ten, - state, - &mut self.events, - &mut self.slot_manager, - &mut self.hotbar, - ); - true - }, - _ => false, }, // Else the player is typing in chat WinEvent::InputUpdate(_key, _) => self.typing(), @@ -2676,8 +2611,8 @@ impl Hud { .handle_event(conrod_core::event::Input::Text("\t".to_string())); } + // Optimization: skip maintaining UI when it's off. if !self.show.ui { - // Optimization: skip maintaining UI when it's off. return std::mem::take(&mut self.events); } @@ -2735,3 +2670,19 @@ fn get_buff_info(buff: &comp::Buff) -> BuffInfo { dur: buff.time, } } + +fn try_hotbar_slot_from_input(input: GameInput) -> Option<hotbar::Slot> { + Some(match input { + GameInput::Slot1 => hotbar::Slot::One, + GameInput::Slot2 => hotbar::Slot::Two, + GameInput::Slot3 => hotbar::Slot::Three, + GameInput::Slot4 => hotbar::Slot::Four, + GameInput::Slot5 => hotbar::Slot::Five, + GameInput::Slot6 => hotbar::Slot::Six, + GameInput::Slot7 => hotbar::Slot::Seven, + GameInput::Slot8 => hotbar::Slot::Eight, + GameInput::Slot9 => hotbar::Slot::Nine, + GameInput::Slot10 => hotbar::Slot::Ten, + _ => return None, + }) +} diff --git a/voxygen/src/hud/overitem.rs b/voxygen/src/hud/overitem.rs index 6ba6823dac..f45366176e 100644 --- a/voxygen/src/hud/overitem.rs +++ b/voxygen/src/hud/overitem.rs @@ -91,9 +91,10 @@ impl<'a> Widget for Overitem<'a> { // ——— // scale at max distance is 10, and at min distance is 30 - let scale: f64 = - ((1.5 - (self.distance_from_player_sqr / common::comp::MAX_PICKUP_RANGE_SQR)) * 20.0) - .into(); + let scale: f64 = ((1.5 + - (self.distance_from_player_sqr / common::consts::MAX_PICKUP_RANGE.powi(2))) + * 20.0) + .into(); let text_font_size = scale * 1.0; let text_pos_y = scale * 1.2; let btn_rect_size = scale * 0.8; diff --git a/voxygen/src/main.rs b/voxygen/src/main.rs index 06b672bcb2..80794f9134 100644 --- a/voxygen/src/main.rs +++ b/voxygen/src/main.rs @@ -19,7 +19,7 @@ use common::{ clock::Clock, }; use std::panic; -use tracing::{error, warn}; +use tracing::{error, info, warn}; fn main() { // Load the settings @@ -35,6 +35,12 @@ fn main() { // Init logging and hold the guards. let _guards = logging::init(&settings); + if let Some(path) = veloren_voxygen::settings::voxygen_data_dir().parent() { + info!("Using userdata dir at: {}", path.display()); + } else { + error!("Can't log userdata dir, voxygen data dir has no parent!"); + } + // Set up panic handler to relay swish panic messages to the user let default_hook = panic::take_hook(); panic::set_hook(Box::new(move |panic_info| { diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index b996ba2e2d..a9a0d88718 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -51,7 +51,7 @@ impl PlayState for MainMenuState { &crate::i18n::i18n_asset_key(&global_state.settings.language.selected_language), ); - //Poll server creation + // Poll server creation #[cfg(feature = "singleplayer")] { if let Some(singleplayer) = &global_state.singleplayer { @@ -62,6 +62,18 @@ impl PlayState for MainMenuState { self.client_init = None; self.main_menu_ui.cancel_connection(); self.main_menu_ui.show_info(format!("Error: {:?}", error)); + } else { + let server_settings = singleplayer.settings(); + // Attempt login after the server is finished initializing + attempt_login( + &mut global_state.settings, + &mut global_state.info_message, + "singleplayer".to_owned(), + "".to_owned(), + server_settings.gameserver_address.ip().to_string(), + server_settings.gameserver_address.port(), + &mut self.client_init, + ); } } } @@ -208,7 +220,8 @@ impl PlayState for MainMenuState { server_address, } => { attempt_login( - global_state, + &mut global_state.settings, + &mut global_state.info_message, username, password, server_address, @@ -229,18 +242,9 @@ impl PlayState for MainMenuState { }, #[cfg(feature = "singleplayer")] MainMenuEvent::StartSingleplayer => { - let (singleplayer, server_settings) = Singleplayer::new(None); // TODO: Make client and server use the same thread pool + let singleplayer = Singleplayer::new(None); // TODO: Make client and server use the same thread pool global_state.singleplayer = Some(singleplayer); - - attempt_login( - global_state, - "singleplayer".to_owned(), - "".to_owned(), - server_settings.gameserver_address.ip().to_string(), - server_settings.gameserver_address.port(), - &mut self.client_init, - ); }, MainMenuEvent::Settings => {}, // TODO MainMenuEvent::Quit => return PlayStateResult::Shutdown, @@ -279,19 +283,20 @@ impl PlayState for MainMenuState { } fn attempt_login( - global_state: &mut GlobalState, + settings: &mut Settings, + info_message: &mut Option<String>, username: String, password: String, server_address: String, server_port: u16, client_init: &mut Option<ClientInit>, ) { - let mut net_settings = &mut global_state.settings.networking; + let mut net_settings = &mut settings.networking; net_settings.username = username.clone(); if !net_settings.servers.contains(&server_address) { net_settings.servers.push(server_address.clone()); } - if let Err(e) = global_state.settings.save_to_file() { + if let Err(e) = settings.save_to_file() { warn!(?e, "Failed to save settings"); } @@ -301,11 +306,11 @@ fn attempt_login( *client_init = Some(ClientInit::new( (server_address, server_port, false), username, - Some(global_state.settings.graphics.view_distance), + Some(settings.graphics.view_distance), password, )); } } else { - global_state.info_message = Some("Invalid username".to_string()); + *info_message = Some("Invalid username".to_string()); } } diff --git a/voxygen/src/scene/terrain/watcher.rs b/voxygen/src/scene/terrain/watcher.rs index b589e09742..23989f305b 100644 --- a/voxygen/src/scene/terrain/watcher.rs +++ b/voxygen/src/scene/terrain/watcher.rs @@ -13,6 +13,9 @@ pub struct BlocksOfInterest { pub beehives: Vec<Vec3<i32>>, pub reeds: Vec<Vec3<i32>>, pub flowers: Vec<Vec3<i32>>, + // Note: these are only needed for chunks within the iteraction range so this is a potential + // area for optimization + pub interactables: Vec<Vec3<i32>>, } impl BlocksOfInterest { @@ -24,6 +27,7 @@ impl BlocksOfInterest { let mut beehives = Vec::new(); let mut reeds = Vec::new(); let mut flowers = Vec::new(); + let mut interactables = Vec::new(); chunk .vol_iter( @@ -34,29 +38,34 @@ impl BlocksOfInterest { chunk.get_max_z(), ), ) - .for_each(|(pos, block)| match block.kind() { - BlockKind::Leaves => { - if thread_rng().gen_range(0, 16) == 0 { - leaves.push(pos) - } - }, - BlockKind::Grass => { - if thread_rng().gen_range(0, 16) == 0 { - grass.push(pos) - } - }, - _ => match block.get_sprite() { - Some(SpriteKind::Ember) => embers.push(pos), - Some(SpriteKind::Beehive) => beehives.push(pos), - Some(SpriteKind::Reed) => reeds.push(pos), - Some(SpriteKind::PinkFlower) => flowers.push(pos), - Some(SpriteKind::PurpleFlower) => flowers.push(pos), - Some(SpriteKind::RedFlower) => flowers.push(pos), - Some(SpriteKind::WhiteFlower) => flowers.push(pos), - Some(SpriteKind::YellowFlower) => flowers.push(pos), - Some(SpriteKind::Sunflower) => flowers.push(pos), - _ => {}, - }, + .for_each(|(pos, block)| { + match block.kind() { + BlockKind::Leaves => { + if thread_rng().gen_range(0, 16) == 0 { + leaves.push(pos) + } + }, + BlockKind::Grass => { + if thread_rng().gen_range(0, 16) == 0 { + grass.push(pos) + } + }, + _ => match block.get_sprite() { + Some(SpriteKind::Ember) => embers.push(pos), + Some(SpriteKind::Beehive) => beehives.push(pos), + Some(SpriteKind::Reed) => reeds.push(pos), + Some(SpriteKind::PinkFlower) => flowers.push(pos), + Some(SpriteKind::PurpleFlower) => flowers.push(pos), + Some(SpriteKind::RedFlower) => flowers.push(pos), + Some(SpriteKind::WhiteFlower) => flowers.push(pos), + Some(SpriteKind::YellowFlower) => flowers.push(pos), + Some(SpriteKind::Sunflower) => flowers.push(pos), + _ => {}, + }, + } + if block.is_collectible() { + interactables.push(pos); + } }); Self { @@ -66,6 +75,7 @@ impl BlocksOfInterest { beehives, reeds, flowers, + interactables, } } } diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index d0b3d3a8c9..f99d8890c4 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -15,10 +15,8 @@ use client::{self, Client}; use common::{ assets::Asset, comp, - comp::{ - ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel, MAX_MOUNT_RANGE_SQR, - MAX_PICKUP_RANGE_SQR, - }, + comp::{ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel}, + consts::{MAX_MOUNT_RANGE, MAX_PICKUP_RANGE}, event::EventBus, outcome::Outcome, span, @@ -26,6 +24,7 @@ use common::{ util::Dir, vol::ReadVol, }; +use ordered_float::OrderedFloat; use specs::{Join, WorldExt}; use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration}; use tracing::{error, info}; @@ -205,6 +204,7 @@ impl PlayState for SessionState { fn tick(&mut self, global_state: &mut GlobalState, events: Vec<Event>) -> PlayStateResult { span!(_guard, "tick", "<Session as PlayState>::tick"); + // TODO: let mut client = self.client.borrow_mut(); // NOTE: Not strictly necessary, but useful for hotloading translation changes. self.voxygen_i18n = VoxygenLocalization::load_expect(&i18n_asset_key( &global_state.settings.language.selected_language, @@ -272,16 +272,24 @@ impl PlayState for SessionState { .get(self.client.borrow().entity()) .is_some(); - // Only highlight collectables - self.scene.set_select_pos(select_pos.filter(|sp| { - self.client - .borrow() - .state() - .terrain() - .get(*sp) - .map(|b| b.is_collectible() || can_build) - .unwrap_or(false) - })); + let interactable = select_interactable( + &self.client.borrow(), + self.target_entity, + select_pos, + &self.scene, + ); + + // Only highlight interactables + // unless in build mode where select_pos highlighted + self.scene + .set_select_pos( + select_pos + .filter(|_| can_build) + .or_else(|| match interactable { + Some(Interactable::Block(_, block_pos)) => Some(block_pos), + _ => None, + }), + ); // Handle window events. for event in events { @@ -457,36 +465,22 @@ impl PlayState for SessionState { .copied(); if let Some(player_pos) = player_pos { // Find closest mountable entity - let mut closest_mountable: Option<(specs::Entity, i32)> = None; - - for (entity, pos, ms) in ( + let closest_mountable_entity = ( &client.state().ecs().entities(), &client.state().ecs().read_storage::<comp::Pos>(), &client.state().ecs().read_storage::<comp::MountState>(), ) .join() - .filter(|(entity, _, _)| *entity != client.entity()) - { - if comp::MountState::Unmounted != *ms { - continue; - } - - let dist = - (player_pos.0.distance_squared(pos.0) * 1000.0) as i32; - if dist > MAX_MOUNT_RANGE_SQR { - continue; - } - - if let Some(previous) = closest_mountable.as_mut() { - if dist < previous.1 { - *previous = (entity, dist); - } - } else { - closest_mountable = Some((entity, dist)); - } - } - - if let Some((mountee_entity, _)) = closest_mountable { + .filter(|(entity, _, mount_state)| { + *entity != client.entity() + && **mount_state == comp::MountState::Unmounted + }) + .map(|(entity, pos, _)| { + (entity, player_pos.0.distance_squared(pos.0)) + }) + .filter(|(_, dist_sqr)| *dist_sqr < MAX_MOUNT_RANGE.powi(2)) + .min_by_key(|(_, dist_sqr)| OrderedFloat(*dist_sqr)); + if let Some((mountee_entity, _)) = closest_mountable_entity { client.mount(mountee_entity); } } @@ -498,40 +492,25 @@ impl PlayState for SessionState { self.key_state.collect = state; if state { - let mut client = self.client.borrow_mut(); - - // Collect terrain sprites - if let Some(select_pos) = self.scene.select_pos() { - client.collect_block(select_pos); - } - - // Collect lootable entities - let player_pos = client - .state() - .read_storage::<comp::Pos>() - .get(client.entity()) - .copied(); - - if let Some(player_pos) = player_pos { - let entity = self.target_entity.or_else(|| { - ( - &client.state().ecs().entities(), - &client.state().ecs().read_storage::<comp::Pos>(), - &client.state().ecs().read_storage::<comp::Item>(), - ) - .join() - .filter(|(_, pos, _)| { - pos.0.distance_squared(player_pos.0) - < MAX_PICKUP_RANGE_SQR - }) - .min_by_key(|(_, pos, _)| { - (pos.0.distance_squared(player_pos.0) * 1000.0) as i32 - }) - .map(|(entity, _, _)| entity) - }); - - if let Some(entity) = entity { - client.pick_up(entity); + if let Some(interactable) = interactable { + let mut client = self.client.borrow_mut(); + match interactable { + Interactable::Block(block, pos) => { + if block.is_collectible() { + client.collect_block(pos); + } + }, + Interactable::Entity(entity) => { + if client + .state() + .ecs() + .read_storage::<comp::Item>() + .get(entity) + .is_some() + { + client.pick_up(entity); + } + }, } } } @@ -1162,6 +1141,7 @@ fn under_cursor( Option<Vec3<i32>>, Option<(specs::Entity, f32)>, ) { + span!(_guard, "under_cursor"); // Choose a spot above the player's head for item distance checks let player_entity = client.entity(); let player_pos = match client @@ -1184,7 +1164,7 @@ fn under_cursor( // The ray hit something, is it within range? let (build_pos, select_pos) = if matches!(cam_ray.1, Ok(Some(_)) if player_pos.distance_squared(cam_pos + cam_dir * cam_dist) - <= MAX_PICKUP_RANGE_SQR) + <= MAX_PICKUP_RANGE.powi(2)) { ( Some((cam_pos + cam_dir * (cam_dist - 0.01)).map(|e| e.floor() as i32)), @@ -1253,3 +1233,109 @@ fn under_cursor( // TODO: consider setting build/select to None when targeting an entity (build_pos, select_pos, target_entity) } + +#[derive(Clone, Copy)] +enum Interactable { + Block(Block, Vec3<i32>), + Entity(specs::Entity), +} + +/// Select interactable to hightlight, display interaction text for, and to +/// interact with if the interact key is pressed +/// Selected in the following order +/// 1) Targeted entity (if interactable) (entities can't be target through +/// blocks) 2) Selected block (if interactabl) +/// 3) Closest of nearest interactable entity/block +fn select_interactable( + client: &Client, + target_entity: Option<specs::Entity>, + selected_pos: Option<Vec3<i32>>, + scene: &Scene, +) -> Option<Interactable> { + span!(_guard, "select_interactable"); + use common::{spiral::Spiral2d, terrain::TerrainChunk, vol::RectRasterableVol}; + target_entity.map(Interactable::Entity) + .or_else(|| selected_pos.and_then(|sp| + client.state().terrain().get(sp).ok().copied() + .filter(Block::is_collectible).map(|b| Interactable::Block(b, sp)) + )) + .or_else(|| { + let ecs = client.state().ecs(); + let player_entity = client.entity(); + ecs + .read_storage::<comp::Pos>() + .get(player_entity).and_then(|player_pos| { + let closest_interactable_entity = ( + &ecs.entities(), + &ecs.read_storage::<comp::Pos>(), + ecs.read_storage::<comp::Scale>().maybe(), + &ecs.read_storage::<comp::Body>(), + // Must have this comp to be interactable (for now) + &ecs.read_storage::<comp::Item>(), + ) + .join() + .filter(|(e, _, _, _, _)| *e != player_entity) + .map(|(e, p, s, b, _)| { + let radius = s.map_or(1.0, |s| s.0) * b.radius(); + // Distance squared from player to the entity + // Note: the position of entities is currently at their feet so this + // distance is between their feet positions + let dist_sqr = p.0.distance_squared(player_pos.0); + (e, radius, dist_sqr) + }) + // Roughly filter out entities farther than interaction distance + .filter(|(_, r, d_sqr)| *d_sqr <= MAX_PICKUP_RANGE.powi(2) + 2.0 * MAX_PICKUP_RANGE * r + r.powi(2)) + // Note: entities are approximated as spheres here + // to determine which is closer + // Substract sphere radius from distance to the player + .map(|(e, r, d_sqr)| (e, d_sqr.sqrt() - r)) + .min_by_key(|(_, dist)| OrderedFloat(*dist)); + + // Only search as far as closest interactable entity + let search_dist = closest_interactable_entity + .map_or(MAX_PICKUP_RANGE, |(_, dist)| dist); + let player_chunk = player_pos.0.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| { + (e.floor() as i32).div_euclid(sz as i32) + }); + let terrain = scene.terrain(); + + // Find closest interactable block + // TODO: consider doing this one first? + let closest_interactable_block_pos = Spiral2d::new() + // TODO: this formula for the number to take was guessed + // Note: assume RECT_SIZE.x == RECT_SIZE.y + .take(((search_dist / TerrainChunk::RECT_SIZE.x as f32).ceil() as usize * 2 + 1).pow(2)) + .flat_map(|offset| { + let chunk_pos = player_chunk + offset; + let chunk_voxel_pos = + Vec3::<i32>::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32)); + terrain.get(chunk_pos).map(|data| (data, chunk_voxel_pos)) + }) + // TODO: maybe we could make this more efficient by putting the + // interactables is some sort of spatial structure + .flat_map(|(chunk_data, chunk_pos)| { + chunk_data + .blocks_of_interest + .interactables + .iter() + .map(move |block_offset| chunk_pos + block_offset) + }) + // TODO: confirm that adding 0.5 here is correct + .map(|block_pos| ( + block_pos, + block_pos.map(|e| e as f32 + 0.5) + .distance_squared(player_pos.0) + )) + .min_by_key(|(_, dist_sqr)| OrderedFloat(*dist_sqr)); + + // Pick closer one if they exist + closest_interactable_block_pos + .filter(|(_, dist_sqr)| search_dist.powi(2) > *dist_sqr) + .and_then(|(block_pos, _)| + client.state().terrain().get(block_pos).ok().copied() + .map(|b| Interactable::Block(b, block_pos)) + ) + .or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e))) + }) + }) +} diff --git a/voxygen/src/singleplayer.rs b/voxygen/src/singleplayer.rs index e60064ffce..b9bd860142 100644 --- a/voxygen/src/singleplayer.rs +++ b/voxygen/src/singleplayer.rs @@ -26,10 +26,12 @@ pub struct Singleplayer { pub receiver: Receiver<Result<(), ServerError>>, // Wether the server is stopped or not paused: Arc<AtomicBool>, + // Settings that the server was started with + settings: server::Settings, } impl Singleplayer { - pub fn new(client: Option<&Client>) -> (Self, server::Settings) { + pub fn new(client: Option<&Client>) -> Self { let (sender, receiver) = unbounded(); // Determine folder to save server data in @@ -119,17 +121,18 @@ impl Singleplayer { run_server(server, receiver, paused1); }); - ( - Singleplayer { - _server_thread: thread, - sender, - receiver: result_receiver, - paused, - }, + Singleplayer { + _server_thread: thread, + sender, + receiver: result_receiver, + paused, settings, - ) + } } + /// Returns reference to the settings the server was started with + pub fn settings(&self) -> &server::Settings { &self.settings } + /// Returns wether or not the server is paused pub fn is_paused(&self) -> bool { self.paused.load(Ordering::SeqCst) }