diff --git a/chat-cli/src/main.rs b/chat-cli/src/main.rs index 00d609ab7a..975dec4e2b 100644 --- a/chat-cli/src/main.rs +++ b/chat-cli/src/main.rs @@ -65,7 +65,7 @@ fn main() { client.send_chat(msg) } - let events = match client.tick(comp::Controller::default(), clock.get_last_delta()) { + let events = match client.tick(comp::ControllerInputs::default(), clock.get_last_delta()) { Ok(events) => events, Err(err) => { error!("Error: {:?}", err); diff --git a/common/src/event.rs b/common/src/event.rs index 8bc938d5e0..c76bc59b16 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -53,6 +53,13 @@ pub enum ServerEvent { body: comp::Body, main: Option, }, + CreateNpc { + pos: comp::Pos, + stats: comp::Stats, + body: comp::Body, + agent: comp::Agent, + scale: comp::Scale, + }, ClientDisconnect(EcsEntity), ChunkRequest(EcsEntity, Vec2), ChatCmd(EcsEntity, String), diff --git a/common/src/region.rs b/common/src/region.rs index 1bbcc72eeb..de8e5a526d 100644 --- a/common/src/region.rs +++ b/common/src/region.rs @@ -2,7 +2,7 @@ use crate::comp::{Pos, Vel}; use hashbrown::{hash_map::DefaultHashBuilder, HashSet}; use hibitset::BitSetLike; use indexmap::IndexMap; -use specs::{BitSet, Entities, Entity as EcsEntity, Join, ReadStorage}; +use specs::{BitSet, Entities, Join, ReadStorage}; use vek::*; pub enum Event { diff --git a/server/src/chunk_generator.rs b/server/src/chunk_generator.rs new file mode 100644 index 0000000000..c8cd76d45e --- /dev/null +++ b/server/src/chunk_generator.rs @@ -0,0 +1,70 @@ +use common::terrain::TerrainChunk; +use crossbeam::channel; +use hashbrown::{hash_map::Entry, HashMap}; +use specs::Entity as EcsEntity; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use vek::*; +use world::{ChunkSupplement, World}; + +type ChunkGenResult = ( + Vec2, + Result<(TerrainChunk, ChunkSupplement), EcsEntity>, +); + +pub struct ChunkGenerator { + chunk_tx: channel::Sender, + chunk_rx: channel::Receiver, + pending_chunks: HashMap, Arc>, +} +impl ChunkGenerator { + pub fn new() -> Self { + let (chunk_tx, chunk_rx) = channel::unbounded(); + Self { + chunk_tx, + chunk_rx, + pending_chunks: HashMap::new(), + } + } + pub fn generate_chunk( + &mut self, + entity: EcsEntity, + key: Vec2, + thread_pool: &mut uvth::ThreadPool, + world: Arc, + ) { + let v = if let Entry::Vacant(v) = self.pending_chunks.entry(key) { + v + } else { + return; + }; + let cancel = Arc::new(AtomicBool::new(false)); + v.insert(Arc::clone(&cancel)); + let chunk_tx = self.chunk_tx.clone(); + thread_pool.execute(move || { + let payload = world + .generate_chunk(key, || cancel.load(Ordering::Relaxed)) + .map_err(|_| entity); + let _ = chunk_tx.send((key, payload)); + }); + } + pub fn recv_new_chunk(&mut self) -> Option { + if let Ok((key, res)) = self.chunk_rx.try_recv() { + self.pending_chunks.remove(&key); + // TODO: do anything else if res is an Err? + Some((key, res)) + } else { + None + } + } + pub fn pending_chunks<'a>(&'a self) -> impl Iterator> + 'a { + self.pending_chunks.keys().copied() + } + pub fn cancel_if_pending(&mut self, key: Vec2) { + if let Some(cancel) = self.pending_chunks.remove(&key) { + cancel.store(true, Ordering::Relaxed); + } + } +} diff --git a/server/src/cmd.rs b/server/src/cmd.rs index c77d64417b..7f57fcf463 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -2,7 +2,7 @@ //! To implement a new command, add an instance of `ChatCommand` to `CHAT_COMMANDS` //! and provide a handler function. -use crate::Server; +use crate::{Server, StateExt}; use chrono::{NaiveTime, Timelike}; use common::{ comp, @@ -430,6 +430,7 @@ fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &C let body = kind_to_body(id); server + .state .create_npc(pos, comp::Stats::new(get_npc_name(id), None), body) .with(comp::Vel(vel)) .with(comp::MountState::Unmounted) diff --git a/server/src/lib.rs b/server/src/lib.rs index 3fe9088795..9d38fbb20a 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -2,6 +2,7 @@ #![feature(drain_filter)] pub mod auth_provider; +pub mod chunk_generator; pub mod client; pub mod cmd; pub mod error; @@ -15,6 +16,7 @@ pub use crate::{error::Error, input::Input, settings::ServerSettings}; use crate::{ auth_provider::AuthProvider, + chunk_generator::ChunkGenerator, client::{Client, RegionSubscription}, cmd::CHAT_COMMANDS, }; @@ -25,26 +27,21 @@ use common::{ msg::{ClientMsg, ClientState, ServerError, ServerInfo, ServerMsg}, net::PostOffice, state::{BlockChange, State, TimeOfDay, Uid}, - terrain::{block::Block, TerrainChunk, TerrainChunkSize, TerrainGrid}, + terrain::{block::Block, TerrainChunkSize, TerrainGrid}, vol::{ReadVol, RectVolSize, Vox}, }; -use crossbeam::channel; -use hashbrown::{hash_map::Entry, HashMap}; use log::{debug, trace}; use metrics::ServerMetrics; use rand::Rng; use specs::{join::Join, world::EntityBuilder as EcsEntityBuilder, Builder, Entity as EcsEntity}; use std::{ i32, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, + sync::Arc, time::{Duration, Instant}, }; use uvth::{ThreadPool, ThreadPoolBuilder}; use vek::*; -use world::{ChunkSupplement, World}; +use world::World; const CLIENT_TIMEOUT: f64 = 20.0; // Seconds @@ -76,15 +73,6 @@ pub struct Server { postoffice: PostOffice, thread_pool: ThreadPool, - chunk_tx: channel::Sender<( - Vec2, - Result<(TerrainChunk, ChunkSupplement), EcsEntity>, - )>, - chunk_rx: channel::Receiver<( - Vec2, - Result<(TerrainChunk, ChunkSupplement), EcsEntity>, - )>, - pending_chunks: HashMap, Arc>, server_info: ServerInfo, metrics: ServerMetrics, @@ -95,8 +83,6 @@ pub struct Server { impl Server { /// Create a new `Server` pub fn new(settings: ServerSettings) -> Result { - let (chunk_tx, chunk_rx) = channel::unbounded(); - let mut state = State::default(); state .ecs_mut() @@ -107,6 +93,7 @@ impl Server { // TODO: anything but this state.ecs_mut().add_resource(AuthProvider::new()); state.ecs_mut().add_resource(Tick(0)); + state.ecs_mut().add_resource(ChunkGenerator::new()); state.ecs_mut().register::(); state.ecs_mut().register::(); @@ -122,9 +109,6 @@ impl Server { thread_pool: ThreadPoolBuilder::new() .name("veloren-worker".into()) .build(), - chunk_tx, - chunk_rx, - pending_chunks: HashMap::new(), server_info: ServerInfo { name: settings.server_name.clone(), @@ -161,26 +145,6 @@ impl Server { &self.world } - /// Build a non-player character. - pub fn create_npc( - &mut self, - pos: comp::Pos, - stats: comp::Stats, - body: comp::Body, - ) -> EcsEntityBuilder { - self.state - .ecs_mut() - .create_entity_synced() - .with(pos) - .with(comp::Vel(Vec3::zero())) - .with(comp::Ori(Vec3::unit_y())) - .with(comp::Controller::default()) - .with(body) - .with(stats) - .with(comp::Gravity(1.0)) - .with(comp::CharacterState::default()) - } - /// Build a static object entity pub fn create_object( &mut self, @@ -694,6 +658,20 @@ impl Server { Self::initialize_region_subscription(state, entity); } + ServerEvent::CreateNpc { + pos, + stats, + body, + agent, + scale, + } => { + state + .create_npc(pos, stats, body) + .with(agent) + .with(scale) + .build(); + } + ServerEvent::ClientDisconnect(entity) => { if let Err(err) = state.ecs_mut().delete_entity_synced(entity) { debug!("Failed to delete disconnected client: {:?}", err); @@ -785,202 +763,14 @@ impl Server { let before_tick_5 = Instant::now(); // 5) Fetch any generated `TerrainChunk`s and insert them into the terrain. - // Also, send the chunk data to anybody that is close by. - 'insert_terrain_chunks: while let Ok((key, res)) = self.chunk_rx.try_recv() { - let (chunk, supplement) = match res { - Ok((chunk, supplement)) => (chunk, supplement), - Err(entity) => { - self.notify_client( - entity, - ServerMsg::TerrainChunkUpdate { - key, - chunk: Err(()), - }, - ); - continue 'insert_terrain_chunks; - } - }; - // Send the chunk to all nearby players. - for (view_distance, pos, client) in ( - &self.state.ecs().read_storage::(), - &self.state.ecs().read_storage::(), - &mut self.state.ecs().write_storage::(), - ) - .join() - .filter_map(|(player, pos, client)| { - player.view_distance.map(|vd| (vd, pos, client)) - }) - { - let chunk_pos = self.state.terrain().pos_key(pos.0.map(|e| e as i32)); - let adjusted_dist_sqr = (Vec2::from(chunk_pos) - Vec2::from(key)) - .map(|e: i32| (e.abs() as u32).checked_sub(2).unwrap_or(0)) - .magnitude_squared(); - - if adjusted_dist_sqr <= view_distance.pow(2) { - client.notify(ServerMsg::TerrainChunkUpdate { - key, - chunk: Ok(Box::new(chunk.clone())), - }); - } - } - - self.state.insert_chunk(key, chunk); - self.pending_chunks.remove(&key); - - // Handle chunk supplement - for npc in supplement.npcs { - let (mut stats, mut body) = if rand::random() { - let stats = comp::Stats::new( - "Humanoid".to_string(), - Some(comp::Item::Tool { - kind: comp::item::Tool::Sword, - power: 5, - stamina: 0, - strength: 0, - dexterity: 0, - intelligence: 0, - }), - ); - let body = comp::Body::Humanoid(comp::humanoid::Body::random()); - (stats, body) - } else { - let stats = comp::Stats::new("Wolf".to_string(), None); - let body = comp::Body::QuadrupedMedium(comp::quadruped_medium::Body::random()); - (stats, body) - }; - let mut scale = 1.0; - - // TODO: Remove this and implement scaling or level depending on stuff like species instead - stats.level.set_level(rand::thread_rng().gen_range(1, 3)); - - if npc.boss { - if rand::random::() < 0.8 { - stats = comp::Stats::new( - "Humanoid".to_string(), - Some(comp::Item::Tool { - kind: comp::item::Tool::Sword, - power: 10, - stamina: 0, - strength: 0, - dexterity: 0, - intelligence: 0, - }), - ); - body = comp::Body::Humanoid(comp::humanoid::Body::random()); - } - stats.level.set_level(rand::thread_rng().gen_range(10, 50)); - scale = 2.5 + rand::random::(); - } - - stats.update_max_hp(); - stats - .health - .set_to(stats.health.maximum(), comp::HealthSource::Revive); - self.create_npc(comp::Pos(npc.pos), stats, body) - .with(comp::Agent::enemy()) - .with(comp::Scale(scale)) - .build(); - } - } - - fn chunk_in_vd( - player_pos: Vec3, - chunk_pos: Vec2, - terrain: &TerrainGrid, - vd: u32, - ) -> bool { - let player_chunk_pos = terrain.pos_key(player_pos.map(|e| e as i32)); - - let adjusted_dist_sqr = Vec2::from(player_chunk_pos - chunk_pos) - .map(|e: i32| (e.abs() as u32).checked_sub(2).unwrap_or(0)) - .magnitude_squared(); - - adjusted_dist_sqr <= vd.pow(2) - } - - // Remove chunks that are too far from players. - let mut chunks_to_remove = Vec::new(); - self.state - .terrain() - .iter() - .map(|(k, _)| k) - .chain(self.pending_chunks.keys().cloned()) - .for_each(|chunk_key| { - let mut should_drop = true; - - // For each player with a position, calculate the distance. - for (player, pos) in ( - &self.state.ecs().read_storage::(), - &self.state.ecs().read_storage::(), - ) - .join() - { - if player - .view_distance - .map(|vd| chunk_in_vd(pos.0, chunk_key, &self.state.terrain(), vd)) - .unwrap_or(false) - { - should_drop = false; - break; - } - } - - if should_drop { - chunks_to_remove.push(chunk_key); - } - }); - for key in chunks_to_remove { - self.state.remove_chunk(key); - if let Some(cancel) = self.pending_chunks.remove(&key) { - cancel.store(true, Ordering::Relaxed); - } - } let before_tick_6 = Instant::now(); // 6) Synchronise clients with the new state of the world. - self.sync_clients(); - - // Sync changed chunks - 'chunk: for chunk_key in &self.state.terrain_changes().modified_chunks { - let terrain = self.state.terrain(); - - for (player, pos, client) in ( - &self.state.ecs().read_storage::(), - &self.state.ecs().read_storage::(), - &mut self.state.ecs().write_storage::(), - ) - .join() - { - if player - .view_distance - .map(|vd| chunk_in_vd(pos.0, *chunk_key, &terrain, vd)) - .unwrap_or(false) - { - client.notify(ServerMsg::TerrainChunkUpdate { - key: *chunk_key, - chunk: Ok(Box::new(match self.state.terrain().get_key(*chunk_key) { - Some(chunk) => chunk.clone(), - None => break 'chunk, - })), - }); - } - } - } - - // Sync changed blocks - let msg = - ServerMsg::TerrainBlockUpdates(self.state.terrain_changes().modified_blocks.clone()); - for (player, client) in ( - &self.state.ecs().read_storage::(), - &mut self.state.ecs().write_storage::(), - ) - .join() - { - // TODO: Don't send all changed blocks to all clients - if player.view_distance.is_some() { - client.notify(msg.clone()); - } - } + // TODO: Remove sphynx + // Sync 'logical' state using Sphynx. + let sync_package = self.state.ecs_mut().next_sync_package(); + self.state + .notify_registered_clients(ServerMsg::EcsSync(sync_package)); // Remove NPCs that are outside the view distances of all players // This is done by removing NPCs in unloaded chunks @@ -1001,6 +791,7 @@ impl Server { } let before_tick_7 = Instant::now(); + // TODO: Update metrics now that a lot of processing has been moved to ecs systems // 7) Update Metrics self.metrics .tick_time @@ -1172,14 +963,6 @@ impl Server { } } - /// Sync client states with the most up to date information. - fn sync_clients(&mut self) { - let sync_package = self.state.ecs_mut().next_sync_package(); - // Sync 'logical' state using Sphynx. - self.state - .notify_registered_clients(ServerMsg::EcsSync(sync_package)); - } - pub fn notify_client(&self, entity: EcsEntity, msg: ServerMsg) { if let Some(client) = self.state.ecs().write_storage::().get_mut(entity) { client.notify(msg) @@ -1187,21 +970,10 @@ impl Server { } pub fn generate_chunk(&mut self, entity: EcsEntity, key: Vec2) { - let v = if let Entry::Vacant(v) = self.pending_chunks.entry(key) { - v - } else { - return; - }; - let cancel = Arc::new(AtomicBool::new(false)); - v.insert(Arc::clone(&cancel)); - let chunk_tx = self.chunk_tx.clone(); - let world = self.world.clone(); - self.thread_pool.execute(move || { - let payload = world - .generate_chunk(key, || cancel.load(Ordering::Relaxed)) - .map_err(|_| entity); - let _ = chunk_tx.send((key, payload)); - }); + self.state + .ecs() + .write_resource::() + .generate_chunk(entity, key, &mut self.thread_pool, self.world.clone()); } fn process_chat_cmd(&mut self, entity: EcsEntity, cmd: String) { @@ -1246,6 +1018,12 @@ trait StateExt { fn give_item(&mut self, entity: EcsEntity, item: comp::Item) -> bool; fn apply_effect(&mut self, entity: EcsEntity, effect: Effect); fn notify_registered_clients(&self, msg: ServerMsg); + fn create_npc( + &mut self, + pos: comp::Pos, + stats: comp::Stats, + body: comp::Body, + ) -> EcsEntityBuilder; } impl StateExt for State { @@ -1279,6 +1057,25 @@ impl StateExt for State { } } + /// Build a non-player character. + fn create_npc( + &mut self, + pos: comp::Pos, + stats: comp::Stats, + body: comp::Body, + ) -> EcsEntityBuilder { + self.ecs_mut() + .create_entity_synced() + .with(pos) + .with(comp::Vel(Vec3::zero())) + .with(comp::Ori(Vec3::unit_y())) + .with(comp::Controller::default()) + .with(body) + .with(stats) + .with(comp::Gravity(1.0)) + .with(comp::CharacterState::default()) + } + fn notify_registered_clients(&self, msg: ServerMsg) { for client in (&mut self.ecs().write_storage::()) .join() diff --git a/server/src/sys/sync.rs b/server/src/sys/entity_sync.rs similarity index 100% rename from server/src/sys/sync.rs rename to server/src/sys/entity_sync.rs diff --git a/server/src/sys/mod.rs b/server/src/sys/mod.rs index 048f4457c0..7f7b2ace42 100644 --- a/server/src/sys/mod.rs +++ b/server/src/sys/mod.rs @@ -1,19 +1,23 @@ -pub mod sync; -//pub mod sync_chunk; +pub mod entity_sync; pub mod message; pub mod subscription; +pub mod terrain; +pub mod terrain_sync; use specs::DispatcherBuilder; // System names -const SYNC_SYS: &str = "server_sync_sys"; +const ENTITY_SYNC_SYS: &str = "server_entity_sync_sys"; const SUBSCRIPTION_SYS: &str = "server_subscription_sys"; +const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys"; +const TERRAIN_SYS: &str = "server_terrain_sys"; const MESSAGE_SYS: &str = "server_message_sys"; //const SYNC_CHUNK_SYS: &str = "server_sync_chunk_sys"; pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { dispatch_builder.add(subscription::Sys, SUBSCRIPTION_SYS, &[]); - dispatch_builder.add(sync::Sys, SYNC_SYS, &[SUBSCRIPTION_SYS]); + dispatch_builder.add(entity_sync::Sys, ENTITY_SYNC_SYS, &[SUBSCRIPTION_SYS]); + dispatch_builder.add(terrain_sync::Sys, TERRAIN_SYS, &[]); + dispatch_builder.add(terrain::Sys, TERRAIN_SYNC_SYS, &[TERRAIN_SYS]); dispatch_builder.add(message::Sys, MESSAGE_SYS, &[]); - //dispatch_builder.add(sync_chunk::Sys, SYNC_CHUNKR_SYS, &[]); } diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs new file mode 100644 index 0000000000..6de11fc2bb --- /dev/null +++ b/server/src/sys/terrain.rs @@ -0,0 +1,199 @@ +use crate::{chunk_generator::ChunkGenerator, client::Client, Tick}; +use common::{ + comp::{self, Player, Pos}, + event::{EventBus, ServerEvent}, + msg::ServerMsg, + state::TerrainChanges, + terrain::TerrainGrid, +}; +use rand::Rng; +use specs::{Join, Read, ReadStorage, System, Write, WriteExpect, WriteStorage}; +use std::sync::Arc; +use vek::*; + +/// This system will handle loading generated chunks and unloading uneeded chunks. +/// 1. Inserts newly generated chunks into the TerrainGrid +/// 2. Sends new chunks to neaby clients +/// 3. Handles the chunk's supplement (e.g. npcs) +/// 4. Removes chunks outside the range of players +pub struct Sys; +impl<'a> System<'a> for Sys { + type SystemData = ( + Read<'a, EventBus>, + Read<'a, Tick>, + WriteExpect<'a, ChunkGenerator>, + WriteExpect<'a, TerrainGrid>, + Write<'a, TerrainChanges>, + ReadStorage<'a, Pos>, + ReadStorage<'a, Player>, + WriteStorage<'a, Client>, + ); + + fn run( + &mut self, + ( + server_emitter, + tick, + mut chunk_generator, + mut terrain, + mut terrain_changes, + positions, + players, + mut clients, + ): Self::SystemData, + ) { + // Fetch any generated `TerrainChunk`s and insert them into the terrain. + // Also, send the chunk data to anybody that is close by. + 'insert_terrain_chunks: while let Some((key, res)) = chunk_generator.recv_new_chunk() { + let (chunk, supplement) = match res { + Ok((chunk, supplement)) => (chunk, supplement), + Err(entity) => { + if let Some(client) = clients.get_mut(entity) { + client.notify(ServerMsg::TerrainChunkUpdate { + key, + chunk: Err(()), + }); + } + continue 'insert_terrain_chunks; + } + }; + // Send the chunk to all nearby players. + for (view_distance, pos, client) in (&players, &positions, &mut clients) + .join() + .filter_map(|(player, pos, client)| { + player.view_distance.map(|vd| (vd, pos, client)) + }) + { + let chunk_pos = terrain.pos_key(pos.0.map(|e| e as i32)); + let adjusted_dist_sqr = (Vec2::from(chunk_pos) - Vec2::from(key)) + .map(|e: i32| (e.abs() as u32).checked_sub(2).unwrap_or(0)) + .magnitude_squared(); + + if adjusted_dist_sqr <= view_distance.pow(2) { + client.notify(ServerMsg::TerrainChunkUpdate { + key, + chunk: Ok(Box::new(chunk.clone())), + }); + } + } + + // TODO: code duplication for chunk insertion between here and state.rs + // Insert the chunk into terrain changes + if terrain.insert(key, Arc::new(chunk)).is_some() { + terrain_changes.modified_chunks.insert(key); + } else { + terrain_changes.new_chunks.insert(key); + } + + // Handle chunk supplement + for npc in supplement.npcs { + let (mut stats, mut body) = if rand::random() { + let stats = comp::Stats::new( + "Humanoid".to_string(), + Some(comp::Item::Tool { + kind: comp::item::Tool::Sword, + power: 5, + stamina: 0, + strength: 0, + dexterity: 0, + intelligence: 0, + }), + ); + let body = comp::Body::Humanoid(comp::humanoid::Body::random()); + (stats, body) + } else { + let stats = comp::Stats::new("Wolf".to_string(), None); + let body = comp::Body::QuadrupedMedium(comp::quadruped_medium::Body::random()); + (stats, body) + }; + let mut scale = 1.0; + + // TODO: Remove this and implement scaling or level depending on stuff like species instead + stats.level.set_level(rand::thread_rng().gen_range(1, 3)); + + if npc.boss { + if rand::random::() < 0.8 { + stats = comp::Stats::new( + "Humanoid".to_string(), + Some(comp::Item::Tool { + kind: comp::item::Tool::Sword, + power: 10, + stamina: 0, + strength: 0, + dexterity: 0, + intelligence: 0, + }), + ); + body = comp::Body::Humanoid(comp::humanoid::Body::random()); + } + stats.level.set_level(rand::thread_rng().gen_range(10, 50)); + scale = 2.5 + rand::random::(); + } + + stats.update_max_hp(); + stats + .health + .set_to(stats.health.maximum(), comp::HealthSource::Revive); + server_emitter.emit(ServerEvent::CreateNpc { + pos: Pos(npc.pos), + stats, + body, + agent: comp::Agent::enemy(), + scale: comp::Scale(scale), + }) + } + } + + // Remove chunks that are too far from players. + let mut chunks_to_remove = Vec::new(); + terrain + .iter() + .map(|(k, _)| k) + // Don't every chunk every tick (spread over 16 ticks) + .filter(|k| k.x.abs() as u64 % 4 + k.y.abs() as u64 % 8 * 4 == tick.0 % 16) + // There shouldn't be to many pending chunks so we will just check them all + .chain(chunk_generator.pending_chunks()) + .for_each(|chunk_key| { + let mut should_drop = true; + + // For each player with a position, calculate the distance. + for (player, pos) in (&players, &positions).join() { + if player + .view_distance + .map(|vd| chunk_in_vd(pos.0, chunk_key, &terrain, vd)) + .unwrap_or(false) + { + should_drop = false; + break; + } + } + + if should_drop { + chunks_to_remove.push(chunk_key); + } + }); + for key in chunks_to_remove { + // TODO: code duplication for chunk insertion between here and state.rs + if terrain.remove(key).is_some() { + terrain_changes.removed_chunks.insert(key); + } + + chunk_generator.cancel_if_pending(key); + } + } +} + +pub fn chunk_in_vd( + player_pos: Vec3, + chunk_pos: Vec2, + terrain: &TerrainGrid, + vd: u32, +) -> bool { + let player_chunk_pos = terrain.pos_key(player_pos.map(|e| e as i32)); + + let adjusted_dist_sqr = Vec2::from(player_chunk_pos - chunk_pos) + .map(|e: i32| (e.abs() as u32).checked_sub(2).unwrap_or(0)) + .magnitude_squared(); + + adjusted_dist_sqr <= vd.pow(2) +} diff --git a/server/src/sys/terrain_sync.rs b/server/src/sys/terrain_sync.rs new file mode 100644 index 0000000000..84e46be1b0 --- /dev/null +++ b/server/src/sys/terrain_sync.rs @@ -0,0 +1,57 @@ +use crate::client::Client; +use common::{ + comp::{Player, Pos}, + msg::ServerMsg, + state::TerrainChanges, + terrain::TerrainGrid, +}; +use specs::{Join, Read, ReadExpect, ReadStorage, System, WriteStorage}; + +/// This system will handle loading generated chunks and unloading uneeded chunks. +/// 1. Inserts newly generated chunks into the TerrainGrid +/// 2. Sends new chunks to neaby clients +/// 3. Handles the chunk's supplement (e.g. npcs) +/// 4. Removes chunks outside the range of players +pub struct Sys; +impl<'a> System<'a> for Sys { + type SystemData = ( + ReadExpect<'a, TerrainGrid>, + Read<'a, TerrainChanges>, + ReadStorage<'a, Pos>, + ReadStorage<'a, Player>, + WriteStorage<'a, Client>, + ); + + fn run( + &mut self, + (terrain, terrain_changes, positions, players, mut clients): Self::SystemData, + ) { + // Sync changed chunks + 'chunk: for chunk_key in &terrain_changes.modified_chunks { + for (player, pos, client) in (&players, &positions, &mut clients).join() { + if player + .view_distance + .map(|vd| super::terrain::chunk_in_vd(pos.0, *chunk_key, &terrain, vd)) + .unwrap_or(false) + { + client.notify(ServerMsg::TerrainChunkUpdate { + key: *chunk_key, + chunk: Ok(Box::new(match terrain.get_key(*chunk_key) { + Some(chunk) => chunk.clone(), + None => break 'chunk, + })), + }); + } + } + } + + // TODO: Don't send all changed blocks to all clients + // Sync changed blocks + let msg = ServerMsg::TerrainBlockUpdates(terrain_changes.modified_blocks.clone()); + for (player, client) in (&players, &mut clients).join() { + if player.view_distance.is_some() { + client.notify(msg.clone()); + } + } + } +}