diff --git a/common/src/state.rs b/common/src/state.rs index 584426d79c..a42f370c02 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -5,7 +5,8 @@ use crate::{ comp, msg::{EcsCompPacket, EcsResPacket}, sys, - terrain::{TerrainChunk, TerrainMap}, + terrain::{TerrainChunk, TerrainMap, Block}, + vol::WriteVol, }; use rayon::{ThreadPool, ThreadPoolBuilder}; use serde_derive::{Deserialize, Serialize}; @@ -15,7 +16,11 @@ use specs::{ Component, DispatcherBuilder, Entity as EcsEntity, }; use sphynx; -use std::{collections::HashSet, sync::Arc, time::Duration}; +use std::{ + collections::{HashSet, HashMap}, + sync::Arc, + time::Duration, +}; use vek::*; /// How much faster should an in-game day be compared to a real day? @@ -40,24 +45,48 @@ pub struct DeltaTime(pub f32); /// lag. Ideally, we'd avoid such a situation. const MAX_DELTA_TIME: f32 = 1.0; -pub struct Changes { +pub struct TerrainChange { + blocks: HashMap, Block>, +} + +impl Default for TerrainChange { + fn default() -> Self { + Self { + blocks: HashMap::new(), + } + } +} + +impl TerrainChange { + pub fn set(&mut self, pos: Vec3, block: Block) { + self.blocks.insert(pos, block); + } + + pub fn clear(&mut self) { + self.blocks.clear(); + } +} + +pub struct ChunkChanges { pub new_chunks: HashSet>, - pub changed_chunks: HashSet>, + pub modified_chunks: HashSet>, pub removed_chunks: HashSet>, } -impl Changes { - pub fn default() -> Self { +impl Default for ChunkChanges { + fn default() -> Self { Self { new_chunks: HashSet::new(), - changed_chunks: HashSet::new(), + modified_chunks: HashSet::new(), removed_chunks: HashSet::new(), } } +} - pub fn cleanup(&mut self) { +impl ChunkChanges { + pub fn clear(&mut self) { self.new_chunks.clear(); - self.changed_chunks.clear(); + self.modified_chunks.clear(); self.removed_chunks.clear(); } } @@ -68,7 +97,6 @@ pub struct State { ecs: sphynx::World, // Avoid lifetime annotation by storing a thread pool instead of the whole dispatcher thread_pool: Arc, - changes: Changes, } impl State { @@ -77,7 +105,6 @@ impl State { Self { ecs: sphynx::World::new(specs::World::new(), Self::setup_sphynx_world), thread_pool: Arc::new(ThreadPoolBuilder::new().build().unwrap()), - changes: Changes::default(), } } @@ -92,7 +119,6 @@ impl State { state_package, ), thread_pool: Arc::new(ThreadPoolBuilder::new().build().unwrap()), - changes: Changes::default(), } } @@ -133,6 +159,8 @@ impl State { ecs.add_resource(Time(0.0)); ecs.add_resource(DeltaTime(0.0)); ecs.add_resource(TerrainMap::new().unwrap()); + ecs.add_resource(TerrainChange::default()); + ecs.add_resource(ChunkChanges::default()); } /// Register a component with the state's ECS. @@ -171,8 +199,8 @@ impl State { /// Get a reference to the `Changes` structure of the state. This contains /// information about state that has changed since the last game tick. - pub fn changes(&self) -> &Changes { - &self.changes + pub fn chunk_changes(&self) -> Fetch { + self.ecs.read_resource() } /// Get the current in-game time of day. @@ -196,12 +224,12 @@ impl State { /// Get a reference to this state's terrain. pub fn terrain(&self) -> Fetch { - self.ecs.read_resource::() + self.ecs.read_resource() } /// Get a writable reference to this state's terrain. pub fn terrain_mut(&self) -> FetchMut { - self.ecs.write_resource::() + self.ecs.write_resource() } /// Removes every chunk of the terrain. @@ -225,9 +253,13 @@ impl State { .insert(key, Arc::new(chunk)) .is_some() { - self.changes.changed_chunks.insert(key); + self.ecs + .write_resource::() + .modified_chunks.insert(key); } else { - self.changes.new_chunks.insert(key); + self.ecs + .write_resource::() + .new_chunks.insert(key); } } @@ -239,7 +271,9 @@ impl State { .remove(key) .is_some() { - self.changes.removed_chunks.insert(key); + self.ecs + .write_resource::() + .removed_chunks.insert(key); } } @@ -261,11 +295,29 @@ impl State { dispatch_builder.build().dispatch(&self.ecs.res); self.ecs.maintain(); + + // Apply terrain changes + let mut terrain = self.ecs.write_resource::(); + let mut chunk_changes = self.ecs.write_resource::(); + self.ecs + .write_resource::() + .blocks + .drain() + .for_each(|(pos, block)| if terrain.set(pos, block).is_ok() { + chunk_changes.modified_chunks.insert(terrain.pos_key(pos)); + } else { + warn!("Tried to modify block outside of terrain at {:?}", pos); + }); } /// Clean up the state after a tick. pub fn cleanup(&mut self) { // Clean up data structures from the last tick. - self.changes.cleanup(); + self.ecs + .write_resource::() + .clear(); + self.ecs + .write_resource::() + .clear(); } } diff --git a/common/src/terrain/chonk.rs b/common/src/terrain/chonk.rs index 50bf080449..5343eedf3f 100644 --- a/common/src/terrain/chonk.rs +++ b/common/src/terrain/chonk.rs @@ -1,6 +1,6 @@ use super::{block::Block, TerrainChunkMeta, TerrainChunkSize}; use crate::{ - vol::{BaseVol, ReadVol, WriteVol}, + vol::{BaseVol, ReadVol, WriteVol, VolSize}, volumes::chunk::{Chunk, ChunkErr}, }; use fxhash::FxHashMap; @@ -185,21 +185,24 @@ impl WriteVol for Chonk { self.sub_chunks[sub_chunk_idx] = SubChunk::Hash(*cblock, map); Ok(()) } - SubChunk::Hash(cblock, _map) if block == *cblock => Ok(()), - SubChunk::Hash(_cblock, map) if map.len() < 4096 => { + SubChunk::Hash(cblock, map) if block == *cblock => { + map.remove(&rpos.map(|e| e as u8)); + Ok(()) + }, + SubChunk::Hash(_cblock, map) if map.len() <= 4096 => { map.insert(rpos.map(|e| e as u8), block); Ok(()) } SubChunk::Hash(cblock, map) => { let mut new_chunk = Chunk::filled(*cblock, ()); - new_chunk.set(rpos, block).unwrap(); // Can't fail (I hope) - for (map_pos, map_block) in map { new_chunk .set(map_pos.map(|e| e as i32), *map_block) .unwrap(); // Can't fail (I hope!) } + new_chunk.set(rpos, block).unwrap(); // Can't fail (I hope) + self.sub_chunks[sub_chunk_idx] = SubChunk::Heterogeneous(new_chunk); Ok(()) } @@ -222,11 +225,22 @@ impl WriteVol for Chonk { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SubChunkSize; + +impl VolSize for SubChunkSize { + const SIZE: Vec3 = Vec3 { + x: TerrainChunkSize::SIZE.x, + y: TerrainChunkSize::SIZE.y, + z: SUB_CHUNK_HEIGHT, + }; +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum SubChunk { Homogeneous(Block), Hash(Block, FxHashMap, Block>), - Heterogeneous(Chunk), + Heterogeneous(Chunk), } impl SubChunk { diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 06d6642d0e..44881fdc83 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -7,7 +7,9 @@ use common::{ comp, msg::ServerMsg, npc::{get_npc_name, NpcKind}, - state::TimeOfDay, + state::{TimeOfDay, TerrainChange}, + terrain::Block, + vol::Vox, }; use specs::{Builder, Entity as EcsEntity, Join}; use vek::*; @@ -104,6 +106,18 @@ lazy_static! { "/players : Show the online players list", handle_players, ), + ChatCommand::new( + "solid", + "{}", + "/solid : Make the blocks around you solid", + handle_solid, + ), + ChatCommand::new( + "empty", + "{}", + "/empty : Make the blocks around you empty", + handle_empty, + ), ChatCommand::new( "help", "", "/help: Display this message", handle_help) ]; @@ -122,7 +136,7 @@ fn handle_jump(server: &mut Server, entity: EcsEntity, args: String, action: &Ch } None => server.clients.notify( entity, - ServerMsg::Chat(String::from("Command 'jump' invalid in current state.")), + ServerMsg::Chat(String::from("You have no position!")), ), } } @@ -242,7 +256,7 @@ fn handle_tp(server: &mut Server, entity: EcsEntity, args: String, action: &Chat None => { server.clients.notify( entity, - ServerMsg::Chat(format!("You don't have any position!")), + ServerMsg::Chat(format!("You have no position!")), ); } } @@ -315,6 +329,58 @@ fn handle_players(server: &mut Server, entity: EcsEntity, _args: String, _action } } +fn handle_solid(server: &mut Server, entity: EcsEntity, args: String, action: &ChatCommand) { + match server.state.read_component_cloned::(entity) { + Some(current_pos) => { + let mut terrain_change = server + .state + .ecs() + .write_resource::(); + + for i in -1..2 { + for j in -1..2 { + for k in -1..2 { + terrain_change.set( + current_pos.0.map(|e| e.floor() as i32) + Vec3::new(i, j, k), + Block::new(1, Rgb::broadcast(255)), + ); + } + } + } + } + None => server.clients.notify( + entity, + ServerMsg::Chat(String::from("You have no position!")), + ), + } +} + +fn handle_empty(server: &mut Server, entity: EcsEntity, args: String, action: &ChatCommand) { + match server.state.read_component_cloned::(entity) { + Some(current_pos) => { + let mut terrain_change = server + .state + .ecs() + .write_resource::(); + + for i in -1..2 { + for j in -1..2 { + for k in -2..1 { + terrain_change.set( + current_pos.0.map(|e| e.floor() as i32) + Vec3::new(i, j, k), + Block::empty(), + ); + } + } + } + } + None => server.clients.notify( + entity, + ServerMsg::Chat(String::from("You have no position!")), + ), + } +} + fn handle_help(server: &mut Server, entity: EcsEntity, _args: String, _action: &ChatCommand) { for cmd in CHAT_COMMANDS.iter() { server diff --git a/server/src/lib.rs b/server/src/lib.rs index 9912863eb4..0becf22dee 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -17,7 +17,7 @@ use common::{ msg::{ClientMsg, ClientState, RequestStateError, ServerInfo, ServerMsg}, net::PostOffice, state::{State, Uid}, - terrain::{TerrainChunk, TerrainChunkSize}, + terrain::{TerrainChunk, TerrainMap, TerrainChunkSize}, vol::VolSize, }; use log::{debug, warn}; @@ -246,9 +246,19 @@ impl Server { self.pending_chunks.remove(&key); } + fn chunk_in_vd(player_pos: Vec3, chunk_pos: Vec2, terrain: &TerrainMap, 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().for_each(|(key, _)| { + self.state.terrain().iter().for_each(|(chunk_key, _)| { let mut should_drop = true; // For each player with a position, calculate the distance. @@ -258,15 +268,9 @@ impl Server { ) .join() { - let chunk_pos = self.state.terrain().pos_key(pos.0.map(|e| e as i32)); - - let adjusted_dist_sqr = Vec2::from(chunk_pos - key) - .map(|e: i32| (e.abs() as u32).checked_sub(2).unwrap_or(0)) - .magnitude_squared(); - if player .view_distance - .map(|vd| adjusted_dist_sqr <= vd.pow(2)) + .map(|vd| chunk_in_vd(pos.0, chunk_key, &self.state.terrain(), vd)) .unwrap_or(false) { should_drop = false; @@ -275,7 +279,7 @@ impl Server { } if should_drop { - chunks_to_remove.push(key); + chunks_to_remove.push(chunk_key); } }); for key in chunks_to_remove { @@ -285,6 +289,36 @@ impl Server { // 6) Synchronise clients with the new state of the world. self.sync_clients(); + // Sync changed chunks + 'chunk: for chunk_key in &self.state.chunk_changes().modified_chunks { + let terrain = self.state.terrain(); + + for (entity, player, pos) in ( + &self.state.ecs().entities(), + &self.state.ecs().read_storage::(), + &self.state.ecs().read_storage::(), + ) + .join() + { + if player + .view_distance + .map(|vd| chunk_in_vd(pos.0, *chunk_key, &terrain, vd)) + .unwrap_or(false) + { + self.clients.notify( + entity, + ServerMsg::TerrainChunkUpdate { + key: *chunk_key, + chunk: Box::new(match self.state.terrain().get_key(*chunk_key) { + Some(chunk) => chunk.clone(), + None => break 'chunk, + }), + }, + ); + } + } + } + // 7) Finish the tick, pass control back to the frontend. // Cleanup diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index f9beaeec07..ab5c840005 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -88,12 +88,18 @@ impl Terrain { let current_tick = client.get_tick(); // Add any recently created or changed chunks to the list of chunks to be meshed. - for pos in client + for (modified, pos) in client .state() - .changes() - .new_chunks + .chunk_changes() + .modified_chunks .iter() - .chain(client.state().changes().changed_chunks.iter()) + .map(|c| (true, c)) + .chain(client + .state() + .chunk_changes() + .new_chunks + .iter() + .map(|c| (false, c))) { // TODO: ANOTHER PROBLEM HERE! // What happens if the block on the edge of a chunk gets modified? We need to spawn @@ -103,7 +109,7 @@ impl Terrain { for j in -1..2 { let pos = pos + Vec2::new(i, j); - if !self.chunks.contains_key(&pos) { + if !self.chunks.contains_key(&pos) || modified { let mut neighbours = true; for i in -1..2 { for j in -1..2 { @@ -116,7 +122,10 @@ impl Terrain { } if neighbours { - self.mesh_todo.entry(pos).or_insert(ChunkMeshState { + if modified { + println!("MODIFIED: {:?}", pos); + } + self.mesh_todo.insert(pos, ChunkMeshState { pos, started_tick: current_tick, active_worker: false, @@ -127,7 +136,7 @@ impl Terrain { } } // Remove any models for chunks that have been recently removed. - for pos in &client.state().changes().removed_chunks { + for pos in &client.state().chunk_changes().removed_chunks { self.chunks.remove(pos); self.mesh_todo.remove(pos); } @@ -177,6 +186,8 @@ impl Terrain { let send = self.mesh_send_tmp.clone(); let pos = todo.pos; + println!("SPAWN: {:?}", todo.pos); + // Queue the worker thread. client.thread_pool().execute(move || { let _ = send.send(mesh_worker( @@ -197,7 +208,7 @@ impl Terrain { match self.mesh_todo.get(&response.pos) { // It's the mesh we want, insert the newly finished model into the terrain model // data structure (convert the mesh to a model first of course). - Some(todo) if response.started_tick == todo.started_tick => { + Some(todo) if response.started_tick <= todo.started_tick => { self.chunks.insert( response.pos, TerrainChunk { @@ -218,6 +229,8 @@ impl Terrain { z_bounds: response.z_bounds, }, ); + + self.mesh_todo.remove(&response.pos); } // Chunk must have been removed, or it was spawned on an old tick. Drop the mesh // since it's either out of date or no longer needed.