From 7555be0e254e86a9f4ca61898a86fecd22ae712c Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Thu, 15 Jul 2021 17:52:20 -0400 Subject: [PATCH 1/3] Spawn players aboveground when using `/site` or when their waypoint is underground. --- CHANGELOG.md | 1 + server/src/cmd.rs | 8 +++++--- server/src/events/entity_creation.rs | 9 ++++++--- server/src/lib.rs | 2 +- server/src/state_ext.rs | 23 ++++++++++++++++++++--- world/src/lib.rs | 28 ++++++++++++++++++++++------ 6 files changed, 55 insertions(+), 16 deletions(-) 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/server/src/cmd.rs b/server/src/cmd.rs index 239a73b95e..76d58dc25a 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/events/entity_creation.rs b/server/src/events/entity_creation.rs index 8112123c96..b9fd5bee70 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -41,9 +41,12 @@ pub fn handle_loaded_character_data( Option, ), ) { - server - .state - .update_character_data(entity, loaded_components); + server.state.update_character_data( + entity, + loaded_components, + &*server.world, + &server.index.as_index_ref(), + ); sys::subscription::initialize_region_subscription(server.state.ecs(), entity); } diff --git a/server/src/lib.rs b/server/src/lib.rs index 5622c5dd9e..b2e8cddbef 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -343,7 +343,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/state_ext.rs b/server/src/state_ext.rs index 90432f4bfa..eea380aeff 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -97,7 +97,13 @@ pub trait StateExt { fn initialize_character_data(&mut self, entity: EcsEntity, character_id: CharacterId); /// 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); + fn update_character_data( + &mut self, + entity: EcsEntity, + components: PersistedComponents, + world: &world::World, + index: &world::IndexRef, + ); /// Iterates over registered clients and send each `ServerMsg` fn send_chat(&self, msg: comp::UnresolvedChatMsg); fn notify_players(&self, msg: ServerGeneral); @@ -480,7 +486,13 @@ impl StateExt for State { } } - fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents) { + fn update_character_data( + &mut self, + entity: EcsEntity, + components: PersistedComponents, + world: &world::World, + index: &world::IndexRef, + ) { let (body, stats, skill_set, inventory, waypoint) = components; if let Some(player_uid) = self.read_component_copied::(entity) { @@ -525,8 +537,13 @@ impl StateExt for State { ); if let Some(waypoint) = waypoint { + // Avoid spawning the player in terrain by finding the highest solid point in + // their waypoint's column to spawn them at + let spawn_pos = + world.find_accessible_pos(*index, waypoint.get_pos().xy().as_::(), false); + 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::Pos(spawn_pos)); self.write_component_ignore_entity_dead(entity, comp::Vel(Vec3::zero())); self.write_component_ignore_entity_dead(entity, comp::ForceUpdate); } diff --git a/world/src/lib.rs b/world/src/lib.rs index dd7a5b9708..b59600521b 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,9 +189,13 @@ 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 @@ -199,9 +205,19 @@ impl World { 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); + 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| pos + Vec3::unit_z() * z_diff) + .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() From c417e8c5b96048ca5207081721c8151cc0228021 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Fri, 16 Jul 2021 15:42:50 -0400 Subject: [PATCH 2/3] Defer repositioning until after chunks are generated normally, to reduce latency and avoid duplicate work. --- common/src/comp/location.rs | 7 +++++ common/src/comp/mod.rs | 2 +- common/src/terrain/mod.rs | 42 ++++++++++++++++++++++++++++ common/state/src/state.rs | 1 + server/src/events/entity_creation.rs | 9 ++---- server/src/state_ext.rs | 24 +++------------- server/src/sys/terrain.rs | 26 ++++++++++++++--- world/src/lib.rs | 36 +----------------------- 8 files changed, 81 insertions(+), 66 deletions(-) diff --git a/common/src/comp/location.rs b/common/src/comp/location.rs index 7ecef35163..bd2ebb678a 100644 --- a/common/src/comp/location.rs +++ b/common/src/comp/location.rs @@ -45,3 +45,10 @@ impl Component for WaypointArea { impl Default for WaypointArea { fn default() -> Self { Self(5.0) } } + +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub struct RepositionOnChunkLoad; + +impl Component for RepositionOnChunkLoad { + type Storage = IdvStorage; +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 2242ad236e..10da4be975 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -80,7 +80,7 @@ pub use self::{ slot, Inventory, InventoryUpdate, InventoryUpdateEvent, }, last::Last, - location::{Waypoint, WaypointArea}, + location::{RepositionOnChunkLoad, Waypoint, WaypointArea}, misc::Object, ori::Ori, phys::{ 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/common/state/src/state.rs b/common/state/src/state.rs index 4bf741311b..5c8d26ea7c 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -182,6 +182,7 @@ impl State { ecs.register::>(); ecs.register::(); ecs.register::(); + ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index b9fd5bee70..8112123c96 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -41,12 +41,9 @@ pub fn handle_loaded_character_data( Option, ), ) { - server.state.update_character_data( - entity, - loaded_components, - &*server.world, - &server.index.as_index_ref(), - ); + server + .state + .update_character_data(entity, loaded_components); sys::subscription::initialize_region_subscription(server.state.ecs(), entity); } diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index eea380aeff..4a20594fd6 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -97,13 +97,7 @@ pub trait StateExt { fn initialize_character_data(&mut self, entity: EcsEntity, character_id: CharacterId); /// 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, - world: &world::World, - index: &world::IndexRef, - ); + fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents); /// Iterates over registered clients and send each `ServerMsg` fn send_chat(&self, msg: comp::UnresolvedChatMsg); fn notify_players(&self, msg: ServerGeneral); @@ -486,13 +480,7 @@ impl StateExt for State { } } - fn update_character_data( - &mut self, - entity: EcsEntity, - components: PersistedComponents, - world: &world::World, - index: &world::IndexRef, - ) { + fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents) { let (body, stats, skill_set, inventory, waypoint) = components; if let Some(player_uid) = self.read_component_copied::(entity) { @@ -537,13 +525,9 @@ impl StateExt for State { ); if let Some(waypoint) = waypoint { - // Avoid spawning the player in terrain by finding the highest solid point in - // their waypoint's column to spawn them at - let spawn_pos = - world.find_accessible_pos(*index, waypoint.get_pos().xy().as_::(), false); - + self.write_component_ignore_entity_dead(entity, comp::RepositionOnChunkLoad); self.write_component_ignore_entity_dead(entity, waypoint); - self.write_component_ignore_entity_dead(entity, comp::Pos(spawn_pos)); + self.write_component_ignore_entity_dead(entity, comp::Pos(waypoint.get_pos())); self.write_component_ignore_entity_dead(entity, comp::Vel(Vec3::zero())); self.write_component_ignore_entity_dead(entity, comp::ForceUpdate); } diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 77afd1f2b1..bb8e7b1b60 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -3,7 +3,7 @@ use crate::{ presence::Presence, rtsim::RtSim, settings::Settings, SpawnPoint, Tick, }; use common::{ - comp::{self, agent, bird_medium, Alignment, BehaviorCapability, Pos}, + comp::{self, agent, bird_medium, Alignment, BehaviorCapability, Pos, RepositionOnChunkLoad}, event::{EventBus, ServerEvent}, generation::{get_npc_name, EntityInfo}, npc::NPC_NAMES, @@ -14,7 +14,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 +93,11 @@ 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>, ); const NAME: &'static str = "terrain"; @@ -114,9 +116,11 @@ impl<'a> System<'a> for Sys { mut terrain, mut terrain_changes, mut rtsim, - positions, + mut positions, presences, clients, + entities, + mut reposition_on_load, ): Self::SystemData, ) { let mut server_emitter = server_event_bus.emitter(); @@ -306,6 +310,20 @@ impl<'a> System<'a> for Sys { } } + for (entity, pos) in (&entities, &mut positions).join() { + if reposition_on_load.get(entity).is_some() { + // If the player is in the new chunk and 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)) = new_chunks.iter().find(|(key, _)| *key == chunk_pos) { + pos.0 = chunk + .find_accessible_pos(pos.0.xy().as_::(), false) + .as_::(); + 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 b59600521b..29531e8b8b 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -202,42 +202,8 @@ impl World { 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, - 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)); - 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)] From a4d6f0f3c1b520102d7c93a9cdee155095af69ae Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Fri, 16 Jul 2021 17:33:31 -0400 Subject: [PATCH 3/3] Address MR 2612 review comments. --- common/src/comp/location.rs | 7 ------- common/src/comp/mod.rs | 2 +- common/state/src/state.rs | 1 - server/src/lib.rs | 3 ++- server/src/presence.rs | 9 ++++++++- server/src/state_ext.rs | 10 +++++++--- server/src/sys/terrain.rs | 39 ++++++++++++++++++++++++------------- 7 files changed, 43 insertions(+), 28 deletions(-) diff --git a/common/src/comp/location.rs b/common/src/comp/location.rs index bd2ebb678a..7ecef35163 100644 --- a/common/src/comp/location.rs +++ b/common/src/comp/location.rs @@ -45,10 +45,3 @@ impl Component for WaypointArea { impl Default for WaypointArea { fn default() -> Self { Self(5.0) } } - -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] -pub struct RepositionOnChunkLoad; - -impl Component for RepositionOnChunkLoad { - type Storage = IdvStorage; -} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 10da4be975..2242ad236e 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -80,7 +80,7 @@ pub use self::{ slot, Inventory, InventoryUpdate, InventoryUpdateEvent, }, last::Last, - location::{RepositionOnChunkLoad, Waypoint, WaypointArea}, + location::{Waypoint, WaypointArea}, misc::Object, ori::Ori, phys::{ diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 5c8d26ea7c..4bf741311b 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -182,7 +182,6 @@ impl State { ecs.register::>(); ecs.register::(); ecs.register::(); - ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/server/src/lib.rs b/server/src/lib.rs index b2e8cddbef..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; 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 4a20594fd6..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,7 +529,7 @@ impl StateExt for State { ); if let Some(waypoint) = waypoint { - self.write_component_ignore_entity_dead(entity, comp::RepositionOnChunkLoad); + 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 bb8e7b1b60..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, RepositionOnChunkLoad}, + comp::{self, agent, bird_medium, Alignment, BehaviorCapability, ForceUpdate, Pos}, event::{EventBus, ServerEvent}, generation::{get_npc_name, EntityInfo}, npc::NPC_NAMES, @@ -98,6 +103,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Client>, Entities<'a>, WriteStorage<'a, RepositionOnChunkLoad>, + WriteStorage<'a, ForceUpdate>, ); const NAME: &'static str = "terrain"; @@ -121,6 +127,7 @@ impl<'a> System<'a> for Sys { clients, entities, mut reposition_on_load, + mut force_update, ): Self::SystemData, ) { let mut server_emitter = server_event_bus.emitter(); @@ -310,19 +317,23 @@ impl<'a> System<'a> for Sys { } } - for (entity, pos) in (&entities, &mut positions).join() { - if reposition_on_load.get(entity).is_some() { - // If the player is in the new chunk and 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)) = new_chunks.iter().find(|(key, _)| *key == chunk_pos) { - pos.0 = chunk - .find_accessible_pos(pos.0.xy().as_::(), false) - .as_::(); - reposition_on_load.remove(entity); - } + 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};