Merge branch 'xMAC94x/netfixA' into 'master'

xmac94x/netfixA

See merge request veloren/veloren!1425
This commit is contained in:
Marcel 2020-10-12 21:07:00 +00:00
commit 2668731a05
29 changed files with 2125 additions and 1562 deletions

13
Cargo.lock generated
View File

@ -1393,9 +1393,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.5"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5"
checksum = "a7a4d35f7401e948629c9c3d6638fb9bf94e0b2121e96c3b428cc4e631f3eb74"
dependencies = [
"futures-core",
"futures-sink",
@ -1403,9 +1403,9 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.5"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399"
checksum = "d674eaa0056896d5ada519900dbf97ead2e46a7b6621e8160d79e2f2e1e2784b"
[[package]]
name = "futures-cpupool"
@ -1449,9 +1449,9 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.5"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc"
checksum = "0d8764258ed64ebc5d9ed185cf86a95db5cac810269c5d20ececb32e0088abbd"
[[package]]
name = "futures-task"
@ -4689,6 +4689,7 @@ dependencies = [
"diesel",
"diesel_migrations",
"dotenv",
"futures-channel",
"futures-executor",
"futures-timer",
"futures-util",

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
use crate::{comp::group::Group, msg::ServerMsg, sync::Uid};
use crate::{comp::group::Group, msg::ServerGeneral, sync::Uid};
use serde::{Deserialize, Serialize};
use specs::Component;
use specs_idvs::IdvStorage;
@ -118,11 +118,11 @@ impl<G> ChatType<G> {
}
}
impl ChatType<String> {
pub fn server_msg<S>(self, msg: S) -> ServerMsg
pub fn server_msg<S>(self, msg: S) -> ServerGeneral
where
S: Into<String>,
{
ServerMsg::ChatMsg(self.chat_msg(msg))
ServerGeneral::ChatMsg(self.chat_msg(msg))
}
}
// Stores chat text, type

View File

@ -1,3 +1,4 @@
use super::PingMsg;
use crate::{
character::CharacterId,
comp,
@ -7,13 +8,46 @@ use crate::{
use serde::{Deserialize, Serialize};
use vek::*;
///This struct contains all messages the client might send (on different
/// streams though). It's used to verify the correctness of the state in
/// debug_assertions
#[derive(Debug, Clone)]
#[allow(clippy::clippy::large_enum_variant)]
pub enum ClientMsg {
///Send on the first connection ONCE to identify client intention for
/// server
Type(ClientType),
///Send ONCE to register/auth to the server
Register(ClientRegister),
///Msg that can be send ALWAYS as soon as we are registered, e.g. `Chat`
General(ClientGeneral),
Ping(PingMsg),
}
/*
2nd Level Enums
*/
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum ClientType {
/// Regular Client like Voxygen who plays the game
Game,
/// A Chatonly client, which doesn't want to connect via its character
ChatOnly,
/// A unprivileged bot, e.g. to request world information
/// Or a privileged bot, e.g. to run admin commands used by server-cli
Bot { privileged: bool },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClientRegister {
pub token_or_username: String,
}
/// Messages sent from the client to the server
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ClientMsg {
Register {
view_distance: Option<u32>,
token_or_username: String,
},
pub enum ClientGeneral {
//Only in Character Screen
RequestCharacterList,
CreateCharacter {
alias: String,
@ -22,20 +56,15 @@ pub enum ClientMsg {
},
DeleteCharacter(CharacterId),
Character(CharacterId),
/// Request `ClientState::Registered` from an ingame state
ExitIngame,
/// Request `ClientState::Spectator` from a registered or ingame state
Spectate,
//Only in game
ControllerInputs(comp::ControllerInputs),
ControlEvent(comp::ControlEvent),
ControlAction(comp::ControlAction),
SetViewDistance(u32),
BreakBlock(Vec3<i32>),
PlaceBlock(Vec3<i32>, Block),
Ping,
Pong,
/// Send the chat message or command to be processed by the server
ChatMsg(String),
ExitInGame,
PlayerPhysics {
pos: comp::Pos,
vel: comp::Vel,
@ -44,9 +73,78 @@ pub enum ClientMsg {
TerrainChunkRequest {
key: Vec2<i32>,
},
Disconnect,
Terminate,
UnlockSkill(Skill),
RefundSkill(Skill),
UnlockSkillGroup(SkillGroupType),
//Always possible
ChatMsg(String),
Disconnect,
Terminate,
}
impl ClientMsg {
pub fn verify(
&self,
c_type: ClientType,
registered: bool,
in_game: Option<super::ClientInGame>,
) -> bool {
match self {
ClientMsg::Type(t) => c_type == *t,
ClientMsg::Register(_) => !registered && in_game.is_none(),
ClientMsg::General(g) => {
registered
&& match g {
ClientGeneral::RequestCharacterList
| ClientGeneral::CreateCharacter { .. }
| ClientGeneral::DeleteCharacter(_) => {
c_type != ClientType::ChatOnly && in_game.is_none()
},
ClientGeneral::Character(_) | ClientGeneral::Spectate => {
c_type == ClientType::Game && in_game.is_none()
},
//Only in game
ClientGeneral::ControllerInputs(_)
| ClientGeneral::ControlEvent(_)
| ClientGeneral::ControlAction(_)
| ClientGeneral::SetViewDistance(_)
| ClientGeneral::BreakBlock(_)
| ClientGeneral::PlaceBlock(_, _)
| ClientGeneral::ExitInGame
| ClientGeneral::PlayerPhysics { .. }
| ClientGeneral::TerrainChunkRequest { .. }
| ClientGeneral::UnlockSkill(_)
| ClientGeneral::RefundSkill(_)
| ClientGeneral::UnlockSkillGroup(_) => {
c_type == ClientType::Game && in_game.is_some()
},
//Always possible
ClientGeneral::ChatMsg(_)
| ClientGeneral::Disconnect
| ClientGeneral::Terminate => true,
}
},
ClientMsg::Ping(_) => true,
}
}
}
/*
end of 2nd level Enums
*/
impl Into<ClientMsg> for ClientType {
fn into(self) -> ClientMsg { ClientMsg::Type(self) }
}
impl Into<ClientMsg> for ClientRegister {
fn into(self) -> ClientMsg { ClientMsg::Register(self) }
}
impl Into<ClientMsg> for ClientGeneral {
fn into(self) -> ClientMsg { ClientMsg::General(self) }
}
impl Into<ClientMsg> for PingMsg {
fn into(self) -> ClientMsg { ClientMsg::Ping(self) }
}

View File

@ -1,27 +1,32 @@
pub mod client;
pub mod ecs_packet;
pub mod server;
pub mod world_packet;
// Reexports
pub use self::{
client::ClientMsg,
client::{ClientGeneral, ClientMsg, ClientRegister, ClientType},
ecs_packet::EcsCompPacket,
server::{
CharacterInfo, DisconnectReason, InviteAnswer, Notification, PlayerInfo, PlayerListUpdate,
RegisterError, RequestStateError, ServerInfo, ServerMsg,
RegisterError, ServerGeneral, ServerInfo, ServerInit, ServerMsg, ServerRegisterAnswer,
},
world_packet::WorldMapMsg,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum ClientState {
Pending,
Connected,
Registered,
pub enum ClientInGame {
Spectator,
Character,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum PingMsg {
Ping,
Pong,
}
pub const MAX_BYTES_CHAT_MSG: usize = 256;
pub enum ChatMsgValidationError {

View File

@ -1,4 +1,4 @@
use super::{ClientState, EcsCompPacket};
use super::{ClientType, EcsCompPacket, PingMsg};
use crate::{
character::CharacterItem,
comp,
@ -14,6 +14,25 @@ use serde::{Deserialize, Serialize};
use std::time::Duration;
use vek::*;
///This struct contains all messages the server might send (on different
/// streams though)
#[derive(Debug, Clone)]
pub enum ServerMsg {
/// Basic info about server, send ONCE, clients need it to Register
Info(ServerInfo),
/// Initial data package, send BEFORE Register ONCE. Not Register relevant
Init(ServerInit),
/// Result to `ClientMsg::Register`. send ONCE
RegisterAnswer(ServerRegisterAnswer),
///Msg that can be send ALWAYS as soon as client is registered, e.g. `Chat`
General(ServerGeneral),
Ping(PingMsg),
}
/*
2nd Level Enums
*/
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerInfo {
pub name: String,
@ -23,6 +42,85 @@ pub struct ServerInfo {
pub auth_provider: Option<String>,
}
/// Reponse To ClientType
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::clippy::large_enum_variant)]
pub enum ServerInit {
TooManyPlayers,
GameSync {
entity_package: sync::EntityPackage<EcsCompPacket>,
time_of_day: state::TimeOfDay,
max_group_size: u32,
client_timeout: Duration,
world_map: crate::msg::world_packet::WorldMapMsg,
recipe_book: RecipeBook,
},
}
pub type ServerRegisterAnswer = Result<(), RegisterError>;
/// Messages sent from the server to the client
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ServerGeneral {
//Character Screen related
/// An error occurred while loading character data
CharacterDataLoadError(String),
/// A list of characters belonging to the a authenticated player was sent
CharacterListUpdate(Vec<CharacterItem>),
/// An error occurred while creating or deleting a character
CharacterActionError(String),
CharacterSuccess,
//Ingame related
GroupUpdate(comp::group::ChangeNotification<sync::Uid>),
/// Indicate to the client that they are invited to join a group
GroupInvite {
inviter: sync::Uid,
timeout: std::time::Duration,
},
/// Indicate to the client that their sent invite was not invalid and is
/// currently pending
InvitePending(sync::Uid),
/// Note: this could potentially include all the failure cases such as
/// inviting yourself in which case the `InvitePending` message could be
/// removed and the client could consider their invite pending until
/// they receive this message Indicate to the client the result of their
/// invite
InviteComplete {
target: sync::Uid,
answer: InviteAnswer,
},
/// Trigger cleanup for when the client goes back to the `Registered` state
/// from an ingame state
ExitInGameSuccess,
InventoryUpdate(comp::Inventory, comp::InventoryUpdateEvent),
TerrainChunkUpdate {
key: Vec2<i32>,
chunk: Result<Box<TerrainChunk>, ()>,
},
TerrainBlockUpdates(HashMap<Vec3<i32>, Block>),
SetViewDistance(u32),
Outcomes(Vec<Outcome>),
Knockback(Vec3<f32>),
// Always possible
PlayerListUpdate(PlayerListUpdate),
/// A message to go into the client chat box. The client is responsible for
/// formatting the message and turning it into a speech bubble.
ChatMsg(comp::ChatMsg),
SetPlayerEntity(Uid),
TimeOfDay(state::TimeOfDay),
EntitySync(sync::EntitySyncPackage),
CompSync(sync::CompSyncPackage<EcsCompPacket>),
CreateEntity(sync::EntityPackage<EcsCompPacket>),
DeleteEntity(Uid),
Disconnect(DisconnectReason),
/// Send a popup notification such as "Waypoint Saved"
Notification(Notification),
}
/*
end of 2nd level Enums
*/
/// Inform the client of updates to the player list.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PlayerListUpdate {
@ -49,127 +147,6 @@ pub struct CharacterInfo {
pub level: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
/// World map information. Note that currently, we always send the whole thing
/// in one go, but the structure aims to try to provide information as locally
/// as possible, so that in the future we can split up large maps into multiple
/// WorldMapMsg fragments.
///
/// TODO: Update message format to make fragmentable, allowing us to send more
/// information without running into bandwidth issues.
///
/// TODO: Add information for rivers (currently, we just prerender them on the
/// server, but this is not a great solution for LoD. The map rendering code is
/// already set up to be able to take advantage of the river rendering being
/// split out, but the format is a little complicated for space reasons and it
/// may take some tweaking to get right, so we avoid sending it for now).
///
/// TODO: measure explicit compression schemes that might save space, e.g.
/// repeating the "small angles" optimization that works well on more detailed
/// shadow maps intended for height maps.
pub struct WorldMapMsg {
/// Log base 2 of world map dimensions (width × height) in chunks.
///
/// NOTE: Invariant: chunk count fits in a u16.
pub dimensions_lg: Vec2<u32>,
/// Sea level (used to provide a base altitude).
pub sea_level: f32,
/// Max height (used to scale altitudes).
pub max_height: f32,
/// RGB+A; the alpha channel is currently unused, but will be used in the
/// future. Entries are in the usual chunk order.
pub rgba: Vec<u32>,
/// Altitudes: bits 2 to 0 are unused, then bits 15 to 3 are used for
/// altitude. The remainder are currently unused, but we have plans to
/// use 7 bits for water depth (using an integer f7 encoding), and we
/// will find other uses for the remaining 12 bits.
pub alt: Vec<u32>,
/// Horizon mapping. This is a variant of shadow mapping that is
/// specifically designed for height maps; it takes advantage of their
/// regular structure (e.g. no holes) to compress all information needed
/// to decide when to cast a sharp shadow into a single nagle, the "horizon
/// angle." This is the smallest angle with the ground at which light can
/// pass through any occluders to reach the chunk, in some chosen
/// horizontal direction. This would not be sufficient for a more
/// complicated 3D structure, but it works for height maps since:
///
/// 1. they have no gaps, so as soon as light can shine through it will
/// always be able to do so, and
/// 2. we only care about lighting from the top, and only from the east and
/// west (since at a large scale like this we mostly just want to
/// handle variable sunlight; moonlight would present more challenges
/// but we currently have no plans to try to cast accurate shadows in
/// moonlight).
///
/// Our chosen format is two pairs of vectors,
/// with the first pair representing west-facing light (casting shadows on
/// the left side) and the second representing east-facing light
/// (casting shadows on the east side).
///
/// The pair of vectors consists of (with each vector in the usual chunk
/// order):
///
/// * Horizon angle pointing east (1 byte, scaled so 1 unit = 255° / 360).
/// We might consider switching to tangent if that represents the
/// information we care about better.
/// * Approximate (floor) height of maximal occluder. We currently use this
/// to try to deliver some approximation of soft shadows, which isn't that
/// big a deal on the world map but is probably needed in order to ensure
/// smooth transitions between chunks in LoD view. Additionally, when we
/// start using the shadow information to do local lighting on the world
/// map, we'll want a quick way to test where we can go out of shadow at
/// arbitrary heights (since the player and other entities cajn find
/// themselves far from the ground at times). While this is only an
/// approximation to a proper distance map, hopefully it will give us
/// something that feels reasonable enough for Veloren's style.
///
/// NOTE: On compression.
///
/// Horizon mapping has a lot of advantages for height maps (simple, easy to
/// understand, doesn't require any fancy math or approximation beyond
/// precision loss), though it loses a few of them by having to store
/// distance to occluder as well. However, just storing tons
/// and tons of regular shadow maps (153 for a full day cycle, stored at
/// irregular intervals) combined with clever explicit compression and
/// avoiding recording sharp local shadows (preferring retracing for
/// these), yielded a compression rate of under 3 bits per column! Since
/// we likely want to avoid per-column shadows for worlds of the sizes we
/// want, we'd still need to store *some* extra information to create
/// soft shadows, but it would still be nice to try to drive down our
/// size as much as possible given how compressible shadows of height
/// maps seem to be in practice. Therefore, we try to take advantage of the
/// way existing compression algorithms tend to work to see if we can
/// achieve significant gains without doing a lot of custom work.
///
/// Specifically, since our rays are cast east/west, we expect that for each
/// row, the horizon angles in each direction should be sequences of
/// monotonically increasing values (as chunks approach a tall
/// occluder), followed by sequences of no shadow, repeated
/// until the end of the map. Monotonic sequences and same-byte sequences
/// are usually easy to compress and existing algorithms are more likely
/// to be able to deal with them than jumbled data. If we were to keep
/// both directions in the same vector, off-the-shelf compression would
/// probably be less effective.
///
/// For related reasons, rather than storing distances as in a standard
/// distance map (which would lead to monotonically *decreasing* values
/// as we approached the occluder from a given direction), we store the
/// estimated *occluder height.* The idea here is that we replace the
/// monotonic sequences with constant sequences, which are extremely
/// straightforward to compress and mostly handled automatically by anything
/// that does run-length encoding (i.e. most off-the-shelf compression
/// algorithms).
///
/// We still need to benchmark this properly, as there's no guarantee our
/// current compression algorithms will actually work well on this data
/// in practice. It's possible that some other permutation (e.g. more
/// bits reserved for "distance to occluder" in exchange for an even
/// more predictible sequence) would end up compressing better than storing
/// angles, or that we don't need as much precision as we currently have
/// (256 possible angles).
pub horizons: [(Vec<u8>, Vec<u8>); 2],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum InviteAnswer {
Accepted,
@ -192,80 +169,6 @@ pub enum DisconnectReason {
Kicked(String),
}
/// Messages sent from the server to the client
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ServerMsg {
InitialSync {
entity_package: sync::EntityPackage<EcsCompPacket>,
server_info: ServerInfo,
time_of_day: state::TimeOfDay,
max_group_size: u32,
client_timeout: Duration,
world_map: WorldMapMsg,
recipe_book: RecipeBook,
},
/// An error occurred while loading character data
CharacterDataLoadError(String),
/// A list of characters belonging to the a authenticated player was sent
CharacterListUpdate(Vec<CharacterItem>),
/// An error occurred while creating or deleting a character
CharacterActionError(String),
PlayerListUpdate(PlayerListUpdate),
GroupUpdate(comp::group::ChangeNotification<sync::Uid>),
// Indicate to the client that they are invited to join a group
GroupInvite {
inviter: sync::Uid,
timeout: std::time::Duration,
},
// Indicate to the client that their sent invite was not invalid and is currently pending
InvitePending(sync::Uid),
// Note: this could potentially include all the failure cases such as inviting yourself in
// which case the `InvitePending` message could be removed and the client could consider their
// invite pending until they receive this message
// Indicate to the client the result of their invite
InviteComplete {
target: sync::Uid,
answer: InviteAnswer,
},
StateAnswer(Result<ClientState, (RequestStateError, ClientState)>),
/// Trigger cleanup for when the client goes back to the `Registered` state
/// from an ingame state
ExitIngameCleanup,
Ping,
Pong,
/// A message to go into the client chat box. The client is responsible for
/// formatting the message and turning it into a speech bubble.
ChatMsg(comp::ChatMsg),
SetPlayerEntity(Uid),
TimeOfDay(state::TimeOfDay),
EntitySync(sync::EntitySyncPackage),
CompSync(sync::CompSyncPackage<EcsCompPacket>),
CreateEntity(sync::EntityPackage<EcsCompPacket>),
DeleteEntity(Uid),
InventoryUpdate(comp::Inventory, comp::InventoryUpdateEvent),
TerrainChunkUpdate {
key: Vec2<i32>,
chunk: Result<Box<TerrainChunk>, ()>,
},
TerrainBlockUpdates(HashMap<Vec3<i32>, Block>),
Disconnect(DisconnectReason),
TooManyPlayers,
/// Send a popup notification such as "Waypoint Saved"
Notification(Notification),
SetViewDistance(u32),
Outcomes(Vec<Outcome>),
Knockback(Vec3<f32>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum RequestStateError {
RegisterDenied(RegisterError),
Denied,
Already,
Impossible,
WrongMessage,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum RegisterError {
AlreadyLoggedIn,
@ -276,10 +179,85 @@ pub enum RegisterError {
//TODO: InvalidAlias,
}
impl ServerMsg {
pub fn verify(
&self,
c_type: ClientType,
registered: bool,
in_game: Option<super::ClientInGame>,
) -> bool {
match self {
ServerMsg::Info(_) | ServerMsg::Init(_) | ServerMsg::RegisterAnswer(_) => {
!registered && in_game.is_none()
},
ServerMsg::General(g) => {
registered
&& match g {
//Character Screen related
ServerGeneral::CharacterDataLoadError(_)
| ServerGeneral::CharacterListUpdate(_)
| ServerGeneral::CharacterActionError(_) => {
c_type != ClientType::ChatOnly && in_game.is_none()
},
ServerGeneral::CharacterSuccess => {
c_type == ClientType::Game && in_game.is_none()
},
//Ingame related
ServerGeneral::GroupUpdate(_)
| ServerGeneral::GroupInvite { .. }
| ServerGeneral::InvitePending(_)
| ServerGeneral::InviteComplete { .. }
| ServerGeneral::ExitInGameSuccess
| ServerGeneral::InventoryUpdate(_, _)
| ServerGeneral::TerrainChunkUpdate { .. }
| ServerGeneral::TerrainBlockUpdates(_)
| ServerGeneral::SetViewDistance(_)
| ServerGeneral::Outcomes(_)
| ServerGeneral::Knockback(_) => {
c_type == ClientType::Game && in_game.is_some()
},
// Always possible
ServerGeneral::PlayerListUpdate(_)
| ServerGeneral::ChatMsg(_)
| ServerGeneral::SetPlayerEntity(_)
| ServerGeneral::TimeOfDay(_)
| ServerGeneral::EntitySync(_)
| ServerGeneral::CompSync(_)
| ServerGeneral::CreateEntity(_)
| ServerGeneral::DeleteEntity(_)
| ServerGeneral::Disconnect(_)
| ServerGeneral::Notification(_) => true,
}
},
ServerMsg::Ping(_) => true,
}
}
}
impl From<AuthClientError> for RegisterError {
fn from(err: AuthClientError) -> Self { Self::AuthError(err.to_string()) }
}
impl From<comp::ChatMsg> for ServerMsg {
fn from(v: comp::ChatMsg) -> Self { ServerMsg::ChatMsg(v) }
impl From<comp::ChatMsg> for ServerGeneral {
fn from(v: comp::ChatMsg) -> Self { ServerGeneral::ChatMsg(v) }
}
impl Into<ServerMsg> for ServerInfo {
fn into(self) -> ServerMsg { ServerMsg::Info(self) }
}
impl Into<ServerMsg> for ServerInit {
fn into(self) -> ServerMsg { ServerMsg::Init(self) }
}
impl Into<ServerMsg> for ServerRegisterAnswer {
fn into(self) -> ServerMsg { ServerMsg::RegisterAnswer(self) }
}
impl Into<ServerMsg> for ServerGeneral {
fn into(self) -> ServerMsg { ServerMsg::General(self) }
}
impl Into<ServerMsg> for PingMsg {
fn into(self) -> ServerMsg { ServerMsg::Ping(self) }
}

View File

@ -0,0 +1,123 @@
use serde::{Deserialize, Serialize};
use vek::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
/// World map information. Note that currently, we always send the whole thing
/// in one go, but the structure aims to try to provide information as locally
/// as possible, so that in the future we can split up large maps into multiple
/// WorldMapMsg fragments.
///
/// TODO: Update message format to make fragmentable, allowing us to send more
/// information without running into bandwidth issues.
///
/// TODO: Add information for rivers (currently, we just prerender them on the
/// server, but this is not a great solution for LoD. The map rendering code is
/// already set up to be able to take advantage of the river rendering being
/// split out, but the format is a little complicated for space reasons and it
/// may take some tweaking to get right, so we avoid sending it for now).
///
/// TODO: measure explicit compression schemes that might save space, e.g.
/// repeating the "small angles" optimization that works well on more detailed
/// shadow maps intended for height maps.
pub struct WorldMapMsg {
/// Log base 2 of world map dimensions (width × height) in chunks.
///
/// NOTE: Invariant: chunk count fits in a u16.
pub dimensions_lg: Vec2<u32>,
/// Sea level (used to provide a base altitude).
pub sea_level: f32,
/// Max height (used to scale altitudes).
pub max_height: f32,
/// RGB+A; the alpha channel is currently unused, but will be used in the
/// future. Entries are in the usual chunk order.
pub rgba: Vec<u32>,
/// Altitudes: bits 2 to 0 are unused, then bits 15 to 3 are used for
/// altitude. The remainder are currently unused, but we have plans to
/// use 7 bits for water depth (using an integer f7 encoding), and we
/// will find other uses for the remaining 12 bits.
pub alt: Vec<u32>,
/// Horizon mapping. This is a variant of shadow mapping that is
/// specifically designed for height maps; it takes advantage of their
/// regular structure (e.g. no holes) to compress all information needed
/// to decide when to cast a sharp shadow into a single nagle, the "horizon
/// angle." This is the smallest angle with the ground at which light can
/// pass through any occluders to reach the chunk, in some chosen
/// horizontal direction. This would not be sufficient for a more
/// complicated 3D structure, but it works for height maps since:
///
/// 1. they have no gaps, so as soon as light can shine through it will
/// always be able to do so, and
/// 2. we only care about lighting from the top, and only from the east and
/// west (since at a large scale like this we mostly just want to
/// handle variable sunlight; moonlight would present more challenges
/// but we currently have no plans to try to cast accurate shadows in
/// moonlight).
///
/// Our chosen format is two pairs of vectors,
/// with the first pair representing west-facing light (casting shadows on
/// the left side) and the second representing east-facing light
/// (casting shadows on the east side).
///
/// The pair of vectors consists of (with each vector in the usual chunk
/// order):
///
/// * Horizon angle pointing east (1 byte, scaled so 1 unit = 255° / 360).
/// We might consider switching to tangent if that represents the
/// information we care about better.
/// * Approximate (floor) height of maximal occluder. We currently use this
/// to try to deliver some approximation of soft shadows, which isn't that
/// big a deal on the world map but is probably needed in order to ensure
/// smooth transitions between chunks in LoD view. Additionally, when we
/// start using the shadow information to do local lighting on the world
/// map, we'll want a quick way to test where we can go out of shadow at
/// arbitrary heights (since the player and other entities cajn find
/// themselves far from the ground at times). While this is only an
/// approximation to a proper distance map, hopefully it will give us
/// something that feels reasonable enough for Veloren's style.
///
/// NOTE: On compression.
///
/// Horizon mapping has a lot of advantages for height maps (simple, easy to
/// understand, doesn't require any fancy math or approximation beyond
/// precision loss), though it loses a few of them by having to store
/// distance to occluder as well. However, just storing tons
/// and tons of regular shadow maps (153 for a full day cycle, stored at
/// irregular intervals) combined with clever explicit compression and
/// avoiding recording sharp local shadows (preferring retracing for
/// these), yielded a compression rate of under 3 bits per column! Since
/// we likely want to avoid per-column shadows for worlds of the sizes we
/// want, we'd still need to store *some* extra information to create
/// soft shadows, but it would still be nice to try to drive down our
/// size as much as possible given how compressible shadows of height
/// maps seem to be in practice. Therefore, we try to take advantage of the
/// way existing compression algorithms tend to work to see if we can
/// achieve significant gains without doing a lot of custom work.
///
/// Specifically, since our rays are cast east/west, we expect that for each
/// row, the horizon angles in each direction should be sequences of
/// monotonically increasing values (as chunks approach a tall
/// occluder), followed by sequences of no shadow, repeated
/// until the end of the map. Monotonic sequences and same-byte sequences
/// are usually easy to compress and existing algorithms are more likely
/// to be able to deal with them than jumbled data. If we were to keep
/// both directions in the same vector, off-the-shelf compression would
/// probably be less effective.
///
/// For related reasons, rather than storing distances as in a standard
/// distance map (which would lead to monotonically *decreasing* values
/// as we approached the occluder from a given direction), we store the
/// estimated *occluder height.* The idea here is that we replace the
/// monotonic sequences with constant sequences, which are extremely
/// straightforward to compress and mostly handled automatically by anything
/// that does run-length encoding (i.e. most off-the-shelf compression
/// algorithms).
///
/// We still need to benchmark this properly, as there's no guarantee our
/// current compression algorithms will actually work well on this data
/// in practice. It's possible that some other permutation (e.g. more
/// bits reserved for "distance to occluder" in exchange for an even
/// more predictible sequence) would end up compressing better than storing
/// angles, or that we don't need as much precision as we currently have
/// (256 possible angles).
pub horizons: [(Vec<u8>, Vec<u8>); 2],
}

View File

@ -22,6 +22,7 @@ uvth = "3.1.1"
futures-util = "0.3"
futures-executor = "0.3"
futures-timer = "2.0"
futures-channel = "0.3"
itertools = "0.9"
lazy_static = "1.4.0"
scan_fmt = "0.2.4"

View File

@ -1,21 +1,24 @@
use crate::error::Error;
use common::msg::{ClientMsg, ClientState, RequestStateError, ServerMsg};
use common::msg::{ClientInGame, ClientType, ServerGeneral, ServerMsg};
use hashbrown::HashSet;
use network::{Participant, Stream};
use serde::{de::DeserializeOwned, Serialize};
use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage;
use std::sync::{
atomic::{AtomicBool, Ordering},
Mutex,
};
use tracing::debug;
use vek::*;
pub struct Client {
pub client_state: ClientState,
pub participant: Mutex<Option<Participant>>,
pub singleton_stream: Stream,
pub network_error: AtomicBool,
pub registered: bool,
pub client_type: ClientType,
pub in_game: Option<ClientInGame>,
pub participant: Option<Participant>,
pub general_stream: Stream,
pub ping_stream: Stream,
pub register_stream: Stream,
pub character_screen_stream: Stream,
pub in_game_stream: Stream,
pub network_error: bool,
pub last_ping: f64,
pub login_msg_sent: bool,
}
@ -25,22 +28,86 @@ impl Component for Client {
}
impl Client {
pub fn notify(&mut self, msg: ServerMsg) {
if !self.network_error.load(Ordering::Relaxed) {
if let Err(e) = self.singleton_stream.send(msg) {
fn internal_send<M: Serialize>(err: &mut bool, s: &mut Stream, msg: M) {
if !*err {
if let Err(e) = s.send(msg) {
debug!(?e, "got a network error with client");
self.network_error.store(true, Ordering::Relaxed);
*err = true;
}
}
}
pub async fn recv(&mut self) -> Result<ClientMsg, Error> {
if !self.network_error.load(Ordering::Relaxed) {
match self.singleton_stream.recv().await {
/*
fn internal_send_raw(b: &AtomicBool, s: &mut Stream, msg: Arc<MessageBuffer>) {
if !b.load(Ordering::Relaxed) {
if let Err(e) = s.send_raw(msg) {
debug!(?e, "got a network error with client");
b.store(true, Ordering::Relaxed);
}
}
}
*/
pub fn send_msg<S>(&mut self, msg: S)
where
S: Into<ServerMsg>,
{
const ERR: &str = "Dont do that, thats only done once at the start, no via this class";
match msg.into() {
ServerMsg::Info(_) => panic!(ERR),
ServerMsg::Init(_) => panic!(ERR),
ServerMsg::RegisterAnswer(msg) => {
Self::internal_send(&mut self.network_error, &mut self.register_stream, &msg)
},
ServerMsg::General(msg) => {
let stream = match &msg {
//Character Screen related
ServerGeneral::CharacterDataLoadError(_)
| ServerGeneral::CharacterListUpdate(_)
| ServerGeneral::CharacterActionError(_)
| ServerGeneral::CharacterSuccess => &mut self.character_screen_stream,
//Ingame related
ServerGeneral::GroupUpdate(_)
| ServerGeneral::GroupInvite { .. }
| ServerGeneral::InvitePending(_)
| ServerGeneral::InviteComplete { .. }
| ServerGeneral::ExitInGameSuccess
| ServerGeneral::InventoryUpdate(_, _)
| ServerGeneral::TerrainChunkUpdate { .. }
| ServerGeneral::TerrainBlockUpdates(_)
| ServerGeneral::SetViewDistance(_)
| ServerGeneral::Outcomes(_)
| ServerGeneral::Knockback(_) => &mut self.in_game_stream,
// Always possible
ServerGeneral::PlayerListUpdate(_)
| ServerGeneral::ChatMsg(_)
| ServerGeneral::SetPlayerEntity(_)
| ServerGeneral::TimeOfDay(_)
| ServerGeneral::EntitySync(_)
| ServerGeneral::CompSync(_)
| ServerGeneral::CreateEntity(_)
| ServerGeneral::DeleteEntity(_)
| ServerGeneral::Disconnect(_)
| ServerGeneral::Notification(_) => &mut self.general_stream,
};
Self::internal_send(&mut self.network_error, stream, &msg)
},
ServerMsg::Ping(msg) => {
Self::internal_send(&mut self.network_error, &mut self.ping_stream, &msg)
},
};
}
pub async fn internal_recv<M: DeserializeOwned>(
err: &mut bool,
s: &mut Stream,
) -> Result<M, Error> {
if !*err {
match s.recv().await {
Ok(r) => Ok(r),
Err(e) => {
debug!(?e, "got a network error with client while recv");
self.network_error.store(true, Ordering::Relaxed);
*err = true;
Err(Error::StreamErr(e))
},
}
@ -48,31 +115,6 @@ impl Client {
Err(Error::StreamErr(network::StreamError::StreamClosed))
}
}
pub fn is_registered(&self) -> bool {
matches!(
self.client_state,
ClientState::Registered | ClientState::Spectator | ClientState::Character
)
}
pub fn is_ingame(&self) -> bool {
matches!(
self.client_state,
ClientState::Spectator | ClientState::Character
)
}
pub fn allow_state(&mut self, new_state: ClientState) {
self.client_state = new_state;
let _ = self
.singleton_stream
.send(ServerMsg::StateAnswer(Ok(new_state)));
}
pub fn error_state(&mut self, error: RequestStateError) {
let _ = self.notify(ServerMsg::StateAnswer(Err((error, self.client_state))));
}
}
// Distance from fuzzy_chunk before snapping to current chunk

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, ServerMsg},
msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral},
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 =
ServerMsg::PlayerListUpdate(PlayerListUpdate::Alias(*uid, player.alias.clone()));
let msg = ServerGeneral::PlayerListUpdate(PlayerListUpdate::Alias(
*uid,
player.alias.clone(),
));
server.state.notify_registered_clients(msg);
// Announce alias change if target has a Body.
@ -667,7 +669,9 @@ fn handle_spawn(
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g)));
.map(|(g, c)| {
c.send_msg(ServerGeneral::GroupUpdate(g))
});
},
);
} else if let Some(group) = match alignment {
@ -1155,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, ServerMsg::Notification(Notification::WaypointSaved));
server.notify_client(
client,
ServerGeneral::Notification(Notification::WaypointSaved),
);
},
None => server.notify_client(
client,
@ -1191,7 +1198,7 @@ fn handle_adminify(
ecs.write_storage().insert(player, comp::Admin).is_ok()
};
// Update player list so the player shows up as admin in client chat.
let msg = ServerMsg::PlayerListUpdate(PlayerListUpdate::Admin(
let msg = ServerGeneral::PlayerListUpdate(PlayerListUpdate::Admin(
*ecs.read_storage::<Uid>()
.get(player)
.expect("Player should have uid"),
@ -1584,7 +1591,7 @@ fn find_target(
ecs: &specs::World,
opt_alias: Option<String>,
fallback: EcsEntity,
) -> Result<EcsEntity, ServerMsg> {
) -> Result<EcsEntity, ServerGeneral> {
if let Some(alias) = opt_alias {
(&ecs.entities(), &ecs.read_storage::<comp::Player>())
.join()
@ -1656,7 +1663,7 @@ fn handle_set_level(
.expect("Failed to get uid for player");
server
.state
.notify_registered_clients(ServerMsg::PlayerListUpdate(
.notify_registered_clients(ServerGeneral::PlayerListUpdate(
PlayerListUpdate::LevelChange(uid, lvl),
));
@ -1896,7 +1903,7 @@ fn kick_player(server: &mut Server, target_player: EcsEntity, reason: &str) {
.emit_now(ServerEvent::ClientDisconnect(target_player));
server.notify_client(
target_player,
ServerMsg::Disconnect(DisconnectReason::Kicked(reason.to_string())),
ServerGeneral::Disconnect(DisconnectReason::Kicked(reason.to_string())),
);
}

View File

@ -0,0 +1,158 @@
use crate::{Client, ClientType, ServerInfo};
use crossbeam::{bounded, unbounded, Receiver, Sender};
use futures_channel::oneshot;
use futures_executor::block_on;
use futures_timer::Delay;
use futures_util::{select, FutureExt};
use network::{Network, Participant, Promises};
use std::{sync::Arc, thread, time::Duration};
use tracing::{debug, error, trace, warn};
pub(crate) struct ServerInfoPacket {
pub info: ServerInfo,
pub time: f64,
}
pub(crate) struct ConnectionHandler {
_network: Arc<Network>,
thread_handle: Option<thread::JoinHandle<()>>,
pub client_receiver: Receiver<Client>,
pub info_requester_receiver: Receiver<Sender<ServerInfoPacket>>,
stop_sender: Option<oneshot::Sender<()>>,
}
/// 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 though 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 (stop_sender, stop_receiver) = oneshot::channel();
let (client_sender, client_receiver) = unbounded::<Client>();
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,
stop_receiver,
));
}));
Self {
_network: network,
thread_handle,
client_receiver,
info_requester_receiver,
stop_sender: Some(stop_sender),
}
}
async fn work(
network: Arc<Network>,
client_sender: Sender<Client>,
info_requester_sender: Sender<Sender<ServerInfoPacket>>,
stop_receiver: oneshot::Receiver<()>,
) {
let mut stop_receiver = stop_receiver.fuse();
loop {
let participant = match select!(
_ = stop_receiver => None,
p = network.connected().fuse() => Some(p),
) {
None => break,
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 select!(
_ = stop_receiver => None,
e = Self::init_participant(participant, client_sender, info_requester_sender).fuse() => Some(e),
) {
None => break,
Some(Ok(())) => (),
Some(Err(e)) => warn!(?e, "drop new participant, because an error occurred"),
}
}
}
async fn init_participant(
participant: Participant,
client_sender: Sender<Client>,
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 character_screen_stream = participant.open(10, reliablec).await?;
let 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: Some(participant),
general_stream,
ping_stream,
register_stream,
in_game_stream,
character_screen_stream,
network_error: false,
last_ping: server_data.time,
login_msg_sent: false,
};
client_sender.send(client)?;
Ok(())
}
}
impl Drop for ConnectionHandler {
fn drop(&mut self) {
let _ = self.stop_sender.take().unwrap().send(());
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, ServerMsg},
msg::{PlayerListUpdate, ServerGeneral},
outcome::Outcome,
state::BlockChange,
sync::{Uid, UidAllocator, WorldSyncExt},
@ -44,7 +44,7 @@ pub fn handle_knockback(server: &Server, entity: EcsEntity, impulse: Vec3<f32>)
}
let mut clients = state.ecs().write_storage::<Client>();
if let Some(client) = clients.get_mut(entity) {
client.notify(ServerMsg::Knockback(impulse));
client.send_msg(ServerGeneral::Knockback(impulse));
}
}
@ -656,7 +656,7 @@ pub fn handle_level_up(server: &mut Server, entity: EcsEntity, new_level: u32) {
server
.state
.notify_registered_clients(ServerMsg::PlayerListUpdate(PlayerListUpdate::LevelChange(
*uid, new_level,
)));
.notify_registered_clients(ServerGeneral::PlayerListUpdate(
PlayerListUpdate::LevelChange(*uid, new_level),
));
}

View File

@ -5,7 +5,7 @@ use common::{
group::{self, Group, GroupManager, Invite, PendingInvites},
ChatType, GroupManip,
},
msg::{InviteAnswer, ServerMsg},
msg::{InviteAnswer, ServerGeneral},
sync,
sync::WorldSyncExt,
};
@ -31,7 +31,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
None => {
// Inform of failure
if let Some(client) = clients.get_mut(entity) {
client.notify(
client.send_msg(
ChatType::Meta
.server_msg("Invite failed, target does not exist.".to_owned()),
);
@ -63,7 +63,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
if already_in_same_group {
// Inform of failure
if let Some(client) = clients.get_mut(entity) {
client.notify(ChatType::Meta.server_msg(
client.send_msg(ChatType::Meta.server_msg(
"Invite failed, can't invite someone already in your group".to_owned(),
));
}
@ -93,7 +93,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
if group_size_limit_reached {
// Inform inviter that they have reached the group size limit
if let Some(client) = clients.get_mut(entity) {
client.notify(
client.send_msg(
ChatType::Meta.server_msg(
"Invite failed, pending invites plus current group size have reached \
the group size limit"
@ -110,7 +110,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
if invites.contains(invitee) {
// Inform inviter that there is already an invite
if let Some(client) = clients.get_mut(entity) {
client.notify(
client.send_msg(
ChatType::Meta
.server_msg("This player already has a pending invite.".to_owned()),
);
@ -155,7 +155,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
(clients.get_mut(invitee), uids.get(entity).copied())
{
if send_invite() {
client.notify(ServerMsg::GroupInvite {
client.send_msg(ServerGeneral::GroupInvite {
inviter,
timeout: PRESENTED_INVITE_TIMEOUT_DUR,
});
@ -163,7 +163,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
} else if agents.contains(invitee) {
send_invite();
} else if let Some(client) = clients.get_mut(entity) {
client.notify(
client.send_msg(
ChatType::Meta.server_msg("Can't invite, not a player or npc".to_owned()),
);
}
@ -171,7 +171,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
// Notify inviter that the invite is pending
if invite_sent {
if let Some(client) = clients.get_mut(entity) {
client.notify(ServerMsg::InvitePending(uid));
client.send_msg(ServerGeneral::InvitePending(uid));
}
}
},
@ -196,7 +196,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
if let (Some(client), Some(target)) =
(clients.get_mut(inviter), uids.get(entity).copied())
{
client.notify(ServerMsg::InviteComplete {
client.send_msg(ServerGeneral::InviteComplete {
target,
answer: InviteAnswer::Accepted,
})
@ -217,7 +217,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g)));
.map(|(g, c)| c.send_msg(ServerGeneral::GroupUpdate(g)));
},
);
}
@ -244,7 +244,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
if let (Some(client), Some(target)) =
(clients.get_mut(inviter), uids.get(entity).copied())
{
client.notify(ServerMsg::InviteComplete {
client.send_msg(ServerGeneral::InviteComplete {
target,
answer: InviteAnswer::Declined,
})
@ -269,7 +269,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g)));
.map(|(g, c)| c.send_msg(ServerGeneral::GroupUpdate(g)));
},
);
},
@ -283,7 +283,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
None => {
// Inform of failure
if let Some(client) = clients.get_mut(entity) {
client.notify(
client.send_msg(
ChatType::Meta
.server_msg("Kick failed, target does not exist.".to_owned()),
);
@ -296,7 +296,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
if matches!(alignments.get(target), Some(comp::Alignment::Owned(owner)) if uids.get(target).map_or(true, |u| u != owner))
{
if let Some(client) = clients.get_mut(entity) {
client.notify(
client.send_msg(
ChatType::Meta.server_msg("Kick failed, you can't kick pets.".to_owned()),
);
}
@ -305,7 +305,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
// Can't kick yourself
if uids.get(entity).map_or(false, |u| *u == uid) {
if let Some(client) = clients.get_mut(entity) {
client.notify(
client.send_msg(
ChatType::Meta
.server_msg("Kick failed, you can't kick yourself.".to_owned()),
);
@ -336,26 +336,26 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g)));
.map(|(g, c)| c.send_msg(ServerGeneral::GroupUpdate(g)));
},
);
// Tell them the have been kicked
if let Some(client) = clients.get_mut(target) {
client.notify(
client.send_msg(
ChatType::Meta
.server_msg("You were removed from the group.".to_owned()),
);
}
// Tell kicker that they were succesful
if let Some(client) = clients.get_mut(entity) {
client.notify(ChatType::Meta.server_msg("Player kicked.".to_owned()));
client.send_msg(ChatType::Meta.server_msg("Player kicked.".to_owned()));
}
},
Some(_) => {
// Inform kicker that they are not the leader
if let Some(client) = clients.get_mut(entity) {
client.notify(ChatType::Meta.server_msg(
client.send_msg(ChatType::Meta.server_msg(
"Kick failed: You are not the leader of the target's group.".to_owned(),
));
}
@ -363,7 +363,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
None => {
// Inform kicker that the target is not in a group
if let Some(client) = clients.get_mut(entity) {
client.notify(
client.send_msg(
ChatType::Meta.server_msg(
"Kick failed: Your target is not in a group.".to_owned(),
),
@ -380,7 +380,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
None => {
// Inform of failure
if let Some(client) = clients.get_mut(entity) {
client.notify(ChatType::Meta.server_msg(
client.send_msg(ChatType::Meta.server_msg(
"Leadership transfer failed, target does not exist".to_owned(),
));
}
@ -410,18 +410,18 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g)));
.map(|(g, c)| c.send_msg(ServerGeneral::GroupUpdate(g)));
},
);
// Tell them they are the leader
if let Some(client) = clients.get_mut(target) {
client.notify(
client.send_msg(
ChatType::Meta.server_msg("You are the group leader now.".to_owned()),
);
}
// Tell the old leader that the transfer was succesful
if let Some(client) = clients.get_mut(target) {
client.notify(
client.send_msg(
ChatType::Meta
.server_msg("You are no longer the group leader.".to_owned()),
);
@ -430,7 +430,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
Some(_) => {
// Inform transferer that they are not the leader
if let Some(client) = clients.get_mut(entity) {
client.notify(
client.send_msg(
ChatType::Meta.server_msg(
"Transfer failed: You are not the leader of the target's group."
.to_owned(),
@ -441,7 +441,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
None => {
// Inform transferer that the target is not in a group
if let Some(client) = clients.get_mut(entity) {
client.notify(ChatType::Meta.server_msg(
client.send_msg(ChatType::Meta.server_msg(
"Transfer failed: Your target is not in a group.".to_owned(),
));
}

View File

@ -4,7 +4,7 @@ use crate::{
};
use common::{
comp::{self, item},
msg::ServerMsg,
msg::ServerGeneral,
sync::{Uid, WorldSyncExt},
};
use specs::{world::WorldExt, Entity as EcsEntity};
@ -116,7 +116,7 @@ pub fn handle_possess(server: &Server, possessor_uid: Uid, possesse_uid: Uid) {
let mut clients = ecs.write_storage::<Client>();
if clients.get_mut(possesse).is_none() {
if let Some(mut client) = clients.remove(possessor) {
client.notify(ServerMsg::SetPlayerEntity(possesse_uid));
client.send_msg(ServerGeneral::SetPlayerEntity(possesse_uid));
clients
.insert(possesse, client)
.err()

View File

@ -5,7 +5,7 @@ use common::{
slot::{self, Slot},
Pos, MAX_PICKUP_RANGE_SQR,
},
msg::ServerMsg,
msg::ServerGeneral,
recipe::default_recipe_book,
sync::{Uid, WorldSyncExt},
vol::ReadVol,
@ -281,7 +281,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
.map(|g| (g, c))
})
.map(|(g, c)| {
c.notify(ServerMsg::GroupUpdate(g))
c.send_msg(ServerGeneral::GroupUpdate(g))
});
},
);

View File

@ -5,7 +5,7 @@ use crate::{
use common::{
comp,
comp::{group, Player},
msg::{ClientState, PlayerListUpdate, ServerMsg},
msg::{PlayerListUpdate, ServerGeneral},
span,
sync::{Uid, UidAllocator},
};
@ -33,9 +33,8 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) {
.cloned();
if let (Some(mut client), Some(uid), Some(player)) = (maybe_client, maybe_uid, maybe_player) {
// Tell client its request was successful
client.allow_state(ClientState::Registered);
// Tell client to clear out other entities and its own components
client.notify(ServerMsg::ExitIngameCleanup);
client.in_game = None;
client.send_msg(ServerGeneral::ExitInGameSuccess);
let entity_builder = state.ecs_mut().create_entity().with(client).with(player);
@ -90,14 +89,13 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) {
pub fn handle_client_disconnect(server: &mut Server, entity: EcsEntity) -> Event {
span!(_guard, "handle_client_disconnect");
if let Some(client) = server.state().read_storage::<Client>().get(entity) {
let participant = match client.participant.try_lock() {
Ok(mut p) => p.take().unwrap(),
Err(e) => {
error!(?e, ?entity, "couldn't lock participant for removal");
return Event::ClientDisconnected { entity };
},
};
if let Some(client) = server
.state()
.ecs()
.write_storage::<Client>()
.get_mut(entity)
{
let participant = client.participant.take().unwrap();
let pid = participant.remote_pid();
std::thread::spawn(move || {
let span = tracing::span!(tracing::Level::DEBUG, "client_disconnect", ?pid, ?entity);
@ -131,8 +129,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(ServerMsg::PlayerListUpdate(PlayerListUpdate::Remove(*uid)));
state.notify_registered_clients(ServerGeneral::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,
@ -44,7 +46,9 @@ use common::{
cmd::ChatCommand,
comp::{self, ChatType},
event::{EventBus, ServerEvent},
msg::{server::WorldMapMsg, ClientState, DisconnectReason, ServerInfo, ServerMsg},
msg::{
ClientType, DisconnectReason, ServerGeneral, ServerInfo, ServerInit, ServerMsg, WorldMapMsg,
},
outcome::Outcome,
recipe::default_recipe_book,
state::{State, TimeOfDay},
@ -53,8 +57,6 @@ 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, ProtocolAddr};
use persistence::{
@ -70,7 +72,7 @@ use std::{
};
#[cfg(not(feature = "worldgen"))]
use test_world::{IndexOwned, World};
use tracing::{debug, error, info, warn};
use tracing::{debug, error, info, trace};
use uvth::{ThreadPool, ThreadPoolBuilder};
use vek::*;
#[cfg(feature = "worldgen")]
@ -97,7 +99,7 @@ pub struct Server {
index: IndexOwned,
map: WorldMapMsg,
network: Network,
connection_handler: ConnectionHandler,
thread_pool: ThreadPool,
@ -332,6 +334,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,
@ -339,7 +342,7 @@ impl Server {
index,
map,
network,
connection_handler,
thread_pool,
@ -449,7 +452,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();
@ -523,11 +526,11 @@ impl Server {
CharacterLoaderResponseType::CharacterList(result) => match result {
Ok(character_list_data) => self.notify_client(
query_result.entity,
ServerMsg::CharacterListUpdate(character_list_data),
ServerGeneral::CharacterListUpdate(character_list_data),
),
Err(error) => self.notify_client(
query_result.entity,
ServerMsg::CharacterActionError(error.to_string()),
ServerGeneral::CharacterActionError(error.to_string()),
),
},
CharacterLoaderResponseType::CharacterData(result) => {
@ -542,7 +545,7 @@ impl Server {
// to display
self.notify_client(
query_result.entity,
ServerMsg::CharacterDataLoadError(error.to_string()),
ServerGeneral::CharacterDataLoadError(error.to_string()),
);
// Clean up the entity data on the server
@ -791,88 +794,65 @@ 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);
const SLOWLORIS_TIMEOUT: Duration = Duration::from_millis(300);
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");
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 singleton_stream = match select!(
_ = Delay::new(SLOWLORIS_TIMEOUT).fuse() => None,
sr = participant.opened().fuse() => Some(sr),
) {
None => {
warn!("Either Slowloris attack or very slow client, dropping");
return Ok(()); //return rather then continue to give removes a tick more to send data.
},
Some(Ok(s)) => s,
Some(Err(e)) => {
warn!(?e, "Failed to open a Stream from remote client. dropping");
continue;
},
};
let mut client = Client {
client_state: ClientState::Connected,
participant: std::sync::Mutex::new(Some(participant)),
singleton_stream,
network_error: std::sync::atomic::AtomicBool::new(false),
last_ping: self.state.get_time(),
login_msg_sent: false,
};
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()
{
// Note: in this case the client is dropped
client.notify(ServerMsg::TooManyPlayers);
} else {
let entity = self
.state
.ecs_mut()
.create_entity_synced()
.with(client)
.build();
self.state
.ecs()
.read_resource::<metrics::PlayerMetrics>()
.clients_connected
.inc();
// Send client all the tracked components currently attached to its entity as
// well as synced resources (currently only `TimeOfDay`)
debug!("Starting initial sync with client.");
self.state
.ecs()
.write_storage::<Client>()
.get_mut(entity)
.unwrap()
.notify(ServerMsg::InitialSync {
// Send client their entity
entity_package: TrackedComps::fetch(&self.state.ecs())
.create_entity_package(entity, None, None, None),
server_info: self.get_server_info(),
time_of_day: *self.state.ecs().read_resource(),
max_group_size: self.settings().max_player_group_size,
client_timeout: self.settings().client_timeout,
world_map: self.map.clone(),
recipe_book: (&*default_recipe_book()).clone(),
});
frontend_events.push(Event::ClientConnected { entity });
debug!("Done initial sync with client.");
trace!(
?client.participant,
"to many players, wont allow participant to connect"
);
client.register_stream.send(ServerInit::TooManyPlayers)?;
continue;
}
let entity = self
.state
.ecs_mut()
.create_entity_synced()
.with(client)
.build();
self.state
.ecs()
.read_resource::<metrics::PlayerMetrics>()
.clients_connected
.inc();
// Send client all the tracked components currently attached to its entity as
// well as synced resources (currently only `TimeOfDay`)
debug!("Starting initial sync with client.");
self.state
.ecs()
.write_storage::<Client>()
.get_mut(entity)
.unwrap()
.register_stream
.send(ServerInit::GameSync {
// Send client their entity
entity_package: TrackedComps::fetch(&self.state.ecs())
.create_entity_package(entity, None, None, None),
time_of_day: *self.state.ecs().read_resource(),
max_group_size: self.settings().max_player_group_size,
client_timeout: self.settings().client_timeout,
world_map: self.map.clone(),
recipe_book: (&*default_recipe_book()).clone(),
})?;
frontend_events.push(Event::ClientConnected { entity });
debug!("Done initial sync with client.");
}
Ok(())
}
pub fn notify_client<S>(&self, entity: EcsEntity, msg: S)
@ -880,11 +860,11 @@ impl Server {
S: Into<ServerMsg>,
{
if let Some(client) = self.state.ecs().write_storage::<Client>().get_mut(entity) {
client.notify(msg.into())
client.send_msg(msg.into())
}
}
pub fn notify_registered_clients(&mut self, msg: ServerMsg) {
pub fn notify_registered_clients(&mut self, msg: ServerGeneral) {
self.state.notify_registered_clients(msg);
}
@ -964,7 +944,7 @@ impl Server {
impl Drop for Server {
fn drop(&mut self) {
self.state
.notify_registered_clients(ServerMsg::Disconnect(DisconnectReason::Shutdown));
.notify_registered_clients(ServerGeneral::Disconnect(DisconnectReason::Shutdown));
}
}

View File

@ -5,7 +5,7 @@ use common::{
character::CharacterId,
comp,
effect::Effect,
msg::{CharacterInfo, ClientState, PlayerListUpdate, ServerMsg},
msg::{CharacterInfo, ClientInGame, PlayerListUpdate, ServerGeneral, ServerMsg},
state::State,
sync::{Uid, UidAllocator, WorldSyncExt},
util::Dir,
@ -59,7 +59,8 @@ pub trait StateExt {
fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents);
/// Iterates over registered clients and send each `ServerMsg`
fn send_chat(&self, msg: comp::UnresolvedChatMsg);
fn notify_registered_clients(&self, msg: ServerMsg);
fn notify_registered_clients(&self, msg: ServerGeneral);
fn notify_in_game_clients(&self, msg: ServerGeneral);
/// Delete an entity, recording the deletion in [`DeletedEntities`]
fn delete_entity_recorded(
&mut self,
@ -216,7 +217,8 @@ impl StateExt for State {
// Tell the client its request was successful.
if let Some(client) = self.ecs().write_storage::<Client>().get_mut(entity) {
client.allow_state(ClientState::Character);
client.in_game = Some(ClientInGame::Character);
client.send_msg(ServerGeneral::CharacterSuccess)
}
}
@ -225,7 +227,7 @@ impl StateExt for State {
if let Some(player_uid) = self.read_component_copied::<Uid>(entity) {
// Notify clients of a player list update
self.notify_registered_clients(ServerMsg::PlayerListUpdate(
self.notify_registered_clients(ServerGeneral::PlayerListUpdate(
PlayerListUpdate::SelectedCharacter(player_uid, CharacterInfo {
name: String::from(&stats.name),
level: stats.level.level(),
@ -272,7 +274,7 @@ impl StateExt for State {
| comp::ChatType::Kill(_, _)
| comp::ChatType::Meta
| comp::ChatType::World(_) => {
self.notify_registered_clients(ServerMsg::ChatMsg(resolved_msg))
self.notify_registered_clients(ServerGeneral::ChatMsg(resolved_msg))
},
comp::ChatType::Tell(u, t) => {
for (client, uid) in (
@ -282,7 +284,7 @@ impl StateExt for State {
.join()
{
if uid == u || uid == t {
client.notify(ServerMsg::ChatMsg(resolved_msg.clone()));
client.send_msg(ServerGeneral::ChatMsg(resolved_msg.clone()));
}
}
},
@ -294,7 +296,7 @@ impl StateExt for State {
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
if is_within(comp::ChatMsg::SAY_DISTANCE, pos, speaker_pos) {
client.notify(ServerMsg::ChatMsg(resolved_msg.clone()));
client.send_msg(ServerGeneral::ChatMsg(resolved_msg.clone()));
}
}
}
@ -306,7 +308,7 @@ impl StateExt for State {
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
if is_within(comp::ChatMsg::REGION_DISTANCE, pos, speaker_pos) {
client.notify(ServerMsg::ChatMsg(resolved_msg.clone()));
client.send_msg(ServerGeneral::ChatMsg(resolved_msg.clone()));
}
}
}
@ -318,7 +320,7 @@ impl StateExt for State {
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
if is_within(comp::ChatMsg::NPC_DISTANCE, pos, speaker_pos) {
client.notify(ServerMsg::ChatMsg(resolved_msg.clone()));
client.send_msg(ServerGeneral::ChatMsg(resolved_msg.clone()));
}
}
}
@ -332,7 +334,7 @@ impl StateExt for State {
.join()
{
if s == &faction.0 {
client.notify(ServerMsg::ChatMsg(resolved_msg.clone()));
client.send_msg(ServerGeneral::ChatMsg(resolved_msg.clone()));
}
}
},
@ -344,7 +346,7 @@ impl StateExt for State {
.join()
{
if g == group {
client.notify(ServerMsg::ChatMsg(resolved_msg.clone()));
client.send_msg(ServerGeneral::ChatMsg(resolved_msg.clone()));
}
}
},
@ -352,12 +354,24 @@ impl StateExt for State {
}
/// Sends the message to all connected clients
fn notify_registered_clients(&self, msg: ServerMsg) {
fn notify_registered_clients(&self, msg: ServerGeneral) {
let msg: ServerMsg = msg.into();
for client in (&mut self.ecs().write_storage::<Client>())
.join()
.filter(|c| c.is_registered())
.filter(|c| c.registered)
{
client.notify(msg.clone());
client.send_msg(msg.clone());
}
}
/// Sends the message to all clients playing in game
fn notify_in_game_clients(&self, msg: ServerGeneral) {
let msg: ServerMsg = msg.into();
for client in (&mut self.ecs().write_storage::<Client>())
.join()
.filter(|c| c.in_game.is_some())
{
client.send_msg(msg.clone());
}
}
@ -384,7 +398,7 @@ impl StateExt for State {
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.notify(ServerMsg::GroupUpdate(g)));
.map(|(g, c)| c.send_msg(ServerGeneral::GroupUpdate(g)));
},
);
}

View File

@ -8,7 +8,7 @@ use crate::{
};
use common::{
comp::{ForceUpdate, Inventory, InventoryUpdate, Last, Ori, Player, Pos, Vel},
msg::ServerMsg,
msg::ServerGeneral,
outcome::Outcome,
region::{Event as RegionEvent, RegionMap},
span,
@ -107,7 +107,7 @@ impl<'a> System<'a> for Sys {
let mut subscribers = (&mut clients, &entities, &subscriptions, &positions)
.join()
.filter_map(|(client, entity, subscription, pos)| {
if client.is_ingame() && subscription.regions.contains(&key) {
if client.in_game.is_some() && subscription.regions.contains(&key) {
Some((client, &subscription.regions, entity, *pos))
} else {
None
@ -129,7 +129,7 @@ impl<'a> System<'a> for Sys {
})
}) {
let create_msg =
ServerMsg::CreateEntity(tracked_comps.create_entity_package(
ServerGeneral::CreateEntity(tracked_comps.create_entity_package(
entity,
Some(*pos),
vel.copied(),
@ -143,7 +143,7 @@ impl<'a> System<'a> for Sys {
// Client doesn't need to know about itself
&& *client_entity != entity
{
client.notify(create_msg.clone());
client.send_msg(create_msg.clone());
}
}
}
@ -157,7 +157,7 @@ impl<'a> System<'a> for Sys {
.map(|key| !regions.contains(key))
.unwrap_or(true)
{
client.notify(ServerMsg::DeleteEntity(uid));
client.send_msg(ServerGeneral::DeleteEntity(uid));
}
}
}
@ -174,14 +174,14 @@ impl<'a> System<'a> for Sys {
.take_deleted_in_region(key)
.unwrap_or_default(),
);
let entity_sync_msg = ServerMsg::EntitySync(entity_sync_package);
let comp_sync_msg = ServerMsg::CompSync(comp_sync_package);
let entity_sync_msg = ServerGeneral::EntitySync(entity_sync_package);
let comp_sync_msg = ServerGeneral::CompSync(comp_sync_package);
subscribers.iter_mut().for_each(move |(client, _, _, _)| {
client.notify(entity_sync_msg.clone());
client.notify(comp_sync_msg.clone());
client.send_msg(entity_sync_msg.clone());
client.send_msg(comp_sync_msg.clone());
});
let mut send_msg = |msg: ServerMsg,
let mut send_msg = |msg: ServerGeneral,
entity: EcsEntity,
pos: Pos,
force_update: Option<&ForceUpdate>,
@ -212,7 +212,7 @@ impl<'a> System<'a> for Sys {
true // Closer than 100 blocks
}
} {
client.notify(msg.clone());
client.send_msg(msg.clone());
}
}
};
@ -287,7 +287,7 @@ impl<'a> System<'a> for Sys {
}
send_msg(
ServerMsg::CompSync(comp_sync_package),
ServerGeneral::CompSync(comp_sync_package),
entity,
pos,
force_update,
@ -303,7 +303,7 @@ impl<'a> System<'a> for Sys {
(&mut clients, &subscriptions)
.join()
.filter_map(|(client, subscription)| {
if client.is_ingame() && subscription.regions.contains(&region_key) {
if client.in_game.is_some() && subscription.regions.contains(&region_key) {
Some(client)
} else {
None
@ -311,7 +311,7 @@ impl<'a> System<'a> for Sys {
})
{
for uid in &deleted {
client.notify(ServerMsg::DeleteEntity(Uid(*uid)));
client.send_msg(ServerGeneral::DeleteEntity(Uid(*uid)));
}
}
}
@ -320,7 +320,7 @@ impl<'a> System<'a> for Sys {
// Sync inventories
for (client, inventory, update) in (&mut clients, &inventories, &inventory_updates).join() {
client.notify(ServerMsg::InventoryUpdate(
client.send_msg(ServerGeneral::InventoryUpdate(
inventory.clone(),
update.event(),
));
@ -341,7 +341,7 @@ impl<'a> System<'a> for Sys {
.cloned()
.collect::<Vec<_>>();
if !outcomes.is_empty() {
client.notify(ServerMsg::Outcomes(outcomes));
client.send_msg(ServerGeneral::Outcomes(outcomes));
}
}
outcomes.clear();
@ -353,9 +353,9 @@ impl<'a> System<'a> for Sys {
// Sync resources
// TODO: doesn't really belong in this system (rename system or create another
// system?)
let tof_msg = ServerMsg::TimeOfDay(*time_of_day);
let tof_msg = ServerGeneral::TimeOfDay(*time_of_day);
for client in (&mut clients).join() {
client.notify(tof_msg.clone());
client.send_msg(tof_msg.clone());
}
timer.end();

View File

@ -2,7 +2,7 @@ use super::SysTimer;
use crate::client::Client;
use common::{
comp::group::{Invite, PendingInvites},
msg::{InviteAnswer, ServerMsg},
msg::{InviteAnswer, ServerGeneral},
span,
sync::Uid,
};
@ -54,7 +54,7 @@ impl<'a> System<'a> for Sys {
if let (Some(client), Some(target)) =
(clients.get_mut(*inviter), uids.get(invitee).copied())
{
client.notify(ServerMsg::InviteComplete {
client.send_msg(ServerGeneral::InviteComplete {
target,
answer: InviteAnswer::TimedOut,
})

View File

@ -15,9 +15,9 @@ use common::{
},
event::{EventBus, ServerEvent},
msg::{
validate_chat_msg, CharacterInfo, ChatMsgValidationError, ClientMsg, ClientState,
DisconnectReason, PlayerInfo, PlayerListUpdate, RequestStateError, ServerMsg,
MAX_BYTES_CHAT_MSG,
validate_chat_msg, CharacterInfo, ChatMsgValidationError, ClientGeneral, ClientInGame,
ClientRegister, DisconnectReason, PingMsg, PlayerInfo, PlayerListUpdate, RegisterError,
ServerGeneral, ServerRegisterAnswer, MAX_BYTES_CHAT_MSG,
},
span,
state::{BlockChange, Time},
@ -32,13 +32,392 @@ use hashbrown::HashMap;
use specs::{
Entities, Join, Read, ReadExpect, ReadStorage, System, Write, WriteExpect, WriteStorage,
};
use tracing::{debug, error, info, warn};
use tracing::{debug, error, info, trace, warn};
impl Sys {
#[allow(clippy::too_many_arguments)]
fn handle_client_msg(
server_emitter: &mut common::event::Emitter<'_, ServerEvent>,
new_chat_msgs: &mut Vec<(Option<specs::Entity>, UnresolvedChatMsg)>,
entity: specs::Entity,
client: &mut Client,
player_metrics: &ReadExpect<'_, PlayerMetrics>,
uids: &ReadStorage<'_, Uid>,
chat_modes: &ReadStorage<'_, ChatMode>,
msg: ClientGeneral,
) -> Result<(), crate::error::Error> {
match msg {
ClientGeneral::ChatMsg(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")
},
}
}
},
ClientGeneral::Disconnect => {
client.send_msg(ServerGeneral::Disconnect(DisconnectReason::Requested));
},
ClientGeneral::Terminate => {
debug!(?entity, "Client send message to termitate session");
player_metrics
.clients_disconnected
.with_label_values(&["gracefully"])
.inc();
server_emitter.emit(ServerEvent::ClientDisconnect(entity));
},
_ => unreachable!("not a client_general msg"),
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn handle_client_in_game_msg(
server_emitter: &mut common::event::Emitter<'_, ServerEvent>,
entity: specs::Entity,
client: &mut Client,
terrain: &ReadExpect<'_, TerrainGrid>,
network_metrics: &ReadExpect<'_, NetworkRequestMetrics>,
can_build: &ReadStorage<'_, CanBuild>,
force_updates: &ReadStorage<'_, ForceUpdate>,
stats: &mut WriteStorage<'_, Stats>,
block_changes: &mut Write<'_, BlockChange>,
positions: &mut WriteStorage<'_, Pos>,
velocities: &mut WriteStorage<'_, Vel>,
orientations: &mut WriteStorage<'_, Ori>,
players: &mut WriteStorage<'_, Player>,
controllers: &mut WriteStorage<'_, Controller>,
settings: &Read<'_, Settings>,
msg: ClientGeneral,
) -> Result<(), crate::error::Error> {
if client.in_game.is_none() {
debug!(?entity, "client is not in_game, ignoring msg");
trace!(?msg, "ignored msg content");
if matches!(msg, ClientGeneral::TerrainChunkRequest{ .. }) {
network_metrics.chunks_request_dropped.inc();
}
return Ok(());
}
match msg {
// Go back to registered state (char selection screen)
ClientGeneral::ExitInGame => {
client.in_game = None;
server_emitter.emit(ServerEvent::ExitIngame { entity });
client.send_msg(ServerGeneral::ExitInGameSuccess);
},
ClientGeneral::SetViewDistance(view_distance) => {
players.get_mut(entity).map(|player| {
player.view_distance = Some(
settings
.max_view_distance
.map(|max| view_distance.min(max))
.unwrap_or(view_distance),
)
});
//correct client if its VD is to high
if settings
.max_view_distance
.map(|max| view_distance > max)
.unwrap_or(false)
{
client.send_msg(ServerGeneral::SetViewDistance(
settings.max_view_distance.unwrap_or(0),
));
}
},
ClientGeneral::ControllerInputs(inputs) => {
if let Some(ClientInGame::Character) = client.in_game {
if let Some(controller) = controllers.get_mut(entity) {
controller.inputs.update_with_new(inputs);
}
}
},
ClientGeneral::ControlEvent(event) => {
if let Some(ClientInGame::Character) = client.in_game {
// Skip respawn if client entity is alive
if let ControlEvent::Respawn = event {
if stats.get(entity).map_or(true, |s| !s.is_dead) {
//Todo: comment why return!
return Ok(());
}
}
if let Some(controller) = controllers.get_mut(entity) {
controller.events.push(event);
}
}
},
ClientGeneral::ControlAction(event) => {
if let Some(ClientInGame::Character) = client.in_game {
if let Some(controller) = controllers.get_mut(entity) {
controller.actions.push(event);
}
}
},
ClientGeneral::PlayerPhysics { pos, vel, ori } => {
if let Some(ClientInGame::Character) = client.in_game {
if force_updates.get(entity).is_none()
&& stats.get(entity).map_or(true, |s| !s.is_dead)
{
let _ = positions.insert(entity, pos);
let _ = velocities.insert(entity, vel);
let _ = orientations.insert(entity, ori);
}
}
},
ClientGeneral::BreakBlock(pos) => {
if let Some(block) = can_build.get(entity).and_then(|_| terrain.get(pos).ok()) {
block_changes.set(pos, block.into_vacant());
}
},
ClientGeneral::PlaceBlock(pos, block) => {
if can_build.get(entity).is_some() {
block_changes.try_set(pos, block);
}
},
ClientGeneral::TerrainChunkRequest { key } => {
let in_vd = if let (Some(view_distance), Some(pos)) = (
players.get(entity).and_then(|p| p.view_distance),
positions.get(entity),
) {
pos.0.xy().map(|e| e as f64).distance(
key.map(|e| e as f64 + 0.5) * TerrainChunkSize::RECT_SIZE.map(|e| e as f64),
) < (view_distance as f64 - 1.0 + 2.5 * 2.0_f64.sqrt())
* TerrainChunkSize::RECT_SIZE.x as f64
} else {
true
};
if in_vd {
match terrain.get_key(key) {
Some(chunk) => {
network_metrics.chunks_served_from_memory.inc();
client.send_msg(ServerGeneral::TerrainChunkUpdate {
key,
chunk: Ok(Box::new(chunk.clone())),
})
},
None => {
network_metrics.chunks_generation_triggered.inc();
server_emitter.emit(ServerEvent::ChunkRequest(entity, key))
},
}
} else {
network_metrics.chunks_request_dropped.inc();
}
},
ClientGeneral::UnlockSkill(skill) => {
stats
.get_mut(entity)
.map(|s| s.skill_set.unlock_skill(skill));
},
ClientGeneral::RefundSkill(skill) => {
stats
.get_mut(entity)
.map(|s| s.skill_set.refund_skill(skill));
},
ClientGeneral::UnlockSkillGroup(skill_group_type) => {
stats
.get_mut(entity)
.map(|s| s.skill_set.unlock_skill_group(skill_group_type));
},
_ => unreachable!("not a client_in_game msg"),
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn handle_client_character_screen_msg(
server_emitter: &mut common::event::Emitter<'_, ServerEvent>,
new_chat_msgs: &mut Vec<(Option<specs::Entity>, UnresolvedChatMsg)>,
entity: specs::Entity,
client: &mut Client,
character_loader: &ReadExpect<'_, CharacterLoader>,
uids: &ReadStorage<'_, Uid>,
players: &mut WriteStorage<'_, Player>,
editable_settings: &ReadExpect<'_, EditableSettings>,
alias_validator: &ReadExpect<'_, AliasValidator>,
msg: ClientGeneral,
) -> Result<(), crate::error::Error> {
match msg {
// Request spectator state
ClientGeneral::Spectate if client.registered => {
client.in_game = Some(ClientInGame::Spectator)
},
ClientGeneral::Spectate => debug!("dropped Spectate msg from unregistered client"),
ClientGeneral::Character(character_id)
if client.registered && client.in_game.is_none() =>
{
if let Some(player) = players.get(entity) {
// Send a request to load the character's component data from the
// DB. Once loaded, persisted components such as stats and inventory
// will be inserted for the entity
character_loader.load_character_data(
entity,
player.uuid().to_string(),
character_id,
);
// Start inserting non-persisted/default components for the entity
// while we load the DB data
server_emitter.emit(ServerEvent::InitCharacterData {
entity,
character_id,
});
// Give the player a welcome message
if !editable_settings.server_description.is_empty() {
client.send_msg(
ChatType::CommandInfo
.server_msg(String::from(&*editable_settings.server_description)),
);
}
if !client.login_msg_sent {
if let Some(player_uid) = uids.get(entity) {
new_chat_msgs.push((None, UnresolvedChatMsg {
chat_type: ChatType::Online(*player_uid),
message: "".to_string(),
}));
client.login_msg_sent = true;
}
}
} else {
client.send_msg(ServerGeneral::CharacterDataLoadError(String::from(
"Failed to fetch player entity",
)))
}
}
ClientGeneral::Character(_) => {
let registered = client.registered;
let in_game = client.in_game;
debug!(?registered, ?in_game, "dropped Character msg from client");
},
ClientGeneral::RequestCharacterList => {
if let Some(player) = players.get(entity) {
character_loader.load_character_list(entity, player.uuid().to_string())
}
},
ClientGeneral::CreateCharacter { alias, tool, body } => {
if let Err(error) = alias_validator.validate(&alias) {
debug!(?error, ?alias, "denied alias as it contained a banned word");
client.send_msg(ServerGeneral::CharacterActionError(error.to_string()));
} else if let Some(player) = players.get(entity) {
character_creator::create_character(
entity,
player.uuid().to_string(),
alias,
tool,
body,
character_loader,
);
}
},
ClientGeneral::DeleteCharacter(character_id) => {
if let Some(player) = players.get(entity) {
character_loader.delete_character(
entity,
player.uuid().to_string(),
character_id,
);
}
},
_ => unreachable!("not a client_character_screen msg"),
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn handle_ping_msg(client: &mut Client, msg: PingMsg) -> Result<(), crate::error::Error> {
match msg {
PingMsg::Ping => client.send_msg(PingMsg::Pong),
PingMsg::Pong => {},
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn handle_register_msg(
player_list: &HashMap<Uid, PlayerInfo>,
new_players: &mut Vec<specs::Entity>,
entity: specs::Entity,
client: &mut Client,
player_metrics: &ReadExpect<'_, PlayerMetrics>,
login_provider: &mut WriteExpect<'_, LoginProvider>,
admins: &mut WriteStorage<'_, Admin>,
players: &mut WriteStorage<'_, Player>,
editable_settings: &ReadExpect<'_, EditableSettings>,
msg: ClientRegister,
) -> Result<(), crate::error::Error> {
let (username, uuid) = match login_provider.try_login(
&msg.token_or_username,
&*editable_settings.admins,
&*editable_settings.whitelist,
&*editable_settings.banlist,
) {
Err(err) => {
client
.register_stream
.send(ServerRegisterAnswer::Err(err))?;
return Ok(());
},
Ok((username, uuid)) => (username, uuid),
};
const INITIAL_VD: Option<u32> = Some(5); //will be changed after login
let player = Player::new(username, None, INITIAL_VD, uuid);
let is_admin = editable_settings.admins.contains(&uuid);
if !player.is_valid() {
// Invalid player
client
.register_stream
.send(ServerRegisterAnswer::Err(RegisterError::InvalidCharacter))?;
return Ok(());
}
if !client.registered && client.in_game.is_none() {
// Add Player component to this client
let _ = players.insert(entity, player);
player_metrics.players_connected.inc();
// Give the Admin component to the player if their name exists in
// admin list
if is_admin {
let _ = admins.insert(entity, Admin);
}
// Tell the client its request was successful.
client.registered = true;
client.register_stream.send(ServerRegisterAnswer::Ok(()))?;
// Send initial player list
client.send_msg(ServerGeneral::PlayerListUpdate(PlayerListUpdate::Init(
player_list.clone(),
)));
// Add to list to notify all clients of the new player
new_players.push(entity);
}
Ok(())
}
///We needed to move this to a async fn, if we would use a async closures
/// the compiler generates to much recursion and fails to compile this
#[allow(clippy::too_many_arguments)]
async fn handle_client_msg(
async fn handle_messages(
server_emitter: &mut common::event::Emitter<'_, ServerEvent>,
new_chat_msgs: &mut Vec<(Option<specs::Entity>, UnresolvedChatMsg)>,
player_list: &HashMap<Uid, PlayerInfo>,
@ -64,357 +443,103 @@ impl Sys {
players: &mut WriteStorage<'_, Player>,
controllers: &mut WriteStorage<'_, Controller>,
settings: &Read<'_, Settings>,
editable_settings: &ReadExpect<'_, EditableSettings>,
alias_validator: &ReadExpect<'_, AliasValidator>,
editable_settings: &EditableSettings,
) -> Result<(), crate::error::Error> {
let (mut b1, mut b2, mut b3, mut b4, mut b5) = (
client.network_error,
client.network_error,
client.network_error,
client.network_error,
client.network_error,
);
loop {
let msg = client.recv().await?;
/*
waiting for 1 of the 5 streams to return a massage asynchronous.
If so, handle that msg type. This code will be refactored soon
*/
let q1 = Client::internal_recv(&mut b1, &mut client.general_stream);
let q2 = Client::internal_recv(&mut b2, &mut client.in_game_stream);
let q3 = Client::internal_recv(&mut b3, &mut client.character_screen_stream);
let q4 = Client::internal_recv(&mut b4, &mut client.ping_stream);
let q5 = Client::internal_recv(&mut b5, &mut client.register_stream);
let (m1, m2, m3, m4, m5) = select!(
msg = q1.fuse() => (Some(msg), None, None, None, None),
msg = q2.fuse() => (None, Some(msg), None, None, None),
msg = q3.fuse() => (None, None, Some(msg), None, None),
msg = q4.fuse() => (None, None, None, Some(msg), None),
msg = q5.fuse() => (None, None, None, None,Some(msg)),
);
*cnt += 1;
match msg {
// Go back to registered state (char selection screen)
ClientMsg::ExitIngame => match client.client_state {
// Use ClientMsg::Register instead.
ClientState::Connected => client.error_state(RequestStateError::WrongMessage),
ClientState::Registered => client.error_state(RequestStateError::Already),
ClientState::Spectator | ClientState::Character => {
server_emitter.emit(ServerEvent::ExitIngame { entity });
},
ClientState::Pending => {},
},
// Request spectator state
ClientMsg::Spectate => match client.client_state {
// Become Registered first.
ClientState::Connected => client.error_state(RequestStateError::Impossible),
ClientState::Spectator => client.error_state(RequestStateError::Already),
ClientState::Registered | ClientState::Character => {
client.allow_state(ClientState::Spectator)
},
ClientState::Pending => {},
},
// Request registered state (login)
ClientMsg::Register {
view_distance,
token_or_username,
} => {
let (username, uuid) = match login_provider.try_login(
&token_or_username,
&*editable_settings.admins,
&*editable_settings.whitelist,
&*editable_settings.banlist,
) {
Err(err) => {
client.error_state(RequestStateError::RegisterDenied(err));
break Ok(());
},
Ok((username, uuid)) => (username, uuid),
};
let vd =
view_distance.map(|vd| vd.min(settings.max_view_distance.unwrap_or(vd)));
let is_admin = editable_settings.admins.contains(&uuid);
let player = Player::new(username.clone(), None, vd, uuid);
if !player.is_valid() {
// Invalid player
client.error_state(RequestStateError::Impossible);
break Ok(());
}
match client.client_state {
ClientState::Connected => {
// Add Player component to this client
let _ = players.insert(entity, player);
player_metrics.players_connected.inc();
// Give the Admin component to the player if their name exists in
// admin list
if is_admin {
let _ = admins.insert(entity, Admin);
}
// 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),
}
//client.allow_state(ClientState::Registered);
// 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(view_distance)
.map(|(max, vd)| vd > max)
.unwrap_or(false)
{
client.notify(ServerMsg::SetViewDistance(
settings.max_view_distance.unwrap_or(0),
));
};
},
ClientMsg::SetViewDistance(view_distance) => {
if let ClientState::Character { .. } = client.client_state {
players.get_mut(entity).map(|player| {
player.view_distance = Some(
settings
.max_view_distance
.map(|max| view_distance.min(max))
.unwrap_or(view_distance),
)
});
if settings
.max_view_distance
.map(|max| view_distance > max)
.unwrap_or(false)
{
client.notify(ServerMsg::SetViewDistance(
settings.max_view_distance.unwrap_or(0),
));
}
}
},
ClientMsg::Character(character_id) => match client.client_state {
// Become Registered first.
ClientState::Connected => client.error_state(RequestStateError::Impossible),
ClientState::Registered | ClientState::Spectator => {
// Only send login message if it wasn't already
// sent previously
if let Some(player) = players.get(entity) {
// Send a request to load the character's component data from the
// DB. Once loaded, persisted components such as stats and inventory
// will be inserted for the entity
character_loader.load_character_data(
entity,
player.uuid().to_string(),
character_id,
);
// Start inserting non-persisted/default components for the entity
// while we load the DB data
server_emitter.emit(ServerEvent::InitCharacterData {
entity,
character_id,
});
// Give the player a welcome message
if !editable_settings.server_description.is_empty() {
client.notify(ChatType::CommandInfo.server_msg(String::from(
&*editable_settings.server_description,
)));
}
// Only send login message if it wasn't already
// sent previously
if !client.login_msg_sent {
if let Some(player_uid) = uids.get(entity) {
new_chat_msgs.push((None, UnresolvedChatMsg {
chat_type: ChatType::Online(*player_uid),
message: "".to_string(),
}));
client.login_msg_sent = true;
}
}
} else {
client.notify(ServerMsg::CharacterDataLoadError(String::from(
"Failed to fetch player entity",
)))
}
},
ClientState::Character => client.error_state(RequestStateError::Already),
ClientState::Pending => {},
},
ClientMsg::ControllerInputs(inputs) => match client.client_state {
ClientState::Connected | ClientState::Registered | ClientState::Spectator => {
client.error_state(RequestStateError::Impossible)
},
ClientState::Character => {
if let Some(controller) = controllers.get_mut(entity) {
controller.inputs.update_with_new(inputs);
}
},
ClientState::Pending => {},
},
ClientMsg::ControlEvent(event) => match client.client_state {
ClientState::Connected | ClientState::Registered | ClientState::Spectator => {
client.error_state(RequestStateError::Impossible)
},
ClientState::Character => {
// Skip respawn if client entity is alive
if let ControlEvent::Respawn = event {
if stats.get(entity).map_or(true, |s| !s.is_dead) {
continue;
}
}
if let Some(controller) = controllers.get_mut(entity) {
controller.events.push(event);
}
},
ClientState::Pending => {},
},
ClientMsg::ControlAction(event) => match client.client_state {
ClientState::Connected | ClientState::Registered | ClientState::Spectator => {
client.error_state(RequestStateError::Impossible)
},
ClientState::Character => {
if let Some(controller) = controllers.get_mut(entity) {
controller.actions.push(event);
}
},
ClientState::Pending => {},
},
ClientMsg::ChatMsg(message) => match client.client_state {
ClientState::Connected => client.error_state(RequestStateError::Impossible),
ClientState::Registered | ClientState::Spectator | ClientState::Character => {
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")
},
}
},
ClientState::Pending => {},
},
ClientMsg::PlayerPhysics { pos, vel, ori } => match client.client_state {
ClientState::Character => {
if force_updates.get(entity).is_none()
&& stats.get(entity).map_or(true, |s| !s.is_dead)
{
let _ = positions.insert(entity, pos);
let _ = velocities.insert(entity, vel);
let _ = orientations.insert(entity, ori);
}
},
// Only characters can send positions.
_ => client.error_state(RequestStateError::Impossible),
},
ClientMsg::BreakBlock(pos) => {
if let Some(block) = can_build.get(entity).and_then(|_| terrain.get(pos).ok()) {
block_changes.set(pos, block.into_vacant());
}
},
ClientMsg::PlaceBlock(pos, block) => {
if can_build.get(entity).is_some() {
block_changes.try_set(pos, block);
}
},
ClientMsg::TerrainChunkRequest { key } => match client.client_state {
ClientState::Connected | ClientState::Registered => {
network_metrics.chunks_request_dropped.inc();
client.error_state(RequestStateError::Impossible);
},
ClientState::Spectator | ClientState::Character => {
let in_vd = if let (Some(view_distance), Some(pos)) = (
players.get(entity).and_then(|p| p.view_distance),
positions.get(entity),
) {
pos.0.xy().map(|e| e as f64).distance(
key.map(|e| e as f64 + 0.5)
* TerrainChunkSize::RECT_SIZE.map(|e| e as f64),
) < (view_distance as f64 - 1.0 + 2.5 * 2.0_f64.sqrt())
* TerrainChunkSize::RECT_SIZE.x as f64
} else {
true
};
if in_vd {
match terrain.get_key(key) {
Some(chunk) => {
network_metrics.chunks_served_from_memory.inc();
client.notify(ServerMsg::TerrainChunkUpdate {
key,
chunk: Ok(Box::new(chunk.clone())),
})
},
None => {
network_metrics.chunks_generation_triggered.inc();
server_emitter.emit(ServerEvent::ChunkRequest(entity, key))
},
}
} else {
network_metrics.chunks_request_dropped.inc();
}
},
ClientState::Pending => {},
},
// Always possible.
ClientMsg::Ping => client.notify(ServerMsg::Pong),
ClientMsg::Pong => {},
ClientMsg::Disconnect => {
client.notify(ServerMsg::Disconnect(DisconnectReason::Requested));
},
ClientMsg::Terminate => {
debug!(?entity, "Client send message to termitate session");
player_metrics
.clients_disconnected
.with_label_values(&["gracefully"])
.inc();
server_emitter.emit(ServerEvent::ClientDisconnect(entity));
break Ok(());
},
ClientMsg::RequestCharacterList => {
if let Some(player) = players.get(entity) {
character_loader.load_character_list(entity, player.uuid().to_string())
}
},
ClientMsg::CreateCharacter { alias, tool, body } => {
if let Err(error) = alias_validator.validate(&alias) {
debug!(?error, ?alias, "denied alias as it contained a banned word");
client.notify(ServerMsg::CharacterActionError(error.to_string()));
} else if let Some(player) = players.get(entity) {
character_creator::create_character(
entity,
player.uuid().to_string(),
alias,
tool,
body,
character_loader,
);
}
},
ClientMsg::DeleteCharacter(character_id) => {
if let Some(player) = players.get(entity) {
character_loader.delete_character(
entity,
player.uuid().to_string(),
character_id,
);
}
},
ClientMsg::UnlockSkill(skill) => {
stats
.get_mut(entity)
.map(|s| s.skill_set.unlock_skill(skill));
},
ClientMsg::RefundSkill(skill) => {
stats
.get_mut(entity)
.map(|s| s.skill_set.refund_skill(skill));
},
ClientMsg::UnlockSkillGroup(skill_group_type) => {
stats
.get_mut(entity)
.map(|s| s.skill_set.unlock_skill_group(skill_group_type));
},
if let Some(msg) = m1 {
client.network_error |= b1;
Self::handle_client_msg(
server_emitter,
new_chat_msgs,
entity,
client,
player_metrics,
uids,
chat_modes,
msg?,
)?;
}
if let Some(msg) = m2 {
client.network_error |= b2;
Self::handle_client_in_game_msg(
server_emitter,
entity,
client,
terrain,
network_metrics,
can_build,
force_updates,
stats,
block_changes,
positions,
velocities,
orientations,
players,
controllers,
settings,
msg?,
)?;
}
if let Some(msg) = m3 {
client.network_error |= b3;
Self::handle_client_character_screen_msg(
server_emitter,
new_chat_msgs,
entity,
client,
character_loader,
uids,
players,
editable_settings,
alias_validator,
msg?,
)?;
}
if let Some(msg) = m4 {
client.network_error |= b4;
Self::handle_ping_msg(client, msg?)?;
}
if let Some(msg) = m5 {
client.network_error |= b5;
Self::handle_register_msg(
player_list,
new_players,
entity,
client,
player_metrics,
login_provider,
admins,
players,
editable_settings,
msg?,
)?;
}
}
}
@ -448,8 +573,8 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Client>,
WriteStorage<'a, Controller>,
Read<'a, Settings>,
ReadExpect<'a, AliasValidator>,
ReadExpect<'a, EditableSettings>,
ReadExpect<'a, AliasValidator>,
);
#[allow(clippy::match_ref_pats)] // TODO: Pending review in #587
@ -481,8 +606,8 @@ impl<'a> System<'a> for Sys {
mut clients,
mut controllers,
settings,
alias_validator,
editable_settings,
alias_validator,
): Self::SystemData,
) {
span!(_guard, "run", "message::Sys::run");
@ -515,7 +640,7 @@ impl<'a> System<'a> for Sys {
let network_err: Result<(), crate::error::Error> = block_on(async {
//TIMEOUT 0.02 ms for msg handling
let work_future = Self::handle_client_msg(
let work_future = Self::handle_messages(
&mut server_emitter,
&mut new_chat_msgs,
&player_list,
@ -541,8 +666,8 @@ impl<'a> System<'a> for Sys {
&mut players,
&mut controllers,
&settings,
&alias_validator,
&editable_settings,
&alias_validator,
);
select!(
_ = Delay::new(std::time::Duration::from_micros(20)).fuse() => Ok(()),
@ -550,8 +675,16 @@ impl<'a> System<'a> for Sys {
)
});
// Update client ping.
if cnt > 0 {
// Network error
if network_err.is_err() {
debug!(?entity, "postbox error with client, disconnecting");
player_metrics
.clients_disconnected
.with_label_values(&["network_error"])
.inc();
server_emitter.emit(ServerEvent::ClientDisconnect(entity));
} else if cnt > 0 {
// Update client ping.
client.last_ping = time.0
} else if time.0 - client.last_ping > settings.client_timeout.as_secs() as f64
// Timeout
@ -562,18 +695,9 @@ impl<'a> System<'a> for Sys {
.with_label_values(&["timeout"])
.inc();
server_emitter.emit(ServerEvent::ClientDisconnect(entity));
} else if network_err.is_err()
// Postbox error
{
debug!(?entity, "postbox error with client, disconnecting");
player_metrics
.clients_disconnected
.with_label_values(&["network_error"])
.inc();
server_emitter.emit(ServerEvent::ClientDisconnect(entity));
} else if time.0 - client.last_ping > settings.client_timeout.as_secs() as f64 * 0.5 {
// Try pinging the client if the timeout is nearing.
client.notify(ServerMsg::Ping);
client.send_msg(PingMsg::Ping);
}
}
@ -581,14 +705,15 @@ 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 = ServerMsg::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.is_registered()) {
client.notify(msg.clone())
let msg =
ServerGeneral::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

@ -5,7 +5,7 @@ use super::{
use crate::client::{self, Client, RegionSubscription};
use common::{
comp::{Ori, Player, Pos, Vel},
msg::ServerMsg,
msg::ServerGeneral,
region::{region_in_vd, regions_in_vd, Event as RegionEvent, RegionMap},
span,
sync::Uid,
@ -80,7 +80,7 @@ impl<'a> System<'a> for Sys {
)
.join()
.filter_map(|(client, s, pos, player, e)| {
if client.is_ingame() {
if client.in_game.is_some() {
player.view_distance.map(|v| (client, s, pos, v, e))
} else {
None
@ -153,7 +153,7 @@ impl<'a> System<'a> for Sys {
.map(|key| subscription.regions.contains(key))
.unwrap_or(false)
{
client.notify(ServerMsg::DeleteEntity(uid));
client.send_msg(ServerGeneral::DeleteEntity(uid));
}
}
},
@ -161,7 +161,7 @@ impl<'a> System<'a> for Sys {
}
// Tell client to delete entities in the region
for (&uid, _) in (&uids, region.entities()).join() {
client.notify(ServerMsg::DeleteEntity(uid));
client.send_msg(ServerGeneral::DeleteEntity(uid));
}
}
// Send deleted entities since they won't be processed for this client in entity
@ -171,7 +171,7 @@ impl<'a> System<'a> for Sys {
.iter()
.flat_map(|v| v.iter())
{
client.notify(ServerMsg::DeleteEntity(Uid(*uid)));
client.send_msg(ServerGeneral::DeleteEntity(Uid(*uid)));
}
}
@ -196,7 +196,7 @@ impl<'a> System<'a> for Sys {
{
// Send message to create entity and tracked components and physics
// components
client.notify(ServerMsg::CreateEntity(
client.send_msg(ServerGeneral::CreateEntity(
tracked_comps.create_entity_package(
entity,
Some(*pos),
@ -249,7 +249,7 @@ pub fn initialize_region_subscription(world: &World, entity: specs::Entity) {
.join()
{
// Send message to create entity and tracked components and physics components
client.notify(ServerMsg::CreateEntity(
client.send_msg(ServerGeneral::CreateEntity(
tracked_comps.create_entity_package(
entity,
Some(*pos),

View File

@ -4,7 +4,7 @@ use common::{
comp::{self, bird_medium, Alignment, Player, Pos},
event::{EventBus, ServerEvent},
generation::get_npc_name,
msg::ServerMsg,
msg::ServerGeneral,
npc::NPC_NAMES,
span,
state::TerrainChanges,
@ -63,7 +63,7 @@ impl<'a> System<'a> for Sys {
Ok((chunk, supplement)) => (chunk, supplement),
Err(Some(entity)) => {
if let Some(client) = clients.get_mut(entity) {
client.notify(ServerMsg::TerrainChunkUpdate {
client.send_msg(ServerGeneral::TerrainChunkUpdate {
key,
chunk: Err(()),
});
@ -90,7 +90,7 @@ impl<'a> System<'a> for Sys {
.magnitude_squared();
if adjusted_dist_sqr <= view_distance.pow(2) {
client.notify(ServerMsg::TerrainChunkUpdate {
client.send_msg(ServerGeneral::TerrainChunkUpdate {
key,
chunk: Ok(Box::new(chunk.clone())),
});

View File

@ -2,7 +2,7 @@ use super::SysTimer;
use crate::client::Client;
use common::{
comp::{Player, Pos},
msg::ServerMsg,
msg::ServerGeneral,
span,
state::TerrainChanges,
terrain::TerrainGrid,
@ -38,7 +38,7 @@ impl<'a> System<'a> for Sys {
.map(|vd| super::terrain::chunk_in_vd(pos.0, *chunk_key, &terrain, vd))
.unwrap_or(false)
{
client.notify(ServerMsg::TerrainChunkUpdate {
client.send_msg(ServerGeneral::TerrainChunkUpdate {
key: *chunk_key,
chunk: Ok(Box::new(match terrain.get_key(*chunk_key) {
Some(chunk) => chunk.clone(),
@ -51,10 +51,10 @@ impl<'a> System<'a> for Sys {
// TODO: Don't send all changed blocks to all clients
// Sync changed blocks
let msg = ServerMsg::TerrainBlockUpdates(terrain_changes.modified_blocks.clone());
let msg = ServerGeneral::TerrainBlockUpdates(terrain_changes.modified_blocks.clone());
for (player, client) in (&players, &mut clients).join() {
if player.view_distance.is_some() {
client.notify(msg.clone());
client.send_msg(msg.clone());
}
}

View File

@ -2,7 +2,7 @@ use super::SysTimer;
use crate::client::Client;
use common::{
comp::{Player, Pos, Waypoint, WaypointArea},
msg::{Notification, ServerMsg},
msg::{Notification, ServerGeneral},
span,
state::Time,
};
@ -42,7 +42,8 @@ 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.notify(ServerMsg::Notification(Notification::WaypointSaved));
client
.send_msg(ServerGeneral::Notification(Notification::WaypointSaved));
}
}
}

View File

@ -10,7 +10,7 @@ use crate::{
Direction, GlobalState, PlayState, PlayStateResult,
};
use client::{self, Client};
use common::{assets::Asset, comp, msg::ClientState, span, state::DeltaTime};
use common::{assets::Asset, comp, span, state::DeltaTime};
use specs::WorldExt;
use std::{cell::RefCell, rc::Rc};
use tracing::error;
@ -61,8 +61,11 @@ impl PlayState for CharSelectionState {
fn tick(&mut self, global_state: &mut GlobalState, events: Vec<WinEvent>) -> PlayStateResult {
span!(_guard, "tick", "<CharSelectionState as PlayState>::tick");
let client_state = self.client.borrow().get_client_state();
if let ClientState::Pending | ClientState::Registered = client_state {
let (client_in_game, client_registered) = {
let client = self.client.borrow();
(client.in_game(), client.registered())
};
if client_in_game.is_none() && client_registered {
// Handle window events
for event in events {
if self.char_selection_ui.handle_event(event.clone()) {

View File

@ -20,7 +20,6 @@ use common::{
MAX_PICKUP_RANGE_SQR,
},
event::EventBus,
msg::ClientState,
outcome::Outcome,
span,
terrain::{Block, BlockKind},
@ -212,9 +211,11 @@ impl PlayState for SessionState {
));
// TODO: can this be a method on the session or are there borrowcheck issues?
let client_state = self.client.borrow().get_client_state();
if let ClientState::Pending | ClientState::Character = client_state {
let (client_in_game, client_registered) = {
let client = self.client.borrow();
(client.in_game(), client.registered())
};
if client_in_game.is_some() {
// Update MyEntity
// Note: Alternatively, the client could emit an event when the entity changes
// which may or may not be more elegant
@ -1088,7 +1089,7 @@ impl PlayState for SessionState {
self.cleanup();
PlayStateResult::Continue
} else if let ClientState::Registered = client_state {
} else if client_registered && client_in_game.is_none() {
PlayStateResult::Switch(Box::new(CharSelectionState::new(
global_state,
Rc::clone(&self.client),

View File

@ -38,7 +38,7 @@ use crate::{
use common::{
comp::{self, bird_medium, quadruped_low, quadruped_medium, quadruped_small},
generation::{ChunkSupplement, EntityInfo},
msg::server::WorldMapMsg,
msg::WorldMapMsg,
terrain::{Block, BlockKind, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
vol::{ReadVol, RectVolSize, WriteVol},
};

View File

@ -33,7 +33,7 @@ use crate::{
};
use common::{
assets,
msg::server::WorldMapMsg,
msg::WorldMapMsg,
store::Id,
terrain::{
map::MapConfig, uniform_idx_as_vec2, vec2_as_uniform_idx, BiomeKind, MapSizeLg,