fix(player list): Show players not in range on the player list

fix(overflow): Stops including block updates that fail (since chunks
don't exist on the client) in `TerrainUpdates` (which would trigger
meshing of those nonexistent chunks). Furthermore, removes
remeshing of chunks with block updates if those chunks don't have all their
neighbours (since those wouldn't be meshed in the first place).
This commit is contained in:
Imbris 2019-12-23 01:02:00 -05:00
parent 722024ed7f
commit ec3e075020
12 changed files with 174 additions and 74 deletions

View File

@ -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<DynamicImage>,
pub player_list: HashMap<u64, String>,
postbox: PostBox<ClientMsg, ServerMsg>,
@ -71,7 +72,6 @@ pub struct Client {
impl Client {
/// Create a new `Client`.
#[allow(dead_code)]
pub fn new<A: Into<SocketAddr>>(addr: A, view_distance: Option<u32>) -> Result<Self, Error> {
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<Vec<Event>, 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
}

View File

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

View File

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

View File

@ -23,6 +23,14 @@ pub struct ServerInfo {
pub git_date: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PlayerListUpdate {
Init(HashMap<u64, String>),
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<usize>, /*, Vec<u32>)*/
},
PlayerListUpdate(PlayerListUpdate),
StateAnswer(Result<ClientState, (RequestStateError, ClientState)>),
ForceState(ClientState),
Ping,

View File

@ -326,17 +326,14 @@ impl State {
// Apply terrain changes
let mut terrain = self.ecs.write_resource::<TerrainGrid>();
self.ecs
.read_resource::<BlockChange>()
.blocks
.iter()
.for_each(|(pos, block)| {
let _ = terrain.set(*pos, *block);
});
self.ecs.write_resource::<TerrainChanges>().modified_blocks = std::mem::replace(
let mut modified_blocks = std::mem::replace(
&mut self.ecs.write_resource::<BlockChange>().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::<TerrainChanges>().modified_blocks = modified_blocks;
// Process local events
let events = self.ecs.read_resource::<EventBus<LocalEvent>>().recv_all();

View File

@ -167,6 +167,8 @@ impl<V: RectRasterableVol> VolGrid2d<V> {
pub struct CachedVolGrid2d<'a, V: RectRasterableVol> {
vol_grid_2d: &'a VolGrid2d<V>,
// This can't be invalidated by mutations of the chunks hashmap since we hold an immutable
// reference to the `VolGrid2d`
cache: Option<(Vec2<i32>, Arc<V>)>,
}
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<i32>) -> Result<&V::Vox, VolGrid2dError<V>> {
// Calculate chunk key from block pos
let ck = VolGrid2d::<V>::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
};

View File

@ -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::<comp::Player>()
.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::<Uid>().get(entity),
ecs.read_storage::<comp::Player>().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)));
}

View File

@ -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::<Uid>().get(entity),
state.read_storage::<comp::Player>().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);
}

View File

@ -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<Self>>,
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::<HashMap<_, _>>();
// 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 {

View File

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

View File

@ -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::<comp::Player>();
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

View File

@ -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<V: RectRasterableVol> Terrain<V> {
.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