diff --git a/client/src/lib.rs b/client/src/lib.rs index 6cb7658c64..932add96d5 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -10,8 +10,8 @@ pub use specs::{join::Join, saveload::Marker, Entity as EcsEntity, ReadStorage, use common::{ comp::{self, ControlEvent, Controller, ControllerInputs, InventoryManip}, msg::{ - validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, RequestStateError, - ServerError, ServerInfo, ServerMsg, MAX_BYTES_CHAT_MSG, + validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, PlayerListUpdate, + RequestStateError, ServerError, ServerInfo, ServerMsg, MAX_BYTES_CHAT_MSG, }, net::PostBox, state::State, @@ -52,6 +52,7 @@ pub struct Client { thread_pool: ThreadPool, pub server_info: ServerInfo, pub world_map: Arc, + pub player_list: HashMap, postbox: PostBox, @@ -71,7 +72,6 @@ pub struct Client { impl Client { /// Create a new `Client`. - #[allow(dead_code)] pub fn new>(addr: A, view_distance: Option) -> Result { let client_state = ClientState::Connected; let mut postbox = PostBox::to(addr)?; @@ -137,6 +137,7 @@ impl Client { thread_pool, server_info, world_map, + player_list: HashMap::new(), postbox, @@ -154,7 +155,6 @@ impl Client { }) } - #[allow(dead_code)] pub fn with_thread_pool(mut self, thread_pool: ThreadPool) -> Self { self.thread_pool = thread_pool; self @@ -282,7 +282,6 @@ impl Client { } /// Send a chat message to the server. - #[allow(dead_code)] pub fn send_chat(&mut self, msg: String) { match validate_chat_msg(&msg) { Ok(()) => self.postbox.send_message(ClientMsg::chat(msg)), @@ -294,7 +293,6 @@ impl Client { } /// Remove all cached terrain - #[allow(dead_code)] pub fn clear_terrain(&mut self) { self.state.clear_terrain(); self.pending_chunks.clear(); @@ -316,7 +314,6 @@ impl Client { } /// Execute a single client tick, handle input and update the game state by the given duration. - #[allow(dead_code)] pub fn tick(&mut self, inputs: ControllerInputs, dt: Duration) -> Result, Error> { // This tick function is the centre of the Veloren universe. Most client-side things are // managed from here, and as such it's important that it stays organised. Please consult @@ -389,6 +386,10 @@ impl Client { // Remove chunks that are too far from the player. let mut chunks_to_remove = Vec::new(); self.state.terrain().iter().for_each(|(key, _)| { + // Subtract 2 from the offset before computing squared magnitude + // 1 for the chunks needed bordering other chunks for meshing + // 1 as a buffer so that if the player moves back in that direction the chunks + // don't need to be reloaded if (chunk_pos - key) .map(|e: i32| (e.abs() as u32).checked_sub(2).unwrap_or(0)) .magnitude_squared() @@ -492,7 +493,6 @@ impl Client { } /// Clean up the client after a tick. - #[allow(dead_code)] pub fn cleanup(&mut self) { // Cleanup the local state self.state.cleanup(); @@ -531,6 +531,27 @@ impl Client { }, ServerMsg::Shutdown => return Err(Error::ServerShutdown), ServerMsg::InitialSync { .. } => return Err(Error::ServerWentMad), + ServerMsg::PlayerListUpdate(PlayerListUpdate::Init(list)) => { + self.player_list = list + } + ServerMsg::PlayerListUpdate(PlayerListUpdate::Add(uid, name)) => { + if let Some(old_name) = self.player_list.insert(uid, name.clone()) { + warn!("Received msg to insert {} with uid {} into the player list but there was already an entry for {} with the same uid that was overwritten!", name, uid, old_name); + } + } + ServerMsg::PlayerListUpdate(PlayerListUpdate::Remove(uid)) => { + if self.player_list.remove(&uid).is_none() { + warn!("Received msg to remove uid {} from the player list by they weren't in the list!", uid); + } + } + ServerMsg::PlayerListUpdate(PlayerListUpdate::Alias(uid, new_name)) => { + if let Some(name) = self.player_list.get_mut(&uid) { + *name = new_name; + } else { + warn!("Received msg to alias player with uid {} to {} but this uid is not in the player list", uid, new_name); + } + } + ServerMsg::Ping => self.postbox.send_message(ClientMsg::Pong), ServerMsg::Pong => { self.last_server_pong = Instant::now(); @@ -602,9 +623,11 @@ impl Client { } self.pending_chunks.remove(&key); } - ServerMsg::TerrainBlockUpdates(mut blocks) => blocks - .drain() - .for_each(|(pos, block)| self.state.set_block(pos, block)), + ServerMsg::TerrainBlockUpdates(mut blocks) => { + blocks.drain().for_each(|(pos, block)| { + self.state.set_block(pos, block); + }); + } ServerMsg::StateAnswer(Ok(state)) => { self.client_state = state; } @@ -636,24 +659,20 @@ impl Client { } /// Get the player's entity. - #[allow(dead_code)] pub fn entity(&self) -> EcsEntity { self.entity } /// Get the client state - #[allow(dead_code)] pub fn get_client_state(&self) -> ClientState { self.client_state } /// Get the current tick number. - #[allow(dead_code)] pub fn get_tick(&self) -> u64 { self.tick } - #[allow(dead_code)] pub fn get_ping_ms(&self) -> f64 { self.last_ping_delta * 1000.0 } @@ -661,19 +680,16 @@ impl Client { /// Get a reference to the client's worker thread pool. This pool should be used for any /// computationally expensive operations that run outside of the main thread (i.e., threads that /// block on I/O operations are exempt). - #[allow(dead_code)] pub fn thread_pool(&self) -> &ThreadPool { &self.thread_pool } /// Get a reference to the client's game state. - #[allow(dead_code)] pub fn state(&self) -> &State { &self.state } /// Get a mutable reference to the client's game state. - #[allow(dead_code)] pub fn state_mut(&mut self) -> &mut State { &mut self.state } diff --git a/common/src/msg/client.rs b/common/src/msg/client.rs index b8e26f95cc..2b031db3f1 100644 --- a/common/src/msg/client.rs +++ b/common/src/msg/client.rs @@ -23,7 +23,7 @@ pub enum ClientMsg { Ping, Pong, ChatMsg { - chat_type: ChatType, + chat_type: ChatType, // This is unused afaik, TODO: remove message: String, }, PlayerPhysics { diff --git a/common/src/msg/mod.rs b/common/src/msg/mod.rs index 12f72addf5..13fc0eeb64 100644 --- a/common/src/msg/mod.rs +++ b/common/src/msg/mod.rs @@ -5,7 +5,7 @@ pub mod server; // Reexports pub use self::client::ClientMsg; pub use self::ecs_packet::EcsCompPacket; -pub use self::server::{RequestStateError, ServerError, ServerInfo, ServerMsg}; +pub use self::server::{PlayerListUpdate, RequestStateError, ServerError, ServerInfo, ServerMsg}; #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum ClientState { diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index 7b922527d4..710d8a4dfa 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -23,6 +23,14 @@ pub struct ServerInfo { pub git_date: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PlayerListUpdate { + Init(HashMap), + Add(u64, String), + Remove(u64), + Alias(u64, String), +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ServerMsg { InitialSync { @@ -31,6 +39,7 @@ pub enum ServerMsg { time_of_day: state::TimeOfDay, // world_map: Vec2, /*, Vec)*/ }, + PlayerListUpdate(PlayerListUpdate), StateAnswer(Result), ForceState(ClientState), Ping, diff --git a/common/src/state.rs b/common/src/state.rs index 5e19b25c43..6966d3b1cc 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -326,17 +326,14 @@ impl State { // Apply terrain changes let mut terrain = self.ecs.write_resource::(); - self.ecs - .read_resource::() - .blocks - .iter() - .for_each(|(pos, block)| { - let _ = terrain.set(*pos, *block); - }); - self.ecs.write_resource::().modified_blocks = std::mem::replace( + let mut modified_blocks = std::mem::replace( &mut self.ecs.write_resource::().blocks, Default::default(), ); + // Apply block modifications + // Only include in `TerrainChanges` if successful + modified_blocks.retain(|pos, block| terrain.set(*pos, *block).is_ok()); + self.ecs.write_resource::().modified_blocks = modified_blocks; // Process local events let events = self.ecs.read_resource::>().recv_all(); diff --git a/common/src/volumes/vol_grid_2d.rs b/common/src/volumes/vol_grid_2d.rs index 0086bd4ee4..00e21a19aa 100644 --- a/common/src/volumes/vol_grid_2d.rs +++ b/common/src/volumes/vol_grid_2d.rs @@ -167,6 +167,8 @@ impl VolGrid2d { pub struct CachedVolGrid2d<'a, V: RectRasterableVol> { vol_grid_2d: &'a VolGrid2d, + // This can't be invalidated by mutations of the chunks hashmap since we hold an immutable + // reference to the `VolGrid2d` cache: Option<(Vec2, Arc)>, } impl<'a, V: RectRasterableVol> CachedVolGrid2d<'a, V> { @@ -178,9 +180,9 @@ impl<'a, V: RectRasterableVol> CachedVolGrid2d<'a, V> { } } impl<'a, V: RectRasterableVol + ReadVol> CachedVolGrid2d<'a, V> { - // Note: this may be invalidated by mutations of the chunks hashmap #[inline(always)] pub fn get(&mut self, pos: Vec3) -> Result<&V::Vox, VolGrid2dError> { + // Calculate chunk key from block pos let ck = VolGrid2d::::chunk_key(pos); let chunk = if self .cache @@ -188,13 +190,16 @@ impl<'a, V: RectRasterableVol + ReadVol> CachedVolGrid2d<'a, V> { .map(|(key, _)| *key == ck) .unwrap_or(false) { + // If the chunk with that key is in the cache use that &self.cache.as_ref().unwrap().1 } else { + // Otherwise retrieve from the hashmap let chunk = self .vol_grid_2d .chunks .get(&ck) .ok_or(VolGrid2dError::NoSuchChunk)?; + // Store most recently looked up chunk in the cache self.cache = Some((ck, chunk.clone())); chunk }; diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 36793f6e66..9f0cc61b46 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -8,11 +8,11 @@ use common::{ assets, comp, event::{EventBus, ServerEvent}, hierarchical::ChunkPath, - msg::ServerMsg, + msg::{PlayerListUpdate, ServerMsg}, npc::{get_npc_name, NpcKind}, pathfinding::WorldPath, state::TimeOfDay, - sync::WorldSyncExt, + sync::{Uid, WorldSyncExt}, terrain::{Block, BlockKind, TerrainChunkSize}, vol::RectVolSize, }; @@ -407,6 +407,19 @@ fn handle_alias(server: &mut Server, entity: EcsEntity, args: String, action: &C .write_storage::() .get_mut(entity) .map(|player| player.alias = alias); + + // Update name on client player lists + let ecs = server.state.ecs(); + if let (Some(uid), Some(player)) = ( + ecs.read_storage::().get(entity), + ecs.read_storage::().get(entity), + ) { + let msg = ServerMsg::PlayerListUpdate(PlayerListUpdate::Alias( + (*uid).into(), + player.alias.clone(), + )); + server.state.notify_registered_clients(msg); + } } else { server.notify_client(entity, ServerMsg::private(String::from(action.help_string))); } diff --git a/server/src/lib.rs b/server/src/lib.rs index 8804efe33f..24429bd6c7 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -25,7 +25,7 @@ use common::{ assets, comp, effect::Effect, event::{EventBus, ServerEvent}, - msg::{ClientMsg, ClientState, ServerError, ServerInfo, ServerMsg}, + msg::{ClientMsg, ClientState, PlayerListUpdate, ServerError, ServerInfo, ServerMsg}, net::PostOffice, state::{BlockChange, State, TimeOfDay}, sync::{Uid, WorldSyncExt}, @@ -762,6 +762,17 @@ impl Server { } ServerEvent::ClientDisconnect(entity) => { + // Tell other clients to remove from player list + if let (Some(uid), Some(_)) = ( + state.read_storage::().get(entity), + state.read_storage::().get(entity), + ) { + state.notify_registered_clients(ServerMsg::PlayerListUpdate( + PlayerListUpdate::Remove((*uid).into()), + )) + } + + // Delete client entity if let Err(err) = state.delete_entity_recorded(entity) { error!("Failed to delete disconnected client: {:?}", err); } diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs index 6537e4e01a..b7b47c0215 100644 --- a/server/src/sys/message.rs +++ b/server/src/sys/message.rs @@ -3,12 +3,16 @@ use crate::{auth_provider::AuthProvider, client::Client, CLIENT_TIMEOUT}; use common::{ comp::{Admin, Body, CanBuild, Controller, ForceUpdate, Ori, Player, Pos, Vel}, event::{EventBus, ServerEvent}, - msg::{validate_chat_msg, ChatMsgValidationError, MAX_BYTES_CHAT_MSG}, - msg::{ClientMsg, ClientState, RequestStateError, ServerMsg}, + msg::{ + validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, PlayerListUpdate, + RequestStateError, ServerMsg, MAX_BYTES_CHAT_MSG, + }, state::{BlockChange, Time}, + sync::Uid, terrain::{Block, TerrainGrid}, vol::Vox, }; +use hashbrown::HashMap; use specs::{ Entities, Join, Read, ReadExpect, ReadStorage, System, Write, WriteExpect, WriteStorage, }; @@ -22,6 +26,7 @@ impl<'a> System<'a> for Sys { Read<'a, Time>, ReadExpect<'a, TerrainGrid>, Write<'a, SysTimer>, + ReadStorage<'a, Uid>, ReadStorage<'a, Body>, ReadStorage<'a, CanBuild>, ReadStorage<'a, Admin>, @@ -44,6 +49,7 @@ impl<'a> System<'a> for Sys { time, terrain, mut timer, + uids, bodies, can_build, admins, @@ -64,6 +70,14 @@ impl<'a> System<'a> for Sys { let mut new_chat_msgs = Vec::new(); + // Player list to send new players. + let player_list = (&uids, &players) + .join() + .map(|(uid, player)| ((*uid).into(), player.alias.clone())) + .collect::>(); + // List of new players to update player lists of all clients. + let mut new_players = Vec::new(); + for (entity, client) in (&entities, &mut clients).join() { let mut disconnect = false; let new_msgs = client.postbox.new_messages(); @@ -127,10 +141,18 @@ impl<'a> System<'a> for Sys { } match client.client_state { ClientState::Connected => { + // Add Player component to this client let _ = players.insert(entity, player); // Tell the client its request was successful. client.allow_state(ClientState::Registered); + + // Send initial player list + client.notify(ServerMsg::PlayerListUpdate(PlayerListUpdate::Init( + player_list.clone(), + ))); + // Add to list to notify all clients of the new player + new_players.push(entity); } // Use RequestState instead (No need to send `player` again). _ => client.error_state(RequestStateError::Impossible), @@ -281,6 +303,20 @@ impl<'a> System<'a> for Sys { } } + // Handle new players. + // Tell all clients to add them to the player list. + for entity in new_players { + if let (Some(uid), Some(player)) = (uids.get(entity), players.get(entity)) { + let msg = ServerMsg::PlayerListUpdate(PlayerListUpdate::Add( + (*uid).into(), + player.alias.clone(), + )); + for client in (&mut clients).join().filter(|c| c.is_registered()) { + client.notify(msg.clone()) + } + } + } + // Handle new chat messages. for (entity, msg) in new_chat_msgs { match msg { diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index af9dee1c74..c9560c4b81 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -71,6 +71,9 @@ impl<'a> System<'a> for Sys { }) { let chunk_pos = terrain.pos_key(pos.0.map(|e| e as i32)); + // Subtract 2 from the offset before computing squared magnitude + // 1 since chunks need neighbors to be meshed + // 1 to act as a buffer if the player moves in that direction 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(); diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index b57ab9226c..9cdea0e1c3 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -1,13 +1,11 @@ use super::{img_ids::Imgs, Fonts, Show, TEXT_COLOR, TEXT_COLOR_3}; -use common::comp; use conrod_core::{ color, widget::{self, Button, Image, Rectangle, Scrollbar, Text}, widget_ids, /*, Color*/ Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; -use specs::{Join, WorldExt}; use client::{self, Client}; @@ -178,25 +176,12 @@ impl<'a> Widget for Social<'a> { // Players list // TODO: this list changes infrequently enough that it should not have to be recreated every frame - let ecs = self.client.state().ecs(); - let players = ecs.read_storage::(); - let mut count = 0; - for player in players.join() { - if ids.player_names.len() <= count { - ids.update(|ids| { - ids.player_names - .resize(count + 1, &mut ui.widget_id_generator()) - }) - } - - Text::new(&player.alias) - .down_from(ids.online_title, count as f64 * (15.0 + 3.0)) - .font_size(15) - .font_id(self.fonts.cyri) - .color(TEXT_COLOR) - .set(ids.player_names[count], ui); - - count += 1; + let count = self.client.player_list.len(); + if ids.player_names.len() < count { + ids.update(|ids| { + ids.player_names + .resize(count, &mut ui.widget_id_generator()) + }) } Text::new(&format!("{} player(s) online\n", count)) .top_left_with_margins_on(ids.content_align, -2.0, 7.0) @@ -204,6 +189,14 @@ impl<'a> Widget for Social<'a> { .font_id(self.fonts.cyri) .color(TEXT_COLOR) .set(ids.online_title, ui); + for (i, (_, player_alias)) in self.client.player_list.iter().enumerate() { + Text::new(player_alias) + .down(3.0) + .font_size(15) + .font_id(self.fonts.cyri) + .color(TEXT_COLOR) + .set(ids.player_names[i], ui); + } } // Friends Tab diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index 02ffd92ec1..93202101e2 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -17,7 +17,7 @@ use common::{ use crossbeam::channel; use dot_vox::DotVoxData; use frustum_query::frustum::Frustum; -use hashbrown::HashMap; +use hashbrown::{hash_map::Entry, HashMap}; use std::{f32, fmt::Debug, i32, marker::PhantomData, ops::Mul, time::Duration}; use vek::*; @@ -836,31 +836,48 @@ impl Terrain { .map(|(p, _)| *p) { let chunk_pos = client.state().terrain().pos_key(pos); + let new_mesh_state = ChunkMeshState { + pos: chunk_pos, + started_tick: current_tick, + active_worker: None, + }; + // Only mesh if this chunk has all its neighbors + // If it does have all its neighbors either it should have already been meshed or is in + // mesh_todo + match self.mesh_todo.entry(chunk_pos) { + Entry::Occupied(mut entry) => { + entry.insert(new_mesh_state); + } + Entry::Vacant(entry) => { + if self.chunks.contains_key(&chunk_pos) { + entry.insert(new_mesh_state); + } + } + } - self.mesh_todo.insert( - chunk_pos, - ChunkMeshState { - pos: chunk_pos, - started_tick: current_tick, - active_worker: None, - }, - ); - - // Handle chunks on chunk borders + // Handle block changes on chunk borders for x in -1..2 { for y in -1..2 { let neighbour_pos = pos + Vec3::new(x, y, 0); let neighbour_chunk_pos = client.state().terrain().pos_key(neighbour_pos); if neighbour_chunk_pos != chunk_pos { - self.mesh_todo.insert( - neighbour_chunk_pos, - ChunkMeshState { - pos: neighbour_chunk_pos, - started_tick: current_tick, - active_worker: None, - }, - ); + let new_mesh_state = ChunkMeshState { + pos: neighbour_chunk_pos, + started_tick: current_tick, + active_worker: None, + }; + // Only mesh if this chunk has all its neighbors + match self.mesh_todo.entry(neighbour_chunk_pos) { + Entry::Occupied(mut entry) => { + entry.insert(new_mesh_state); + } + Entry::Vacant(entry) => { + if self.chunks.contains_key(&neighbour_chunk_pos) { + entry.insert(new_mesh_state); + } + } + } } // TODO: Remesh all neighbours because we have complex lighting now