mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Move terrain management and syncing into server side ecs systems
This commit is contained in:
@ -65,7 +65,7 @@ fn main() {
|
|||||||
client.send_chat(msg)
|
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,
|
Ok(events) => events,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error: {:?}", err);
|
error!("Error: {:?}", err);
|
||||||
|
@ -53,6 +53,13 @@ pub enum ServerEvent {
|
|||||||
body: comp::Body,
|
body: comp::Body,
|
||||||
main: Option<comp::Item>,
|
main: Option<comp::Item>,
|
||||||
},
|
},
|
||||||
|
CreateNpc {
|
||||||
|
pos: comp::Pos,
|
||||||
|
stats: comp::Stats,
|
||||||
|
body: comp::Body,
|
||||||
|
agent: comp::Agent,
|
||||||
|
scale: comp::Scale,
|
||||||
|
},
|
||||||
ClientDisconnect(EcsEntity),
|
ClientDisconnect(EcsEntity),
|
||||||
ChunkRequest(EcsEntity, Vec2<i32>),
|
ChunkRequest(EcsEntity, Vec2<i32>),
|
||||||
ChatCmd(EcsEntity, String),
|
ChatCmd(EcsEntity, String),
|
||||||
|
@ -2,7 +2,7 @@ use crate::comp::{Pos, Vel};
|
|||||||
use hashbrown::{hash_map::DefaultHashBuilder, HashSet};
|
use hashbrown::{hash_map::DefaultHashBuilder, HashSet};
|
||||||
use hibitset::BitSetLike;
|
use hibitset::BitSetLike;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use specs::{BitSet, Entities, Entity as EcsEntity, Join, ReadStorage};
|
use specs::{BitSet, Entities, Join, ReadStorage};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
|
70
server/src/chunk_generator.rs
Normal file
70
server/src/chunk_generator.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
//! To implement a new command, add an instance of `ChatCommand` to `CHAT_COMMANDS`
|
//! To implement a new command, add an instance of `ChatCommand` to `CHAT_COMMANDS`
|
||||||
//! and provide a handler function.
|
//! and provide a handler function.
|
||||||
|
|
||||||
use crate::Server;
|
use crate::{Server, StateExt};
|
||||||
use chrono::{NaiveTime, Timelike};
|
use chrono::{NaiveTime, Timelike};
|
||||||
use common::{
|
use common::{
|
||||||
comp,
|
comp,
|
||||||
@ -430,6 +430,7 @@ fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &C
|
|||||||
|
|
||||||
let body = kind_to_body(id);
|
let body = kind_to_body(id);
|
||||||
server
|
server
|
||||||
|
.state
|
||||||
.create_npc(pos, comp::Stats::new(get_npc_name(id), None), body)
|
.create_npc(pos, comp::Stats::new(get_npc_name(id), None), body)
|
||||||
.with(comp::Vel(vel))
|
.with(comp::Vel(vel))
|
||||||
.with(comp::MountState::Unmounted)
|
.with(comp::MountState::Unmounted)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
#![feature(drain_filter)]
|
#![feature(drain_filter)]
|
||||||
|
|
||||||
pub mod auth_provider;
|
pub mod auth_provider;
|
||||||
|
pub mod chunk_generator;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod cmd;
|
pub mod cmd;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
@ -15,6 +16,7 @@ pub use crate::{error::Error, input::Input, settings::ServerSettings};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth_provider::AuthProvider,
|
auth_provider::AuthProvider,
|
||||||
|
chunk_generator::ChunkGenerator,
|
||||||
client::{Client, RegionSubscription},
|
client::{Client, RegionSubscription},
|
||||||
cmd::CHAT_COMMANDS,
|
cmd::CHAT_COMMANDS,
|
||||||
};
|
};
|
||||||
@ -25,26 +27,21 @@ use common::{
|
|||||||
msg::{ClientMsg, ClientState, ServerError, ServerInfo, ServerMsg},
|
msg::{ClientMsg, ClientState, ServerError, ServerInfo, ServerMsg},
|
||||||
net::PostOffice,
|
net::PostOffice,
|
||||||
state::{BlockChange, State, TimeOfDay, Uid},
|
state::{BlockChange, State, TimeOfDay, Uid},
|
||||||
terrain::{block::Block, TerrainChunk, TerrainChunkSize, TerrainGrid},
|
terrain::{block::Block, TerrainChunkSize, TerrainGrid},
|
||||||
vol::{ReadVol, RectVolSize, Vox},
|
vol::{ReadVol, RectVolSize, Vox},
|
||||||
};
|
};
|
||||||
use crossbeam::channel;
|
|
||||||
use hashbrown::{hash_map::Entry, HashMap};
|
|
||||||
use log::{debug, trace};
|
use log::{debug, trace};
|
||||||
use metrics::ServerMetrics;
|
use metrics::ServerMetrics;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use specs::{join::Join, world::EntityBuilder as EcsEntityBuilder, Builder, Entity as EcsEntity};
|
use specs::{join::Join, world::EntityBuilder as EcsEntityBuilder, Builder, Entity as EcsEntity};
|
||||||
use std::{
|
use std::{
|
||||||
i32,
|
i32,
|
||||||
sync::{
|
sync::Arc,
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Arc,
|
|
||||||
},
|
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use uvth::{ThreadPool, ThreadPoolBuilder};
|
use uvth::{ThreadPool, ThreadPoolBuilder};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
use world::{ChunkSupplement, World};
|
use world::World;
|
||||||
|
|
||||||
const CLIENT_TIMEOUT: f64 = 20.0; // Seconds
|
const CLIENT_TIMEOUT: f64 = 20.0; // Seconds
|
||||||
|
|
||||||
@ -76,15 +73,6 @@ pub struct Server {
|
|||||||
postoffice: PostOffice<ServerMsg, ClientMsg>,
|
postoffice: PostOffice<ServerMsg, ClientMsg>,
|
||||||
|
|
||||||
thread_pool: ThreadPool,
|
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,
|
server_info: ServerInfo,
|
||||||
metrics: ServerMetrics,
|
metrics: ServerMetrics,
|
||||||
@ -95,8 +83,6 @@ pub struct Server {
|
|||||||
impl Server {
|
impl Server {
|
||||||
/// Create a new `Server`
|
/// Create a new `Server`
|
||||||
pub fn new(settings: ServerSettings) -> Result<Self, Error> {
|
pub fn new(settings: ServerSettings) -> Result<Self, Error> {
|
||||||
let (chunk_tx, chunk_rx) = channel::unbounded();
|
|
||||||
|
|
||||||
let mut state = State::default();
|
let mut state = State::default();
|
||||||
state
|
state
|
||||||
.ecs_mut()
|
.ecs_mut()
|
||||||
@ -107,6 +93,7 @@ impl Server {
|
|||||||
// TODO: anything but this
|
// TODO: anything but this
|
||||||
state.ecs_mut().add_resource(AuthProvider::new());
|
state.ecs_mut().add_resource(AuthProvider::new());
|
||||||
state.ecs_mut().add_resource(Tick(0));
|
state.ecs_mut().add_resource(Tick(0));
|
||||||
|
state.ecs_mut().add_resource(ChunkGenerator::new());
|
||||||
state.ecs_mut().register::<RegionSubscription>();
|
state.ecs_mut().register::<RegionSubscription>();
|
||||||
state.ecs_mut().register::<Client>();
|
state.ecs_mut().register::<Client>();
|
||||||
|
|
||||||
@ -122,9 +109,6 @@ impl Server {
|
|||||||
thread_pool: ThreadPoolBuilder::new()
|
thread_pool: ThreadPoolBuilder::new()
|
||||||
.name("veloren-worker".into())
|
.name("veloren-worker".into())
|
||||||
.build(),
|
.build(),
|
||||||
chunk_tx,
|
|
||||||
chunk_rx,
|
|
||||||
pending_chunks: HashMap::new(),
|
|
||||||
|
|
||||||
server_info: ServerInfo {
|
server_info: ServerInfo {
|
||||||
name: settings.server_name.clone(),
|
name: settings.server_name.clone(),
|
||||||
@ -161,26 +145,6 @@ impl Server {
|
|||||||
&self.world
|
&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
|
/// Build a static object entity
|
||||||
pub fn create_object(
|
pub fn create_object(
|
||||||
&mut self,
|
&mut self,
|
||||||
@ -694,6 +658,20 @@ impl Server {
|
|||||||
Self::initialize_region_subscription(state, entity);
|
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) => {
|
ServerEvent::ClientDisconnect(entity) => {
|
||||||
if let Err(err) = state.ecs_mut().delete_entity_synced(entity) {
|
if let Err(err) = state.ecs_mut().delete_entity_synced(entity) {
|
||||||
debug!("Failed to delete disconnected client: {:?}", err);
|
debug!("Failed to delete disconnected client: {:?}", err);
|
||||||
@ -785,202 +763,14 @@ impl Server {
|
|||||||
|
|
||||||
let before_tick_5 = Instant::now();
|
let before_tick_5 = Instant::now();
|
||||||
// 5) Fetch any generated `TerrainChunk`s and insert them into the terrain.
|
// 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();
|
let before_tick_6 = Instant::now();
|
||||||
// 6) Synchronise clients with the new state of the world.
|
// 6) Synchronise clients with the new state of the world.
|
||||||
self.sync_clients();
|
// TODO: Remove sphynx
|
||||||
|
// Sync 'logical' state using Sphynx.
|
||||||
// Sync changed chunks
|
let sync_package = self.state.ecs_mut().next_sync_package();
|
||||||
'chunk: for chunk_key in &self.state.terrain_changes().modified_chunks {
|
self.state
|
||||||
let terrain = self.state.terrain();
|
.notify_registered_clients(ServerMsg::EcsSync(sync_package));
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove NPCs that are outside the view distances of all players
|
// Remove NPCs that are outside the view distances of all players
|
||||||
// This is done by removing NPCs in unloaded chunks
|
// This is done by removing NPCs in unloaded chunks
|
||||||
@ -1001,6 +791,7 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let before_tick_7 = Instant::now();
|
let before_tick_7 = Instant::now();
|
||||||
|
// TODO: Update metrics now that a lot of processing has been moved to ecs systems
|
||||||
// 7) Update Metrics
|
// 7) Update Metrics
|
||||||
self.metrics
|
self.metrics
|
||||||
.tick_time
|
.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) {
|
pub fn notify_client(&self, entity: EcsEntity, msg: ServerMsg) {
|
||||||
if let Some(client) = self.state.ecs().write_storage::<Client>().get_mut(entity) {
|
if let Some(client) = self.state.ecs().write_storage::<Client>().get_mut(entity) {
|
||||||
client.notify(msg)
|
client.notify(msg)
|
||||||
@ -1187,21 +970,10 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_chunk(&mut self, entity: EcsEntity, key: Vec2<i32>) {
|
pub fn generate_chunk(&mut self, entity: EcsEntity, key: Vec2<i32>) {
|
||||||
let v = if let Entry::Vacant(v) = self.pending_chunks.entry(key) {
|
self.state
|
||||||
v
|
.ecs()
|
||||||
} else {
|
.write_resource::<ChunkGenerator>()
|
||||||
return;
|
.generate_chunk(entity, key, &mut self.thread_pool, self.world.clone());
|
||||||
};
|
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_chat_cmd(&mut self, entity: EcsEntity, cmd: String) {
|
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 give_item(&mut self, entity: EcsEntity, item: comp::Item) -> bool;
|
||||||
fn apply_effect(&mut self, entity: EcsEntity, effect: Effect);
|
fn apply_effect(&mut self, entity: EcsEntity, effect: Effect);
|
||||||
fn notify_registered_clients(&self, msg: ServerMsg);
|
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 {
|
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) {
|
fn notify_registered_clients(&self, msg: ServerMsg) {
|
||||||
for client in (&mut self.ecs().write_storage::<Client>())
|
for client in (&mut self.ecs().write_storage::<Client>())
|
||||||
.join()
|
.join()
|
||||||
|
@ -1,19 +1,23 @@
|
|||||||
pub mod sync;
|
pub mod entity_sync;
|
||||||
//pub mod sync_chunk;
|
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod subscription;
|
pub mod subscription;
|
||||||
|
pub mod terrain;
|
||||||
|
pub mod terrain_sync;
|
||||||
|
|
||||||
use specs::DispatcherBuilder;
|
use specs::DispatcherBuilder;
|
||||||
|
|
||||||
// System names
|
// 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 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 MESSAGE_SYS: &str = "server_message_sys";
|
||||||
//const SYNC_CHUNK_SYS: &str = "server_sync_chunk_sys";
|
//const SYNC_CHUNK_SYS: &str = "server_sync_chunk_sys";
|
||||||
|
|
||||||
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
||||||
dispatch_builder.add(subscription::Sys, SUBSCRIPTION_SYS, &[]);
|
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(message::Sys, MESSAGE_SYS, &[]);
|
||||||
//dispatch_builder.add(sync_chunk::Sys, SYNC_CHUNKR_SYS, &[]);
|
|
||||||
}
|
}
|
||||||
|
199
server/src/sys/terrain.rs
Normal file
199
server/src/sys/terrain.rs
Normal 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)
|
||||||
|
}
|
57
server/src/sys/terrain_sync.rs
Normal file
57
server/src/sys/terrain_sync.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user