Move terrain management and syncing into server side ecs systems

This commit is contained in:
Imbris 2019-10-20 01:19:50 -04:00 committed by Imbris
parent 2703c8afe1
commit 8f81b69a25
10 changed files with 401 additions and 266 deletions

View File

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

View File

@ -53,6 +53,13 @@ pub enum ServerEvent {
body: comp::Body,
main: Option<comp::Item>,
},
CreateNpc {
pos: comp::Pos,
stats: comp::Stats,
body: comp::Body,
agent: comp::Agent,
scale: comp::Scale,
},
ClientDisconnect(EcsEntity),
ChunkRequest(EcsEntity, Vec2<i32>),
ChatCmd(EcsEntity, String),

View File

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

View File

@ -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<i32>,
Result<(TerrainChunk, ChunkSupplement), EcsEntity>,
);
pub struct ChunkGenerator {
chunk_tx: channel::Sender<ChunkGenResult>,
chunk_rx: channel::Receiver<ChunkGenResult>,
pending_chunks: HashMap<Vec2<i32>, Arc<AtomicBool>>,
}
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<i32>,
thread_pool: &mut uvth::ThreadPool,
world: Arc<World>,
) {
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<ChunkGenResult> {
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<Item = Vec2<i32>> + 'a {
self.pending_chunks.keys().copied()
}
pub fn cancel_if_pending(&mut self, key: Vec2<i32>) {
if let Some(cancel) = self.pending_chunks.remove(&key) {
cancel.store(true, Ordering::Relaxed);
}
}
}

View File

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

View File

@ -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<ServerMsg, ClientMsg>,
thread_pool: ThreadPool,
chunk_tx: channel::Sender<(
Vec2<i32>,
Result<(TerrainChunk, ChunkSupplement), EcsEntity>,
)>,
chunk_rx: channel::Receiver<(
Vec2<i32>,
Result<(TerrainChunk, ChunkSupplement), EcsEntity>,
)>,
pending_chunks: HashMap<Vec2<i32>, Arc<AtomicBool>>,
server_info: ServerInfo,
metrics: ServerMetrics,
@ -95,8 +83,6 @@ pub struct Server {
impl Server {
/// Create a new `Server`
pub fn new(settings: ServerSettings) -> Result<Self, Error> {
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::<RegionSubscription>();
state.ecs_mut().register::<Client>();
@ -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::<comp::Player>(),
&self.state.ecs().read_storage::<comp::Pos>(),
&mut self.state.ecs().write_storage::<Client>(),
)
.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::<f32>() < 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::<f32>();
}
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<f32>,
chunk_pos: Vec2<i32>,
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::<comp::Player>(),
&self.state.ecs().read_storage::<comp::Pos>(),
)
.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::<comp::Player>(),
&self.state.ecs().read_storage::<comp::Pos>(),
&mut self.state.ecs().write_storage::<Client>(),
)
.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::<comp::Player>(),
&mut self.state.ecs().write_storage::<Client>(),
)
.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::<Client>().get_mut(entity) {
client.notify(msg)
@ -1187,21 +970,10 @@ impl Server {
}
pub fn generate_chunk(&mut self, entity: EcsEntity, key: Vec2<i32>) {
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::<ChunkGenerator>()
.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::<Client>())
.join()

View File

@ -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, &[]);
}

199
server/src/sys/terrain.rs Normal file
View File

@ -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<ServerEvent>>,
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::<f32>() < 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::<f32>();
}
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<f32>,
chunk_pos: Vec2<i32>,
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)
}

View File

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