diff --git a/CHANGELOG.md b/CHANGELOG.md index a0add171a5..4388709cfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Significantly improved the performance of playing sound effects - Dismantle and Material crafting tabs don't have duplicated recipes - Campfires now despawn when underwater +- Players no longer spawn underground if their waypoint is underground ## [0.10.0] - 2021-06-12 diff --git a/common/src/terrain/mod.rs b/common/src/terrain/mod.rs index 19cc22bdd5..f31e906ba7 100644 --- a/common/src/terrain/mod.rs +++ b/common/src/terrain/mod.rs @@ -175,6 +175,48 @@ impl TerrainGrid { } } +impl TerrainChunk { + /// Find the highest or lowest accessible position within the chunk + pub fn find_accessible_pos(&self, spawn_wpos: Vec2, ascending: bool) -> Vec3 { + let min_z = self.get_min_z(); + let max_z = self.get_max_z(); + + let pos = Vec3::new( + spawn_wpos.x, + spawn_wpos.y, + if ascending { min_z } else { max_z }, + ); + (0..(max_z - min_z)) + .map(|z_diff| { + if ascending { + pos + Vec3::unit_z() * z_diff + } else { + pos - Vec3::unit_z() * z_diff + } + }) + .find(|test_pos| { + let chunk_relative_xy = test_pos + .xy() + .map2(TerrainChunkSize::RECT_SIZE, |e, sz| e.rem_euclid(sz as i32)); + self.get( + Vec3::new(chunk_relative_xy.x, chunk_relative_xy.y, test_pos.z) + - Vec3::unit_z(), + ) + .map_or(false, |b| b.is_filled()) + && (0..3).all(|z| { + self.get( + Vec3::new(chunk_relative_xy.x, chunk_relative_xy.y, test_pos.z) + + Vec3::unit_z() * z, + ) + .map_or(true, |b| !b.is_solid()) + }) + }) + .unwrap_or(pos) + .map(|e| e as f32) + + 0.5 + } +} + // Terrain helper functions used across multiple crates. /// Computes the position Vec2 of a SimChunk from an index, where the index was diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 14bf15ab67..f1948cf3ab 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -676,9 +676,11 @@ fn handle_site( }) .ok_or_else(|| "Site not found".to_string())?; - let site_pos = server - .world - .find_lowest_accessible_pos(server.index.as_index_ref(), site.center); + let site_pos = server.world.find_accessible_pos( + server.index.as_index_ref(), + TerrainChunkSize::center_wpos(site.center), + false, + ); position_mut(server, target, "target", |current_pos| { current_pos.0 = site_pos diff --git a/server/src/lib.rs b/server/src/lib.rs index 5622c5dd9e..66184909fa 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -50,7 +50,7 @@ use crate::{ connection_handler::ConnectionHandler, data_dir::DataDir, login_provider::LoginProvider, - presence::{Presence, RegionSubscription}, + presence::{Presence, RegionSubscription, RepositionOnChunkLoad}, rtsim::RtSim, state_ext::StateExt, sys::sentinel::{DeletedEntities, TrackedComps}, @@ -250,6 +250,7 @@ impl Server { state.ecs_mut().register::(); state.ecs_mut().register::(); state.ecs_mut().register::(); + state.ecs_mut().register::(); //Alias validator let banned_words_paths = &settings.banned_words_files; @@ -343,7 +344,7 @@ impl Server { }, }; - world.find_lowest_accessible_pos(index, spawn_chunk) + world.find_accessible_pos(index, TerrainChunkSize::center_wpos(spawn_chunk), false) }); #[cfg(not(feature = "worldgen"))] let spawn_point = SpawnPoint::default(); diff --git a/server/src/presence.rs b/server/src/presence.rs index 7c6df973a7..03a60d411d 100644 --- a/server/src/presence.rs +++ b/server/src/presence.rs @@ -1,7 +1,7 @@ use common_net::msg::PresenceKind; use hashbrown::HashSet; use serde::{Deserialize, Serialize}; -use specs::{Component, DerefFlaggedStorage}; +use specs::{Component, DerefFlaggedStorage, NullStorage}; use specs_idvs::IdvStorage; use vek::*; @@ -40,3 +40,10 @@ pub struct RegionSubscription { impl Component for RegionSubscription { type Storage = DerefFlaggedStorage>; } + +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)] +pub struct RepositionOnChunkLoad; + +impl Component for RepositionOnChunkLoad { + type Storage = NullStorage; +} diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 90432f4bfa..0e6044d517 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -1,6 +1,10 @@ use crate::{ - client::Client, persistence::PersistedComponents, presence::Presence, settings::Settings, - sys::sentinel::DeletedEntities, wiring, SpawnPoint, + client::Client, + persistence::PersistedComponents, + presence::{Presence, RepositionOnChunkLoad}, + settings::Settings, + sys::sentinel::DeletedEntities, + wiring, SpawnPoint, }; use common::{ character::CharacterId, @@ -525,6 +529,7 @@ impl StateExt for State { ); if let Some(waypoint) = waypoint { + self.write_component_ignore_entity_dead(entity, RepositionOnChunkLoad); self.write_component_ignore_entity_dead(entity, waypoint); self.write_component_ignore_entity_dead(entity, comp::Pos(waypoint.get_pos())); self.write_component_ignore_entity_dead(entity, comp::Vel(Vec3::zero())); diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 77afd1f2b1..8514cfbb31 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -1,9 +1,14 @@ use crate::{ - chunk_generator::ChunkGenerator, client::Client, metrics::NetworkRequestMetrics, - presence::Presence, rtsim::RtSim, settings::Settings, SpawnPoint, Tick, + chunk_generator::ChunkGenerator, + client::Client, + metrics::NetworkRequestMetrics, + presence::{Presence, RepositionOnChunkLoad}, + rtsim::RtSim, + settings::Settings, + SpawnPoint, Tick, }; use common::{ - comp::{self, agent, bird_medium, Alignment, BehaviorCapability, Pos}, + comp::{self, agent, bird_medium, Alignment, BehaviorCapability, ForceUpdate, Pos}, event::{EventBus, ServerEvent}, generation::{get_npc_name, EntityInfo}, npc::NPC_NAMES, @@ -14,7 +19,7 @@ use common_ecs::{Job, Origin, Phase, System}; use common_net::msg::{SerializedTerrainChunk, ServerGeneral}; use common_state::TerrainChanges; use comp::Behavior; -use specs::{Join, Read, ReadExpect, ReadStorage, Write, WriteExpect}; +use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteExpect, WriteStorage}; use std::sync::Arc; use vek::*; @@ -93,9 +98,12 @@ impl<'a> System<'a> for Sys { WriteExpect<'a, TerrainGrid>, Write<'a, TerrainChanges>, WriteExpect<'a, RtSim>, - ReadStorage<'a, Pos>, + WriteStorage<'a, Pos>, ReadStorage<'a, Presence>, ReadStorage<'a, Client>, + Entities<'a>, + WriteStorage<'a, RepositionOnChunkLoad>, + WriteStorage<'a, ForceUpdate>, ); const NAME: &'static str = "terrain"; @@ -114,9 +122,12 @@ impl<'a> System<'a> for Sys { mut terrain, mut terrain_changes, mut rtsim, - positions, + mut positions, presences, clients, + entities, + mut reposition_on_load, + mut force_update, ): Self::SystemData, ) { let mut server_emitter = server_event_bus.emitter(); @@ -306,6 +317,24 @@ impl<'a> System<'a> for Sys { } } + let mut repositioned = Vec::new(); + for (entity, pos, _) in (&entities, &mut positions, &reposition_on_load).join() { + // If an entity is marked as needing repositioning once the chunk loads (e.g. + // from having just logged in), reposition them. + + let chunk_pos = terrain.pos_key(pos.0.map(|e| e as i32)); + if let Some(chunk) = terrain.get_key(chunk_pos) { + pos.0 = chunk + .find_accessible_pos(pos.0.xy().as_::(), false) + .as_::(); + repositioned.push(entity); + let _ = force_update.insert(entity, ForceUpdate); + } + } + for entity in repositioned { + reposition_on_load.remove(entity); + } + // Send the chunk to all nearby players. use rayon::iter::{IntoParallelIterator, ParallelIterator}; new_chunks.into_par_iter().for_each(|(key, chunk)| { diff --git a/world/src/lib.rs b/world/src/lib.rs index dd7a5b9708..29531e8b8b 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -52,7 +52,9 @@ use common::{ assets, generation::{ChunkSupplement, EntityInfo}, resources::TimeOfDay, - terrain::{Block, BlockKind, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize}, + terrain::{ + Block, BlockKind, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize, TerrainGrid, + }, vol::{ReadVol, RectVolSize, WriteVol}, }; use common_net::msg::{world_msg, WorldMapMsg}; @@ -187,41 +189,21 @@ impl World { pub fn sample_blocks(&self) -> BlockGen { BlockGen::new(ColumnGen::new(&self.sim)) } - pub fn find_lowest_accessible_pos(&self, index: IndexRef, chunk_pos: Vec2) -> Vec3 { - // Calculate the middle of the chunk in the world - let spawn_wpos = TerrainChunkSize::center_wpos(chunk_pos); + pub fn find_accessible_pos( + &self, + index: IndexRef, + spawn_wpos: Vec2, + ascending: bool, + ) -> Vec3 { + let chunk_pos = TerrainGrid::chunk_key(spawn_wpos); // Unwrapping because generate_chunk only returns err when should_continue evals // to true let (tc, _cs) = self .generate_chunk(index, chunk_pos, || false, None) .unwrap(); - let min_z = tc.get_min_z(); - let max_z = tc.get_max_z(); - let pos = Vec3::new(spawn_wpos.x, spawn_wpos.y, min_z); - (0..(max_z - min_z)) - .map(|z_diff| pos + Vec3::unit_z() * z_diff) - .find(|test_pos| { - let chunk_relative_xy = test_pos - .xy() - .map2(TerrainChunkSize::RECT_SIZE, |e, sz| e.rem_euclid(sz as i32)); - tc.get( - Vec3::new(chunk_relative_xy.x, chunk_relative_xy.y, test_pos.z) - - Vec3::unit_z(), - ) - .map_or(false, |b| b.is_filled()) - && (0..3).all(|z| { - tc.get( - Vec3::new(chunk_relative_xy.x, chunk_relative_xy.y, test_pos.z) - + Vec3::unit_z() * z, - ) - .map_or(true, |b| !b.is_solid()) - }) - }) - .unwrap_or(pos) - .map(|e| e as f32) - + 0.5 + tc.find_accessible_pos(spawn_wpos, ascending) } #[allow(clippy::eval_order_dependence)]