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:
parent
2703c8afe1
commit
8f81b69a25
@ -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);
|
||||
|
@ -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),
|
||||
|
@ -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 {
|
||||
|
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`
|
||||
//! 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)
|
||||
|
@ -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()
|
||||
|
@ -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
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user