No longer block the main thread for client connections, new clients will be handled by server without waiting.

- Instread we have a dedicated thread that will async wait for new participants to connect and then notify the main thread
- registry no longer sends a view distance with it.
- remove ClientMsg::Command again as it's unused
This commit is contained in:
Marcel Märtens 2020-10-05 12:44:33 +02:00
parent 017e004309
commit 8b40f81ee2
14 changed files with 253 additions and 137 deletions

View File

@ -25,10 +25,10 @@ use common::{
},
event::{EventBus, LocalEvent},
msg::{
validate_chat_msg, ChatMsgValidationError, ClientInGameMsg, ClientIngame, ClientGeneralMsg,
validate_chat_msg, ChatMsgValidationError, ClientGeneralMsg, ClientInGameMsg, ClientIngame,
ClientNotInGameMsg, ClientRegisterMsg, ClientType, DisconnectReason, InviteAnswer,
Notification, PingMsg, PlayerInfo, PlayerListUpdate, RegisterError, ServerInGameMsg,
ServerInfo, ServerInitMsg, ServerGeneralMsg, ServerNotInGameMsg, ServerRegisterAnswerMsg,
Notification, PingMsg, PlayerInfo, PlayerListUpdate, RegisterError, ServerGeneralMsg,
ServerInGameMsg, ServerInfo, ServerInitMsg, ServerNotInGameMsg, ServerRegisterAnswerMsg,
MAX_BYTES_CHAT_MSG,
},
outcome::Outcome,
@ -444,11 +444,8 @@ impl Client {
}
).unwrap_or(Ok(username))?;
//TODO move ViewDistance out of register
self.register_stream.send(ClientRegisterMsg {
view_distance: self.view_distance,
token_or_username,
})?;
self.register_stream
.send(ClientRegisterMsg { token_or_username })?;
match block_on(self.register_stream.recv::<ServerRegisterAnswerMsg>())? {
Err(RegisterError::AlreadyLoggedIn) => Err(Error::AlreadyLoggedIn),
@ -1123,11 +1120,13 @@ impl Client {
},
DisconnectReason::Kicked(reason) => {
debug!("sending ClientMsg::Terminate because we got kicked");
frontend_events.push(Event::Kicked(reason.clone()));
frontend_events.push(Event::Kicked(reason));
self.singleton_stream.send(ClientGeneralMsg::Terminate)?;
},
},
ServerGeneralMsg::PlayerListUpdate(PlayerListUpdate::Init(list)) => self.player_list = list,
ServerGeneralMsg::PlayerListUpdate(PlayerListUpdate::Init(list)) => {
self.player_list = list
},
ServerGeneralMsg::PlayerListUpdate(PlayerListUpdate::Add(uid, player_info)) => {
if let Some(old_player_info) = self.player_list.insert(uid, player_info.clone()) {
warn!(
@ -1148,7 +1147,10 @@ impl Client {
);
}
},
ServerGeneralMsg::PlayerListUpdate(PlayerListUpdate::SelectedCharacter(uid, char_info)) => {
ServerGeneralMsg::PlayerListUpdate(PlayerListUpdate::SelectedCharacter(
uid,
char_info,
)) => {
if let Some(player_info) = self.player_list.get_mut(&uid) {
player_info.character = Some(char_info);
} else {
@ -1423,6 +1425,9 @@ impl Client {
},
ServerNotInGameMsg::CharacterSuccess => {
debug!("client is now in ingame state on server");
if let Some(vd) = self.view_distance {
self.set_view_distance(vd);
}
},
}
Ok(())

View File

@ -20,7 +20,6 @@ pub enum ClientType {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClientRegisterMsg {
pub view_distance: Option<u32>,
pub token_or_username: String,
}
@ -65,7 +64,6 @@ pub enum ClientInGameMsg {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ClientGeneralMsg {
ChatMsg(String),
Command(String),
Disconnect,
Terminate,
}
}

View File

@ -4,12 +4,14 @@ pub mod server;
// Reexports
pub use self::{
client::{ClientInGameMsg, ClientGeneralMsg, ClientNotInGameMsg, ClientRegisterMsg, ClientType},
client::{
ClientGeneralMsg, ClientInGameMsg, ClientNotInGameMsg, ClientRegisterMsg, ClientType,
},
ecs_packet::EcsCompPacket,
server::{
CharacterInfo, DisconnectReason, InviteAnswer, Notification, PlayerInfo, PlayerListUpdate,
RegisterError, ServerInGameMsg, ServerInfo, ServerInitMsg, ServerGeneralMsg, ServerNotInGameMsg,
ServerRegisterAnswerMsg,
RegisterError, ServerGeneralMsg, ServerInGameMsg, ServerInfo, ServerInitMsg,
ServerNotInGameMsg, ServerRegisterAnswerMsg,
},
};
use serde::{Deserialize, Serialize};

View File

@ -194,6 +194,7 @@ pub enum DisconnectReason {
/// Reponse To ClientType
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::clippy::large_enum_variant)]
pub enum ServerInitMsg {
TooManyPlayers,
GameSync {

View File

@ -1,7 +1,7 @@
use crate::error::Error;
use common::msg::{
ClientInGameMsg, ClientIngame, ClientGeneralMsg, ClientNotInGameMsg, ClientType, PingMsg,
ServerInGameMsg, ServerInitMsg, ServerGeneralMsg, ServerNotInGameMsg,
ClientGeneralMsg, ClientInGameMsg, ClientIngame, ClientNotInGameMsg, ClientType, PingMsg,
ServerGeneralMsg, ServerInGameMsg, ServerInitMsg, ServerNotInGameMsg,
};
use hashbrown::HashSet;
use network::{MessageBuffer, Participant, Stream};

View File

@ -12,7 +12,7 @@ use common::{
cmd::{ChatCommand, CHAT_COMMANDS, CHAT_SHORTCUTS},
comp::{self, ChatType, Item, LightEmitter, WaypointArea},
event::{EventBus, ServerEvent},
msg::{DisconnectReason, Notification, PlayerListUpdate, ServerInGameMsg, ServerGeneralMsg},
msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneralMsg, ServerInGameMsg},
npc::{self, get_npc_name},
state::TimeOfDay,
sync::{Uid, WorldSyncExt},
@ -504,8 +504,10 @@ fn handle_alias(
ecs.read_storage::<comp::Player>().get(target),
old_alias_optional,
) {
let msg =
ServerGeneralMsg::PlayerListUpdate(PlayerListUpdate::Alias(*uid, player.alias.clone()));
let msg = ServerGeneralMsg::PlayerListUpdate(PlayerListUpdate::Alias(
*uid,
player.alias.clone(),
));
server.state.notify_registered_clients(msg);
// Announce alias change if target has a Body.
@ -1157,7 +1159,10 @@ fn handle_waypoint(
.write_storage::<comp::Waypoint>()
.insert(target, comp::Waypoint::new(pos.0, *time));
server.notify_client(client, ChatType::CommandInfo.server_msg("Waypoint saved!"));
server.notify_client(client, ServerGeneralMsg::Notification(Notification::WaypointSaved));
server.notify_client(
client,
ServerGeneralMsg::Notification(Notification::WaypointSaved),
);
},
None => server.notify_client(
client,

View File

@ -0,0 +1,163 @@
use crate::{Client, ClientType, ServerInfo};
use crossbeam::{bounded, unbounded, Receiver, Sender};
use futures_executor::block_on;
use futures_timer::Delay;
use futures_util::{select, FutureExt};
use network::{Network, Participant, Promises};
use std::{
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread,
time::Duration,
};
use tracing::{debug, error, trace, warn};
pub(crate) struct ServerInfoPacket {
pub info: ServerInfo,
pub time: f64,
}
pub(crate) type ConnectionDataPacket = Client;
pub(crate) struct ConnectionHandler {
_network: Arc<Network>,
thread_handle: Option<thread::JoinHandle<()>>,
pub client_receiver: Receiver<ConnectionDataPacket>,
pub info_requester_receiver: Receiver<Sender<ServerInfoPacket>>,
running: Arc<AtomicBool>,
}
/// Instead of waiting the main loop we are handling connections, especially
/// their slow network .await part on a different thread. We need to communicate
/// to the Server main thread sometimes tough to get the current server_info and
/// time
impl ConnectionHandler {
pub fn new(network: Network) -> Self {
let network = Arc::new(network);
let network_clone = Arc::clone(&network);
let running = Arc::new(AtomicBool::new(true));
let running_clone = Arc::clone(&running);
let (client_sender, client_receiver) = unbounded::<ConnectionDataPacket>();
let (info_requester_sender, info_requester_receiver) =
bounded::<Sender<ServerInfoPacket>>(1);
let thread_handle = Some(thread::spawn(|| {
block_on(Self::work(
network_clone,
client_sender,
info_requester_sender,
running_clone,
));
}));
Self {
_network: network,
thread_handle,
client_receiver,
info_requester_receiver,
running,
}
}
async fn work(
network: Arc<Network>,
client_sender: Sender<ConnectionDataPacket>,
info_requester_sender: Sender<Sender<ServerInfoPacket>>,
running: Arc<AtomicBool>,
) {
while running.load(Ordering::Relaxed) {
const TIMEOUT: Duration = Duration::from_secs(5);
let participant = match select!(
_ = Delay::new(TIMEOUT).fuse() => None,
p = network.connected().fuse() => Some(p),
) {
None => continue, //check condition
Some(Ok(p)) => p,
Some(Err(e)) => {
error!(
?e,
"Stopping Conection Handler, no new connections can be made to server now!"
);
break;
},
};
let client_sender = client_sender.clone();
let info_requester_sender = info_requester_sender.clone();
match Self::init_participant(participant, client_sender, info_requester_sender).await {
Ok(_) => (),
Err(e) => warn!(?e, "drop new participant, because an error occurred"),
}
}
}
async fn init_participant(
participant: Participant,
client_sender: Sender<ConnectionDataPacket>,
info_requester_sender: Sender<Sender<ServerInfoPacket>>,
) -> Result<(), Box<dyn std::error::Error>> {
debug!("New Participant connected to the server");
let (sender, receiver) = bounded(1);
info_requester_sender.send(sender)?;
let reliable = Promises::ORDERED | Promises::CONSISTENCY;
let reliablec = reliable | Promises::COMPRESSED;
let general_stream = participant.open(10, reliablec).await?;
let ping_stream = participant.open(5, reliable).await?;
let mut register_stream = participant.open(10, reliablec).await?;
let in_game_stream = participant.open(10, reliablec).await?;
let not_in_game_stream = participant.open(10, reliablec).await?;
let server_data = receiver.recv()?;
register_stream.send(server_data.info)?;
const TIMEOUT: Duration = Duration::from_secs(5);
let client_type = match select!(
_ = Delay::new(TIMEOUT).fuse() => None,
t = register_stream.recv::<ClientType>().fuse() => Some(t),
) {
None => {
debug!("slow client connection detected, dropping it");
return Ok(());
},
Some(client_type) => client_type?,
};
let client = Client {
registered: false,
client_type,
in_game: None,
participant: std::sync::Mutex::new(Some(participant)),
singleton_stream: general_stream,
ping_stream,
register_stream,
in_game_stream,
not_in_game_stream,
network_error: std::sync::atomic::AtomicBool::new(false),
last_ping: server_data.time,
login_msg_sent: false,
};
client_sender.send(client)?;
Ok(())
}
}
impl Drop for ConnectionHandler {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
trace!("blocking till ConnectionHandler is closed");
self.thread_handle
.take()
.unwrap()
.join()
.expect("There was an error in ConnectionHandler, clean shutdown impossible");
trace!("gracefully closed ConnectionHandler!");
}
}

View File

@ -12,7 +12,7 @@ use common::{
Player, Pos, Stats,
},
lottery::Lottery,
msg::{PlayerListUpdate, ServerInGameMsg, ServerGeneralMsg},
msg::{PlayerListUpdate, ServerGeneralMsg, ServerInGameMsg},
outcome::Outcome,
state::BlockChange,
sync::{Uid, UidAllocator, WorldSyncExt},
@ -656,7 +656,7 @@ pub fn handle_level_up(server: &mut Server, entity: EcsEntity, new_level: u32) {
server
.state
.notify_registered_clients(ServerGeneralMsg::PlayerListUpdate(PlayerListUpdate::LevelChange(
*uid, new_level,
)));
.notify_registered_clients(ServerGeneralMsg::PlayerListUpdate(
PlayerListUpdate::LevelChange(*uid, new_level),
));
}

View File

@ -5,7 +5,7 @@ use crate::{
use common::{
comp,
comp::{group, Player},
msg::{PlayerListUpdate, ServerInGameMsg, ServerGeneralMsg},
msg::{PlayerListUpdate, ServerGeneralMsg, ServerInGameMsg},
span,
sync::{Uid, UidAllocator},
};
@ -130,8 +130,9 @@ pub fn handle_client_disconnect(server: &mut Server, entity: EcsEntity) -> Event
) {
state.notify_registered_clients(comp::ChatType::Offline(*uid).server_msg(""));
state
.notify_registered_clients(ServerGeneralMsg::PlayerListUpdate(PlayerListUpdate::Remove(*uid)));
state.notify_registered_clients(ServerGeneralMsg::PlayerListUpdate(
PlayerListUpdate::Remove(*uid),
));
}
// Make sure to remove the player from the logged in list. (See LoginProvider)

View File

@ -9,6 +9,7 @@ mod character_creator;
pub mod chunk_generator;
pub mod client;
pub mod cmd;
pub mod connection_handler;
mod data_dir;
pub mod error;
pub mod events;
@ -35,6 +36,7 @@ use crate::{
chunk_generator::ChunkGenerator,
client::{Client, RegionSubscription},
cmd::ChatCommandExt,
connection_handler::ConnectionHandler,
data_dir::DataDir,
login_provider::LoginProvider,
state_ext::StateExt,
@ -45,8 +47,8 @@ use common::{
comp::{self, ChatType},
event::{EventBus, ServerEvent},
msg::{
server::WorldMapMsg, ClientType, DisconnectReason, ServerInGameMsg, ServerInfo,
ServerInitMsg, ServerGeneralMsg, ServerNotInGameMsg,
server::WorldMapMsg, ClientType, DisconnectReason, ServerGeneralMsg, ServerInGameMsg,
ServerInfo, ServerInitMsg, ServerNotInGameMsg,
},
outcome::Outcome,
recipe::default_recipe_book,
@ -56,10 +58,8 @@ use common::{
vol::{ReadVol, RectVolSize},
};
use futures_executor::block_on;
use futures_timer::Delay;
use futures_util::{select, FutureExt};
use metrics::{ServerMetrics, StateTickMetrics, TickMetrics};
use network::{Network, Pid, Promises, ProtocolAddr};
use network::{Network, Pid, ProtocolAddr};
use persistence::{
character_loader::{CharacterLoader, CharacterLoaderResponseType},
character_updater::CharacterUpdater,
@ -100,7 +100,7 @@ pub struct Server {
index: IndexOwned,
map: WorldMapMsg,
network: Network,
connection_handler: ConnectionHandler,
thread_pool: ThreadPool,
@ -335,6 +335,7 @@ impl Server {
.expect("Failed to initialize server metrics submodule.");
thread_pool.execute(f);
block_on(network.listen(ProtocolAddr::Tcp(settings.gameserver_address)))?;
let connection_handler = ConnectionHandler::new(network);
let this = Self {
state,
@ -342,7 +343,7 @@ impl Server {
index,
map,
network,
connection_handler,
thread_pool,
@ -452,7 +453,7 @@ impl Server {
let before_new_connections = Instant::now();
// 3) Handle inputs from clients
block_on(self.handle_new_connections(&mut frontend_events))?;
self.handle_new_connections(&mut frontend_events)?;
let before_message_system = Instant::now();
@ -794,59 +795,30 @@ impl Server {
}
/// Handle new client connections.
async fn handle_new_connections(
&mut self,
frontend_events: &mut Vec<Event>,
) -> Result<(), Error> {
//TIMEOUT 0.1 ms for msg handling
const TIMEOUT: Duration = Duration::from_micros(100);
loop {
let participant = match select!(
_ = Delay::new(TIMEOUT).fuse() => None,
pr = self.network.connected().fuse() => Some(pr),
) {
None => return Ok(()),
Some(pr) => pr?,
};
debug!("New Participant connected to the server");
let reliable = Promises::ORDERED | Promises::CONSISTENCY;
let reliablec = reliable | Promises::COMPRESSED;
fn handle_new_connections(&mut self, frontend_events: &mut Vec<Event>) -> Result<(), Error> {
while let Ok(sender) = self.connection_handler.info_requester_receiver.try_recv() {
// can fail, e.g. due to timeout or network prob.
trace!("sending info to connection_handler");
let _ = sender.send(crate::connection_handler::ServerInfoPacket {
info: self.get_server_info(),
time: self.state.get_time(),
});
}
let stream = participant.open(10, reliablec).await?;
let ping_stream = participant.open(5, reliable).await?;
let mut register_stream = participant.open(10, reliablec).await?;
let in_game_stream = participant.open(10, reliablec).await?;
let not_in_game_stream = participant.open(10, reliablec).await?;
register_stream.send(self.get_server_info())?;
let client_type: ClientType = register_stream.recv().await?;
while let Ok(data) = self.connection_handler.client_receiver.try_recv() {
let mut client = data;
if self.settings().max_players
<= self.state.ecs().read_storage::<Client>().join().count()
{
trace!(
?participant,
?client.participant,
"to many players, wont allow participant to connect"
);
register_stream.send(ServerInitMsg::TooManyPlayers)?;
client.register_stream.send(ServerInitMsg::TooManyPlayers)?;
continue;
}
let client = Client {
registered: false,
client_type,
in_game: None,
participant: std::sync::Mutex::new(Some(participant)),
singleton_stream: stream,
ping_stream,
register_stream,
in_game_stream,
not_in_game_stream,
network_error: std::sync::atomic::AtomicBool::new(false),
last_ping: self.state.get_time(),
login_msg_sent: false,
};
let entity = self
.state
.ecs_mut()
@ -881,6 +853,7 @@ impl Server {
frontend_events.push(Event::ClientConnected { entity });
debug!("Done initial sync with client.");
}
Ok(())
}
pub fn notify_client<S>(&self, entity: EcsEntity, msg: S)

View File

@ -6,7 +6,7 @@ use common::{
comp,
effect::Effect,
msg::{
CharacterInfo, ClientIngame, PlayerListUpdate, ServerInGameMsg, ServerGeneralMsg,
CharacterInfo, ClientIngame, PlayerListUpdate, ServerGeneralMsg, ServerInGameMsg,
ServerNotInGameMsg,
},
state::State,

View File

@ -8,7 +8,7 @@ use crate::{
};
use common::{
comp::{ForceUpdate, Inventory, InventoryUpdate, Last, Ori, Player, Pos, Vel},
msg::{ServerInGameMsg, ServerGeneralMsg},
msg::{ServerGeneralMsg, ServerInGameMsg},
outcome::Outcome,
region::{Event as RegionEvent, RegionMap},
span,
@ -128,13 +128,14 @@ impl<'a> System<'a> for Sys {
(uid, pos, velocities.get(entity), orientations.get(entity))
})
}) {
let create_msg =
ServerGeneralMsg::CreateEntity(tracked_comps.create_entity_package(
let create_msg = ServerGeneralMsg::CreateEntity(
tracked_comps.create_entity_package(
entity,
Some(*pos),
vel.copied(),
ori.copied(),
));
),
);
for (client, regions, client_entity, _) in &mut subscribers {
if maybe_key
.as_ref()

View File

@ -15,10 +15,10 @@ use common::{
},
event::{EventBus, ServerEvent},
msg::{
validate_chat_msg, CharacterInfo, ChatMsgValidationError, ClientInGameMsg, ClientIngame,
ClientGeneralMsg, ClientNotInGameMsg, ClientRegisterMsg, DisconnectReason, PingMsg, PlayerInfo,
PlayerListUpdate, RegisterError, ServerInGameMsg, ServerGeneralMsg, ServerNotInGameMsg,
ServerRegisterAnswerMsg, MAX_BYTES_CHAT_MSG,
validate_chat_msg, CharacterInfo, ChatMsgValidationError, ClientGeneralMsg,
ClientInGameMsg, ClientIngame, ClientNotInGameMsg, ClientRegisterMsg, DisconnectReason,
PingMsg, PlayerInfo, PlayerListUpdate, RegisterError, ServerGeneralMsg, ServerInGameMsg,
ServerNotInGameMsg, ServerRegisterAnswerMsg, MAX_BYTES_CHAT_MSG,
},
span,
state::{BlockChange, Time},
@ -68,26 +68,6 @@ impl Sys {
}
}
},
ClientGeneralMsg::Command(message) => {
if client.registered {
match validate_chat_msg(&message) {
Ok(()) => {
if let Some(from) = uids.get(entity) {
let mode = chat_modes.get(entity).cloned().unwrap_or_default();
let msg = mode.new_message(*from, message);
new_chat_msgs.push((Some(entity), msg));
} else {
error!("Could not send message. Missing player uid");
}
},
Err(ChatMsgValidationError::TooLong) => {
let max = MAX_BYTES_CHAT_MSG;
let len = message.len();
warn!(?len, ?max, "Received a chat message that's too long")
},
}
}
},
ClientGeneralMsg::Disconnect => {
client.send_msg(ServerGeneralMsg::Disconnect(DisconnectReason::Requested));
},
@ -122,7 +102,7 @@ impl Sys {
settings: &Read<'_, Settings>,
msg: ClientInGameMsg,
) -> Result<(), crate::error::Error> {
if !client.in_game.is_some() {
if client.in_game.is_none() {
debug!(?entity, "client is not in_game, ignoring msg");
trace!(?msg, "ignored msg content");
if matches!(msg, ClientInGameMsg::TerrainChunkRequest{ .. }) {
@ -147,6 +127,7 @@ impl Sys {
)
});
//correct client if its VD is to high
if settings
.max_view_distance
.map(|max| view_distance > max)
@ -278,7 +259,7 @@ impl Sys {
}
},
ClientNotInGameMsg::Character(character_id) => {
if client.registered && !client.in_game.is_some() {
if client.registered && client.in_game.is_none() {
// Only send login message if it wasn't already
// sent previously
if let Some(player) = players.get(entity) {
@ -384,7 +365,6 @@ impl Sys {
login_provider: &mut WriteExpect<'_, LoginProvider>,
admins: &mut WriteStorage<'_, Admin>,
players: &mut WriteStorage<'_, Player>,
settings: &Read<'_, Settings>,
editable_settings: &ReadExpect<'_, EditableSettings>,
msg: ClientRegisterMsg,
) -> Result<(), crate::error::Error> {
@ -403,10 +383,8 @@ impl Sys {
Ok((username, uuid)) => (username, uuid),
};
let vd = msg
.view_distance
.map(|vd| vd.min(settings.max_view_distance.unwrap_or(vd)));
let player = Player::new(username.clone(), None, vd, uuid);
const INITIAL_VD: Option<u32> = Some(5); //will be changed after login
let player = Player::new(username.clone(), None, INITIAL_VD, uuid);
let is_admin = editable_settings.admins.contains(&uuid);
if !player.is_valid() {
@ -442,19 +420,6 @@ impl Sys {
// Add to list to notify all clients of the new player
new_players.push(entity);
}
// Limit view distance if it's too high
// This comes after state registration so that the client actually hears it
if settings
.max_view_distance
.zip(msg.view_distance)
.map(|(max, vd)| vd > max)
.unwrap_or(false)
{
client.send_in_game(ServerInGameMsg::SetViewDistance(
settings.max_view_distance.unwrap_or(0),
));
};
Ok(())
}
@ -564,7 +529,6 @@ impl Sys {
login_provider,
admins,
players,
settings,
editable_settings,
msg,
)?;
@ -734,12 +698,13 @@ impl<'a> System<'a> for Sys {
// 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 = ServerGeneralMsg::PlayerListUpdate(PlayerListUpdate::Add(*uid, PlayerInfo {
player_alias: player.alias.clone(),
is_online: true,
is_admin: admins.get(entity).is_some(),
character: None, // new players will be on character select.
}));
let msg =
ServerGeneralMsg::PlayerListUpdate(PlayerListUpdate::Add(*uid, PlayerInfo {
player_alias: player.alias.clone(),
is_online: true,
is_admin: admins.get(entity).is_some(),
character: None, // new players will be on character select.
}));
for client in (&mut clients).join().filter(|c| c.registered) {
client.send_msg(msg.clone())
}

View File

@ -42,7 +42,9 @@ impl<'a> System<'a> for Sys {
if let Ok(wp_old) = waypoints.insert(entity, Waypoint::new(player_pos.0, *time))
{
if wp_old.map_or(true, |w| w.elapsed(*time) > NOTIFY_TIME) {
client.send_msg(ServerGeneralMsg::Notification(Notification::WaypointSaved));
client.send_msg(ServerGeneralMsg::Notification(
Notification::WaypointSaved,
));
}
}
}