Merge branch 'imbris/random-fixes' into 'master'

Random Fixes 3

See merge request veloren/veloren!1463
This commit is contained in:
Imbris 2020-10-30 01:52:36 +00:00
commit 397dffd5bc
20 changed files with 332 additions and 251 deletions

View File

@ -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

14
Cargo.lock generated
View File

@ -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",

View File

@ -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()
};
}

View File

@ -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>>,

View File

@ -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};

View File

@ -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 {

3
common/src/consts.rs Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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
};

View File

@ -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,
}
}

View File

@ -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")
}

View File

@ -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.");
}

View File

@ -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}

View File

@ -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,
})
}

View File

@ -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;

View File

@ -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| {

View File

@ -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());
}
}

View File

@ -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,
}
}
}

View File

@ -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)))
})
})
}

View File

@ -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) }