veloren/client/src/lib.rs
2024-01-21 21:30:06 +00:00

3091 lines
116 KiB
Rust

#![deny(unsafe_code)]
#![deny(clippy::clone_on_ref_ptr)]
#![feature(option_zip)]
pub mod addr;
pub mod error;
// Reexports
pub use crate::error::Error;
pub use authc::AuthClientError;
pub use common_net::msg::ServerInfo;
pub use specs::{
Builder, DispatcherBuilder, Entity as EcsEntity, Join, LendJoin, ReadStorage, World, WorldExt,
};
use crate::addr::ConnectionArgs;
use byteorder::{ByteOrder, LittleEndian};
use common::{
character::{CharacterId, CharacterItem},
comp::{
self,
chat::KillSource,
controller::{BlockInteraction, CraftEvent},
dialogue::Subject,
group,
inventory::item::{modular, tool, ItemKind},
invite::{InviteKind, InviteResponse},
skills::Skill,
slot::{EquipSlot, InvSlotId, Slot},
CharacterState, ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs,
GroupManip, InputKind, InventoryAction, InventoryEvent, InventoryUpdateEvent,
MapMarkerChange, PresenceKind, UtteranceKind,
},
event::{EventBus, LocalEvent, UpdateCharacterMetadata},
grid::Grid,
link::Is,
lod,
mounting::{Rider, VolumePos, VolumeRider},
outcome::Outcome,
recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook},
resources::{GameMode, PlayerEntity, Time, TimeOfDay},
shared_server_config::ServerConstants,
spiral::Spiral2d,
terrain::{
block::Block, map::MapConfig, neighbors, site::DungeonKindMeta, BiomeKind,
CoordinateConversions, SiteKindMeta, SpriteKind, TerrainChunk, TerrainChunkSize,
TerrainGrid,
},
trade::{PendingTrade, SitePrices, TradeAction, TradeId, TradeResult},
uid::{IdMaps, Uid},
vol::RectVolSize,
weather::{Weather, WeatherGrid},
};
#[cfg(feature = "tracy")] use common_base::plot;
use common_base::{prof_span, span};
use common_net::{
msg::{
self,
server::ServerDescription,
world_msg::{EconomyInfo, PoiInfo, SiteId, SiteInfo},
ChatTypeContext, ClientGeneral, ClientMsg, ClientRegister, ClientType, DisconnectReason,
InviteAnswer, Notification, PingMsg, PlayerInfo, PlayerListUpdate, RegisterError,
ServerGeneral, ServerInit, ServerRegisterAnswer,
},
sync::WorldSyncExt,
};
use common_state::State;
use common_systems::add_local_systems;
use comp::BuffKind;
use hashbrown::{HashMap, HashSet};
use image::DynamicImage;
use network::{ConnectAddr, Network, Participant, Pid, Stream};
use num::traits::FloatConst;
use rayon::prelude::*;
use specs::Component;
use std::{
collections::{BTreeMap, VecDeque},
mem,
sync::Arc,
time::{Duration, Instant},
};
use tokio::runtime::Runtime;
use tracing::{debug, error, trace, warn};
use vek::*;
pub const MAX_SELECTABLE_VIEW_DISTANCE: u32 = 65;
const PING_ROLLING_AVERAGE_SECS: usize = 10;
#[derive(Debug)]
pub enum Event {
Chat(comp::ChatMsg),
GroupInventoryUpdate(comp::Item, String, Uid),
InviteComplete {
target: Uid,
answer: InviteAnswer,
kind: InviteKind,
},
TradeComplete {
result: TradeResult,
trade: PendingTrade,
},
Disconnect,
DisconnectionNotification(u64),
InventoryUpdated(Vec<InventoryUpdateEvent>),
Kicked(String),
Notification(Notification),
SetViewDistance(u32),
Outcome(Outcome),
CharacterCreated(CharacterId),
CharacterEdited(CharacterId),
CharacterJoined(UpdateCharacterMetadata),
CharacterError(String),
MapMarker(comp::MapMarkerUpdate),
StartSpectate(Vec3<f32>),
SpectatePosition(Vec3<f32>),
}
#[derive(Debug)]
pub enum ClientInitStage {
/// A connection to the server is being created
ConnectionEstablish,
/// Waiting for server version
WatingForServerVersion,
/// We're currently authenticating with the server
Authentication,
/// Loading map data, site information, recipe information and other
/// initialization data
LoadingInitData,
/// Prepare data received by the server to be used by the client (insert
/// data into the ECS, render map)
StartingClient,
}
pub struct WorldData {
/// Just the "base" layer for LOD; currently includes colors and nothing
/// else. In the future we'll add more layers, like shadows, rivers, and
/// probably foliage, cities, roads, and other structures.
pub lod_base: Grid<u32>,
/// The "height" layer for LOD; currently includes only land altitudes, but
/// in the future should also water depth, and probably other
/// information as well.
pub lod_alt: Grid<u32>,
/// The "shadow" layer for LOD. Includes east and west horizon angles and
/// an approximate max occluder height, which we use to try to
/// approximate soft and volumetric shadows.
pub lod_horizon: Grid<u32>,
/// A fully rendered map image for use with the map and minimap; note that
/// this can be constructed dynamically by combining the layers of world
/// map data (e.g. with shadow map data or river data), but at present
/// we opt not to do this.
///
/// The first two elements of the tuple are the regular and topographic maps
/// respectively. The third element of the tuple is the world size (as a 2D
/// grid, in chunks), and the fourth element holds the minimum height for
/// any land chunk (i.e. the sea level) in its x coordinate, and the maximum
/// land height above this height (i.e. the max height) in its y coordinate.
map: (Vec<Arc<DynamicImage>>, Vec2<u16>, Vec2<f32>),
}
impl WorldData {
pub fn chunk_size(&self) -> Vec2<u16> { self.map.1 }
pub fn map_layers(&self) -> &Vec<Arc<DynamicImage>> { &self.map.0 }
pub fn map_image(&self) -> &Arc<DynamicImage> { &self.map.0[0] }
pub fn topo_map_image(&self) -> &Arc<DynamicImage> { &self.map.0[1] }
pub fn min_chunk_alt(&self) -> f32 { self.map.2.x }
pub fn max_chunk_alt(&self) -> f32 { self.map.2.y }
}
pub struct SiteInfoRich {
pub site: SiteInfo,
pub economy: Option<EconomyInfo>,
}
struct WeatherLerp {
old: (WeatherGrid, Instant),
new: (WeatherGrid, Instant),
}
impl WeatherLerp {
fn weather_update(&mut self, weather: WeatherGrid) {
self.old = mem::replace(&mut self.new, (weather, Instant::now()));
}
// TODO: Make improvements to this interpolation, it's main issue is assuming
// that updates come at regular intervals.
fn update(&mut self, to_update: &mut WeatherGrid) {
prof_span!("WeatherLerp::update");
let old = &self.old.0;
let new = &self.new.0;
if new.size() == Vec2::zero() {
return;
}
if to_update.size() != new.size() {
*to_update = new.clone();
}
if old.size() == new.size() {
// Assumes updates are regular
let t = (self.new.1.elapsed().as_secs_f32()
/ self.new.1.duration_since(self.old.1).as_secs_f32())
.clamp(0.0, 1.0);
to_update
.iter_mut()
.zip(old.iter().zip(new.iter()))
.for_each(|((_, current), ((_, old), (_, new)))| {
*current = Weather::lerp_unclamped(old, new, t);
});
}
}
}
impl Default for WeatherLerp {
fn default() -> Self {
Self {
old: (WeatherGrid::new(Vec2::zero()), Instant::now()),
new: (WeatherGrid::new(Vec2::zero()), Instant::now()),
}
}
}
pub struct Client {
registered: bool,
presence: Option<PresenceKind>,
runtime: Arc<Runtime>,
server_info: ServerInfo,
/// Localized server motd and rules
server_description: ServerDescription,
world_data: WorldData,
weather: WeatherLerp,
player_list: HashMap<Uid, PlayerInfo>,
character_list: CharacterList,
sites: HashMap<SiteId, SiteInfoRich>,
possible_starting_sites: Vec<SiteId>,
pois: Vec<PoiInfo>,
pub chat_mode: ChatMode,
recipe_book: RecipeBook,
component_recipe_book: ComponentRecipeBook,
repair_recipe_book: RepairRecipeBook,
available_recipes: HashMap<String, Option<SpriteKind>>,
lod_zones: HashMap<Vec2<i32>, lod::Zone>,
lod_last_requested: Option<Instant>,
lod_pos_fallback: Option<Vec2<f32>>,
force_update_counter: u64,
max_group_size: u32,
// Client has received an invite (inviter uid, time out instant)
invite: Option<(Uid, Instant, Duration, InviteKind)>,
group_leader: Option<Uid>,
// Note: potentially representable as a client only component
group_members: HashMap<Uid, group::Role>,
// Pending invites that this client has sent out
pending_invites: HashSet<Uid>,
// The pending trade the client is involved in, and it's id
pending_trade: Option<(TradeId, PendingTrade, Option<SitePrices>)>,
network: Option<Network>,
participant: Option<Participant>,
general_stream: Stream,
ping_stream: Stream,
register_stream: Stream,
character_screen_stream: Stream,
in_game_stream: Stream,
terrain_stream: Stream,
client_timeout: Duration,
last_server_ping: f64,
last_server_pong: f64,
last_ping_delta: f64,
ping_deltas: VecDeque<f64>,
tick: u64,
state: State,
flashing_lights_enabled: bool,
/// Terrrain view distance
server_view_distance_limit: Option<u32>,
view_distance: Option<u32>,
lod_distance: f32,
// TODO: move into voxygen
loaded_distance: f32,
pending_chunks: HashMap<Vec2<i32>, Instant>,
target_time_of_day: Option<TimeOfDay>,
dt_adjustment: f64,
connected_server_constants: ServerConstants,
}
/// Holds data related to the current players characters, as well as some
/// additional state to handle UI.
#[derive(Debug, Default)]
pub struct CharacterList {
pub characters: Vec<CharacterItem>,
pub loading: bool,
}
impl Client {
pub async fn new(
addr: ConnectionArgs,
runtime: Arc<Runtime>,
// TODO: refactor to avoid needing to use this out parameter
mismatched_server_info: &mut Option<ServerInfo>,
username: &str,
password: &str,
locale: Option<String>,
auth_trusted: impl FnMut(&str) -> bool,
init_stage_update: &(dyn Fn(ClientInitStage) + Send + Sync),
add_foreign_systems: impl Fn(&mut DispatcherBuilder) + Send + 'static,
) -> Result<Self, Error> {
let network = Network::new(Pid::new(), &runtime);
init_stage_update(ClientInitStage::ConnectionEstablish);
let mut participant = match addr {
ConnectionArgs::Tcp {
hostname,
prefer_ipv6,
} => addr::try_connect(&network, &hostname, prefer_ipv6, ConnectAddr::Tcp).await?,
ConnectionArgs::Quic {
hostname,
prefer_ipv6,
} => {
warn!(
"QUIC is enabled. This is experimental and you won't be able to connect to \
TCP servers unless deactivated"
);
let config = quinn::ClientConfig::with_native_roots();
addr::try_connect(&network, &hostname, prefer_ipv6, |a| {
ConnectAddr::Quic(a, config.clone(), hostname.clone())
})
.await?
},
ConnectionArgs::Mpsc(id) => network.connect(ConnectAddr::Mpsc(id)).await?,
};
let stream = participant.opened().await?;
let ping_stream = participant.opened().await?;
let mut register_stream = participant.opened().await?;
let character_screen_stream = participant.opened().await?;
let in_game_stream = participant.opened().await?;
let terrain_stream = participant.opened().await?;
init_stage_update(ClientInitStage::WatingForServerVersion);
register_stream.send(ClientType::Game)?;
let server_info: ServerInfo = register_stream.recv().await?;
if server_info.git_hash != *common::util::GIT_HASH {
warn!(
"Server is running {}[{}], you are running {}[{}], versions might be incompatible!",
server_info.git_hash,
server_info.git_date,
common::util::GIT_HASH.to_string(),
*common::util::GIT_DATE,
);
}
// Pass the server info back to the caller to ensure they can access it even
// if this function errors.
mem::swap(mismatched_server_info, &mut Some(server_info.clone()));
debug!("Auth Server: {:?}", server_info.auth_provider);
ping_stream.send(PingMsg::Ping)?;
init_stage_update(ClientInitStage::Authentication);
// Register client
Self::register(
username,
password,
locale,
auth_trusted,
&server_info,
&mut register_stream,
)
.await?;
init_stage_update(ClientInitStage::LoadingInitData);
// Wait for initial sync
let mut ping_interval = tokio::time::interval(Duration::from_secs(1));
let ServerInit::GameSync {
entity_package,
time_of_day,
max_group_size,
client_timeout,
world_map,
recipe_book,
component_recipe_book,
material_stats,
ability_map,
server_constants,
repair_recipe_book,
description,
} = loop {
tokio::select! {
// Spawn in a blocking thread (leaving the network thread free). This is mostly
// useful for bots.
res = register_stream.recv() => break res?,
_ = ping_interval.tick() => ping_stream.send(PingMsg::Ping)?,
}
};
init_stage_update(ClientInitStage::StartingClient);
// Spawn in a blocking thread (leaving the network thread free). This is mostly
// useful for bots.
let mut task = tokio::task::spawn_blocking(move || {
let map_size_lg =
common::terrain::MapSizeLg::new(world_map.dimensions_lg).map_err(|_| {
Error::Other(format!(
"Server sent bad world map dimensions: {:?}",
world_map.dimensions_lg,
))
})?;
let sea_level = world_map.default_chunk.get_min_z() as f32;
// Initialize `State`
let pools = State::pools(GameMode::Client);
let mut state = State::client(
pools,
map_size_lg,
world_map.default_chunk,
// TODO: Add frontend systems
|dispatch_builder| {
add_local_systems(dispatch_builder);
add_foreign_systems(dispatch_builder);
},
);
// Client-only components
state.ecs_mut().register::<comp::Last<CharacterState>>();
let entity = state.ecs_mut().apply_entity_package(entity_package);
*state.ecs_mut().write_resource() = time_of_day;
*state.ecs_mut().write_resource() = PlayerEntity(Some(entity));
state.ecs_mut().insert(material_stats);
state.ecs_mut().insert(ability_map);
let map_size = map_size_lg.chunks();
let max_height = world_map.max_height;
let rgba = world_map.rgba;
let alt = world_map.alt;
if rgba.size() != map_size.map(|e| e as i32) {
return Err(Error::Other("Server sent a bad world map image".into()));
}
if alt.size() != map_size.map(|e| e as i32) {
return Err(Error::Other("Server sent a bad altitude map.".into()));
}
let [west, east] = world_map.horizons;
let scale_angle = |a: u8| (a as f32 / 255.0 * <f32 as FloatConst>::FRAC_PI_2()).tan();
let scale_height = |h: u8| h as f32 / 255.0 * max_height;
let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height;
debug!("Preparing image...");
let unzip_horizons = |(angles, heights): &(Vec<_>, Vec<_>)| {
(
angles.iter().copied().map(scale_angle).collect::<Vec<_>>(),
heights
.iter()
.copied()
.map(scale_height)
.collect::<Vec<_>>(),
)
};
let horizons = [unzip_horizons(&west), unzip_horizons(&east)];
// Redraw map (with shadows this time).
let mut world_map_rgba = vec![0u32; rgba.size().product() as usize];
let mut world_map_topo = vec![0u32; rgba.size().product() as usize];
let mut map_config = common::terrain::map::MapConfig::orthographic(
map_size_lg,
core::ops::RangeInclusive::new(0.0, max_height),
);
map_config.horizons = Some(&horizons);
let rescale_height = |h: f32| h / max_height;
let bounds_check = |pos: Vec2<i32>| {
pos.reduce_partial_min() >= 0
&& pos.x < map_size.x as i32
&& pos.y < map_size.y as i32
};
fn sample_pos(
map_config: &MapConfig,
pos: Vec2<i32>,
alt: &Grid<u32>,
rgba: &Grid<u32>,
map_size: &Vec2<u16>,
map_size_lg: &common::terrain::MapSizeLg,
max_height: f32,
) -> common::terrain::map::MapSample {
let rescale_height = |h: f32| h / max_height;
let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height;
let bounds_check = |pos: Vec2<i32>| {
pos.reduce_partial_min() >= 0
&& pos.x < map_size.x as i32
&& pos.y < map_size.y as i32
};
let MapConfig {
gain,
is_contours,
is_height_map,
is_stylized_topo,
..
} = *map_config;
let mut is_contour_line = false;
let mut is_border = false;
let (rgb, alt, downhill_wpos) = if bounds_check(pos) {
let posi = pos.y as usize * map_size.x as usize + pos.x as usize;
let [r, g, b, _a] = rgba[pos].to_le_bytes();
let is_water = r == 0 && b > 102 && g < 77;
let alti = alt[pos];
// Compute contours (chunks are assigned in the river code below)
let altj = rescale_height(scale_height_big(alti));
let contour_interval = 150.0;
let chunk_contour = (altj * gain / contour_interval) as u32;
// Compute downhill.
let downhill = {
let mut best = -1;
let mut besth = alti;
for nposi in neighbors(*map_size_lg, posi) {
let nbh = alt.raw()[nposi];
let nalt = rescale_height(scale_height_big(nbh));
let nchunk_contour = (nalt * gain / contour_interval) as u32;
if !is_contour_line && chunk_contour > nchunk_contour {
is_contour_line = true;
}
let [nr, ng, nb, _na] = rgba.raw()[nposi].to_le_bytes();
let n_is_water = nr == 0 && nb > 102 && ng < 77;
if !is_border && is_water && !n_is_water {
is_border = true;
}
if nbh < besth {
besth = nbh;
best = nposi as isize;
}
}
best
};
let downhill_wpos = if downhill < 0 {
None
} else {
Some(
Vec2::new(
(downhill as usize % map_size.x as usize) as i32,
(downhill as usize / map_size.x as usize) as i32,
) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
)
};
(Rgb::new(r, g, b), alti, downhill_wpos)
} else {
(Rgb::zero(), 0, None)
};
let alt = f64::from(rescale_height(scale_height_big(alt)));
let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
let downhill_wpos =
downhill_wpos.unwrap_or(wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32));
let is_path = rgb.r == 0x37 && rgb.g == 0x29 && rgb.b == 0x23;
let rgb = rgb.map(|e: u8| e as f64 / 255.0);
let is_water = rgb.r == 0.0 && rgb.b > 0.4 && rgb.g < 0.3;
let rgb = if is_height_map {
if is_path {
// Path color is Rgb::new(0x37, 0x29, 0x23)
Rgb::new(0.9, 0.9, 0.63)
} else if is_water {
Rgb::new(0.23, 0.47, 0.53)
} else if is_contours && is_contour_line {
// Color contour lines
Rgb::new(0.15, 0.15, 0.15)
} else {
// Color hill shading
let lightness = (alt + 0.2).min(1.0);
Rgb::new(lightness, 0.9 * lightness, 0.5 * lightness)
}
} else if is_stylized_topo {
if is_path {
Rgb::new(0.9, 0.9, 0.63)
} else if is_water {
if is_border {
Rgb::new(0.10, 0.34, 0.50)
} else {
Rgb::new(0.23, 0.47, 0.63)
}
} else if is_contour_line {
Rgb::new(0.25, 0.25, 0.25)
} else {
// Stylized colors
Rgb::new(
(rgb.r + 0.25).min(1.0),
(rgb.g + 0.23).min(1.0),
(rgb.b + 0.10).min(1.0),
)
}
} else {
Rgb::new(rgb.r, rgb.g, rgb.b)
}
.map(|e| (e * 255.0) as u8);
common::terrain::map::MapSample {
rgb,
alt,
downhill_wpos,
connections: None,
}
}
// Generate standard shaded map
map_config.is_shaded = true;
map_config.generate(
|pos| {
sample_pos(
&map_config,
pos,
&alt,
&rgba,
&map_size,
&map_size_lg,
max_height,
)
},
|wpos| {
let pos = wpos.wpos_to_cpos();
rescale_height(if bounds_check(pos) {
scale_height_big(alt[pos])
} else {
0.0
})
},
|pos, (r, g, b, a)| {
world_map_rgba[pos.y * map_size.x as usize + pos.x] =
u32::from_le_bytes([r, g, b, a]);
},
);
// Generate map with topographical lines and stylized colors
map_config.is_contours = true;
map_config.is_stylized_topo = true;
map_config.generate(
|pos| {
sample_pos(
&map_config,
pos,
&alt,
&rgba,
&map_size,
&map_size_lg,
max_height,
)
},
|wpos| {
let pos = wpos.wpos_to_cpos();
rescale_height(if bounds_check(pos) {
scale_height_big(alt[pos])
} else {
0.0
})
},
|pos, (r, g, b, a)| {
world_map_topo[pos.y * map_size.x as usize + pos.x] =
u32::from_le_bytes([r, g, b, a]);
},
);
let make_raw = |rgb| -> Result<_, Error> {
let mut raw = vec![0u8; 4 * world_map_rgba.len()];
LittleEndian::write_u32_into(rgb, &mut raw);
Ok(Arc::new(
DynamicImage::ImageRgba8({
// Should not fail if the dimensions are correct.
let map =
image::ImageBuffer::from_raw(u32::from(map_size.x), u32::from(map_size.y), raw);
map.ok_or_else(|| Error::Other("Server sent a bad world map image".into()))?
})
// Flip the image, since Voxygen uses an orientation where rotation from
// positive x axis to positive y axis is counterclockwise around the z axis.
.flipv(),
))
};
let lod_base = rgba;
let lod_alt = alt;
let world_map_rgb_img = make_raw(&world_map_rgba)?;
let world_map_topo_img = make_raw(&world_map_topo)?;
let world_map_layers = vec![world_map_rgb_img, world_map_topo_img];
let horizons = (west.0, west.1, east.0, east.1)
.into_par_iter()
.map(|(wa, wh, ea, eh)| u32::from_le_bytes([wa, wh, ea, eh]))
.collect::<Vec<_>>();
let lod_horizon = horizons;
let map_bounds = Vec2::new(sea_level, max_height);
debug!("Done preparing image...");
Ok((
state,
lod_base,
lod_alt,
Grid::from_raw(map_size.map(|e| e as i32), lod_horizon),
(world_map_layers, map_size, map_bounds),
world_map.sites,
world_map.possible_starting_sites,
world_map.pois,
recipe_book,
component_recipe_book,
repair_recipe_book,
max_group_size,
client_timeout,
))
});
let (
state,
lod_base,
lod_alt,
lod_horizon,
world_map,
sites,
possible_starting_sites,
pois,
recipe_book,
component_recipe_book,
repair_recipe_book,
max_group_size,
client_timeout,
) = loop {
tokio::select! {
res = &mut task => break res.expect("Client thread should not panic")?,
_ = ping_interval.tick() => ping_stream.send(PingMsg::Ping)?,
}
};
ping_stream.send(PingMsg::Ping)?;
debug!("Initial sync done");
Ok(Self {
registered: true,
presence: None,
runtime,
server_info,
server_description: description,
world_data: WorldData {
lod_base,
lod_alt,
lod_horizon,
map: world_map,
},
weather: WeatherLerp::default(),
player_list: HashMap::new(),
character_list: CharacterList::default(),
sites: sites
.iter()
.map(|s| {
(s.id, SiteInfoRich {
site: s.clone(),
economy: None,
})
})
.collect(),
possible_starting_sites,
pois,
recipe_book,
component_recipe_book,
repair_recipe_book,
available_recipes: HashMap::default(),
chat_mode: ChatMode::default(),
lod_zones: HashMap::new(),
lod_last_requested: None,
lod_pos_fallback: None,
force_update_counter: 0,
max_group_size,
invite: None,
group_leader: None,
group_members: HashMap::new(),
pending_invites: HashSet::new(),
pending_trade: None,
network: Some(network),
participant: Some(participant),
general_stream: stream,
ping_stream,
register_stream,
character_screen_stream,
in_game_stream,
terrain_stream,
client_timeout,
last_server_ping: 0.0,
last_server_pong: 0.0,
last_ping_delta: 0.0,
ping_deltas: VecDeque::new(),
tick: 0,
state,
flashing_lights_enabled: true,
server_view_distance_limit: None,
view_distance: None,
lod_distance: 4.0,
loaded_distance: 0.0,
pending_chunks: HashMap::new(),
target_time_of_day: None,
dt_adjustment: 1.0,
connected_server_constants: server_constants,
})
}
/// Request a state transition to `ClientState::Registered`.
async fn register(
username: &str,
password: &str,
locale: Option<String>,
mut auth_trusted: impl FnMut(&str) -> bool,
server_info: &ServerInfo,
register_stream: &mut Stream,
) -> Result<(), Error> {
// Authentication
let token_or_username = match &server_info.auth_provider {
Some(addr) => {
// Query whether this is a trusted auth server
if auth_trusted(addr) {
let (scheme, authority) = match addr.split_once("://") {
Some((s, a)) => (s, a),
None => return Err(Error::AuthServerUrlInvalid(addr.to_string())),
};
let scheme = match scheme.parse::<authc::Scheme>() {
Ok(s) => s,
Err(_) => return Err(Error::AuthServerUrlInvalid(addr.to_string())),
};
let authority = match authority.parse::<authc::Authority>() {
Ok(a) => a,
Err(_) => return Err(Error::AuthServerUrlInvalid(addr.to_string())),
};
Ok(authc::AuthClient::new(scheme, authority)?
.sign_in(username, password)
.await?
.serialize())
} else {
Err(Error::AuthServerNotTrusted)
}
},
None => Ok(username.to_owned()),
}?;
debug!("Registering client...");
register_stream.send(ClientRegister {
token_or_username,
locale,
})?;
match register_stream.recv::<ServerRegisterAnswer>().await? {
Err(RegisterError::AuthError(err)) => Err(Error::AuthErr(err)),
Err(RegisterError::InvalidCharacter) => Err(Error::InvalidCharacter),
Err(RegisterError::NotOnWhitelist) => Err(Error::NotOnWhitelist),
Err(RegisterError::Kicked(err)) => Err(Error::Kicked(err)),
Err(RegisterError::Banned(reason)) => Err(Error::Banned(reason)),
Err(RegisterError::TooManyPlayers) => Err(Error::TooManyPlayers),
Ok(()) => {
debug!("Client registered successfully.");
Ok(())
},
}
}
fn send_msg_err<S>(&mut self, msg: S) -> Result<(), network::StreamError>
where
S: Into<ClientMsg>,
{
prof_span!("send_msg_err");
let msg: ClientMsg = msg.into();
#[cfg(debug_assertions)]
{
const C_TYPE: ClientType = ClientType::Game;
let verified = msg.verify(C_TYPE, self.registered, self.presence);
// Due to the fact that character loading is performed asynchronously after
// initial connect it is possible to receive messages after a character load
// error while in the wrong state.
if !verified {
warn!(
"Received ClientType::Game message when not in game (Registered: {} Presence: \
{:?}), dropping message: {:?} ",
self.registered, self.presence, msg
);
return Ok(());
}
}
match msg {
ClientMsg::Type(msg) => self.register_stream.send(msg),
ClientMsg::Register(msg) => self.register_stream.send(msg),
ClientMsg::General(msg) => {
#[cfg(feature = "tracy")]
let (mut ingame, mut terrain) = (0.0, 0.0);
let stream = match msg {
ClientGeneral::RequestCharacterList
| ClientGeneral::CreateCharacter { .. }
| ClientGeneral::EditCharacter { .. }
| ClientGeneral::DeleteCharacter(_)
| ClientGeneral::Character(_, _)
| ClientGeneral::Spectate(_) => &mut self.character_screen_stream,
// Only in game
ClientGeneral::ControllerInputs(_)
| ClientGeneral::ControlEvent(_)
| ClientGeneral::ControlAction(_)
| ClientGeneral::SetViewDistance(_)
| ClientGeneral::BreakBlock(_)
| ClientGeneral::PlaceBlock(_, _)
| ClientGeneral::ExitInGame
| ClientGeneral::PlayerPhysics { .. }
| ClientGeneral::UnlockSkill(_)
| ClientGeneral::RequestSiteInfo(_)
| ClientGeneral::RequestPlayerPhysics { .. }
| ClientGeneral::RequestLossyTerrainCompression { .. }
| ClientGeneral::UpdateMapMarker(_)
| ClientGeneral::SpectatePosition(_) => {
#[cfg(feature = "tracy")]
{
ingame = 1.0;
}
&mut self.in_game_stream
},
// Terrain
ClientGeneral::TerrainChunkRequest { .. }
| ClientGeneral::LodZoneRequest { .. } => {
#[cfg(feature = "tracy")]
{
terrain = 1.0;
}
&mut self.terrain_stream
},
// Always possible
ClientGeneral::ChatMsg(_)
| ClientGeneral::Command(_, _)
| ClientGeneral::Terminate => &mut self.general_stream,
};
#[cfg(feature = "tracy")]
{
plot!("ingame_sends", ingame);
plot!("terrain_sends", terrain);
}
stream.send(msg)
},
ClientMsg::Ping(msg) => self.ping_stream.send(msg),
}
}
pub fn request_player_physics(&mut self, server_authoritative: bool) {
self.send_msg(ClientGeneral::RequestPlayerPhysics {
server_authoritative,
})
}
pub fn request_lossy_terrain_compression(&mut self, lossy_terrain_compression: bool) {
self.send_msg(ClientGeneral::RequestLossyTerrainCompression {
lossy_terrain_compression,
})
}
fn send_msg<S>(&mut self, msg: S)
where
S: Into<ClientMsg>,
{
let res = self.send_msg_err(msg);
if let Err(e) = res {
warn!(
?e,
"connection to server no longer possible, couldn't send msg"
);
}
}
/// Request a state transition to `ClientState::Character`.
pub fn request_character(
&mut self,
character_id: CharacterId,
view_distances: common::ViewDistances,
) {
let view_distances = self.set_view_distances_local(view_distances);
self.send_msg(ClientGeneral::Character(character_id, view_distances));
// Assume we are in_game unless server tells us otherwise
self.presence = Some(PresenceKind::Character(character_id));
}
/// Request a state transition to `ClientState::Spectate`.
pub fn request_spectate(&mut self, view_distances: common::ViewDistances) {
let view_distances = self.set_view_distances_local(view_distances);
self.send_msg(ClientGeneral::Spectate(view_distances));
self.presence = Some(PresenceKind::Spectator);
}
/// Load the current players character list
pub fn load_character_list(&mut self) {
self.character_list.loading = true;
self.send_msg(ClientGeneral::RequestCharacterList);
}
/// New character creation
pub fn create_character(
&mut self,
alias: String,
mainhand: Option<String>,
offhand: Option<String>,
body: comp::Body,
start_site: Option<SiteId>,
) {
self.character_list.loading = true;
self.send_msg(ClientGeneral::CreateCharacter {
alias,
mainhand,
offhand,
body,
start_site,
});
}
pub fn edit_character(&mut self, alias: String, id: CharacterId, body: comp::Body) {
self.character_list.loading = true;
self.send_msg(ClientGeneral::EditCharacter { alias, id, body });
}
/// Character deletion
pub fn delete_character(&mut self, character_id: CharacterId) {
// Pre-emptively remove the character to be deleted from the character list as
// character deletes are processed asynchronously by the server so we can't rely
// on a timely response to update the character list
if let Some(pos) = self
.character_list
.characters
.iter()
.position(|x| x.character.id == Some(character_id))
{
self.character_list.characters.remove(pos);
}
self.send_msg(ClientGeneral::DeleteCharacter(character_id));
}
/// Send disconnect message to the server
pub fn logout(&mut self) {
debug!("Sending logout from server");
self.send_msg(ClientGeneral::Terminate);
self.registered = false;
self.presence = None;
}
/// Request a state transition to `ClientState::Registered` from an ingame
/// state.
pub fn request_remove_character(&mut self) {
self.chat_mode = ChatMode::World;
self.send_msg(ClientGeneral::ExitInGame);
}
pub fn set_view_distances(&mut self, view_distances: common::ViewDistances) {
let view_distances = self.set_view_distances_local(view_distances);
self.send_msg(ClientGeneral::SetViewDistance(view_distances));
}
/// Clamps provided view distances, locally sets the terrain view distance
/// in the client's properties and returns the clamped values for the
/// caller to send to the server.
fn set_view_distances_local(
&mut self,
view_distances: common::ViewDistances,
) -> common::ViewDistances {
let view_distances = common::ViewDistances {
terrain: view_distances
.terrain
.clamp(1, MAX_SELECTABLE_VIEW_DISTANCE),
entity: view_distances.entity.max(1),
};
self.view_distance = Some(view_distances.terrain);
view_distances
}
pub fn set_lod_distance(&mut self, lod_distance: u32) {
let lod_distance = lod_distance.clamp(0, 1000) as f32 / lod::ZONE_SIZE as f32;
self.lod_distance = lod_distance;
}
pub fn set_flashing_lights_enabled(&mut self, flashing_lights_enabled: bool) {
self.flashing_lights_enabled = flashing_lights_enabled;
}
pub fn use_slot(&mut self, slot: Slot) {
self.control_action(ControlAction::InventoryAction(InventoryAction::Use(slot)))
}
pub fn swap_slots(&mut self, a: Slot, b: Slot) {
match (a, b) {
(Slot::Equip(equip), slot) | (slot, Slot::Equip(equip)) => self.control_action(
ControlAction::InventoryAction(InventoryAction::Swap(equip, slot)),
),
(Slot::Inventory(inv1), Slot::Inventory(inv2)) => {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
InventoryEvent::Swap(inv1, inv2),
)))
},
}
}
pub fn drop_slot(&mut self, slot: Slot) {
match slot {
Slot::Equip(equip) => {
self.control_action(ControlAction::InventoryAction(InventoryAction::Drop(equip)))
},
Slot::Inventory(inv) => self.send_msg(ClientGeneral::ControlEvent(
ControlEvent::InventoryEvent(InventoryEvent::Drop(inv)),
)),
}
}
pub fn sort_inventory(&mut self) {
self.control_action(ControlAction::InventoryAction(InventoryAction::Sort));
}
pub fn perform_trade_action(&mut self, action: TradeAction) {
if let Some((id, _, _)) = self.pending_trade {
if let TradeAction::Decline = action {
self.pending_trade.take();
}
self.send_msg(ClientGeneral::ControlEvent(
ControlEvent::PerformTradeAction(id, action),
));
}
}
pub fn is_dead(&self) -> bool { self.current::<comp::Health>().map_or(false, |h| h.is_dead) }
pub fn is_gliding(&self) -> bool {
self.current::<CharacterState>()
.map_or(false, |cs| matches!(cs, CharacterState::Glide(_)))
}
pub fn split_swap_slots(&mut self, a: Slot, b: Slot) {
match (a, b) {
(Slot::Equip(equip), slot) | (slot, Slot::Equip(equip)) => self.control_action(
ControlAction::InventoryAction(InventoryAction::Swap(equip, slot)),
),
(Slot::Inventory(inv1), Slot::Inventory(inv2)) => {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
InventoryEvent::SplitSwap(inv1, inv2),
)))
},
}
}
pub fn split_drop_slot(&mut self, slot: Slot) {
match slot {
Slot::Equip(equip) => {
self.control_action(ControlAction::InventoryAction(InventoryAction::Drop(equip)))
},
Slot::Inventory(inv) => self.send_msg(ClientGeneral::ControlEvent(
ControlEvent::InventoryEvent(InventoryEvent::SplitDrop(inv)),
)),
}
}
pub fn pick_up(&mut self, entity: EcsEntity) {
// Get the health component from the entity
if let Some(uid) = self.state.read_component_copied(entity) {
// If we're dead, exit before sending the message
if self.is_dead() {
return;
}
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
InventoryEvent::Pickup(uid),
)));
}
}
pub fn npc_interact(&mut self, npc_entity: EcsEntity, subject: Subject) {
// If we're dead, exit before sending message
if self.is_dead() {
return;
}
if let Some(uid) = self.state.read_component_copied(npc_entity) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Interact(
uid, subject,
)));
}
}
pub fn player_list(&self) -> &HashMap<Uid, PlayerInfo> { &self.player_list }
pub fn character_list(&self) -> &CharacterList { &self.character_list }
pub fn server_info(&self) -> &ServerInfo { &self.server_info }
pub fn server_description(&self) -> &ServerDescription { &self.server_description }
pub fn world_data(&self) -> &WorldData { &self.world_data }
pub fn recipe_book(&self) -> &RecipeBook { &self.recipe_book }
pub fn component_recipe_book(&self) -> &ComponentRecipeBook { &self.component_recipe_book }
pub fn repair_recipe_book(&self) -> &RepairRecipeBook { &self.repair_recipe_book }
pub fn available_recipes(&self) -> &HashMap<String, Option<SpriteKind>> {
&self.available_recipes
}
pub fn lod_zones(&self) -> &HashMap<Vec2<i32>, lod::Zone> { &self.lod_zones }
/// Set the fallback position used for loading LoD zones when the client
/// entity does not have a position.
pub fn set_lod_pos_fallback(&mut self, pos: Vec2<f32>) { self.lod_pos_fallback = Some(pos); }
/// Returns whether the specified recipe can be crafted and the sprite, if
/// any, that is required to do so.
pub fn can_craft_recipe(&self, recipe: &str, amount: u32) -> (bool, Option<SpriteKind>) {
self.recipe_book
.get(recipe)
.zip(self.inventories().get(self.entity()))
.map(|(recipe, inv)| {
(
recipe.inventory_contains_ingredients(inv, amount).is_ok(),
recipe.craft_sprite,
)
})
.unwrap_or((false, None))
}
pub fn craft_recipe(
&mut self,
recipe: &str,
slots: Vec<(u32, InvSlotId)>,
craft_sprite: Option<(VolumePos, SpriteKind)>,
amount: u32,
) -> bool {
let (can_craft, required_sprite) = self.can_craft_recipe(recipe, amount);
let has_sprite = required_sprite.map_or(true, |s| Some(s) == craft_sprite.map(|(_, s)| s));
if can_craft && has_sprite {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
InventoryEvent::CraftRecipe {
craft_event: CraftEvent::Simple {
recipe: recipe.to_string(),
slots,
amount,
},
craft_sprite: craft_sprite.map(|(pos, _)| pos),
},
)));
true
} else {
false
}
}
/// Checks if the item in the given slot can be salvaged.
pub fn can_salvage_item(&self, slot: InvSlotId) -> bool {
self.inventories()
.get(self.entity())
.and_then(|inv| inv.get(slot))
.map_or(false, |item| item.is_salvageable())
}
/// Salvage the item in the given inventory slot. `salvage_pos` should be
/// the location of a relevant crafting station within range of the player.
pub fn salvage_item(&mut self, slot: InvSlotId, salvage_pos: VolumePos) -> bool {
let is_salvageable = self.can_salvage_item(slot);
if is_salvageable {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
InventoryEvent::CraftRecipe {
craft_event: CraftEvent::Salvage(slot),
craft_sprite: Some(salvage_pos),
},
)));
}
is_salvageable
}
/// Crafts modular weapon from components in the provided slots.
/// `sprite_pos` should be the location of the necessary crafting station in
/// range of the player.
/// Returns whether or not the networking event was sent (which is based on
/// whether the player has two modular components in the provided slots)
pub fn craft_modular_weapon(
&mut self,
primary_component: InvSlotId,
secondary_component: InvSlotId,
sprite_pos: Option<VolumePos>,
) -> bool {
let inventories = self.inventories();
let inventory = inventories.get(self.entity());
enum ModKind {
Primary,
Secondary,
}
// Closure to get inner modular component info from item in a given slot
let mod_kind = |slot| match inventory
.and_then(|inv| inv.get(slot).map(|item| item.kind()))
.as_deref()
{
Some(ItemKind::ModularComponent(modular::ModularComponent::ToolPrimaryComponent {
..
})) => Some(ModKind::Primary),
Some(ItemKind::ModularComponent(
modular::ModularComponent::ToolSecondaryComponent { .. },
)) => Some(ModKind::Secondary),
_ => None,
};
if let (Some(ModKind::Primary), Some(ModKind::Secondary)) =
(mod_kind(primary_component), mod_kind(secondary_component))
{
drop(inventories);
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
InventoryEvent::CraftRecipe {
craft_event: CraftEvent::ModularWeapon {
primary_component,
secondary_component,
},
craft_sprite: sprite_pos,
},
)));
true
} else {
false
}
}
pub fn craft_modular_weapon_component(
&mut self,
toolkind: tool::ToolKind,
material: InvSlotId,
modifier: Option<InvSlotId>,
slots: Vec<(u32, InvSlotId)>,
sprite_pos: Option<VolumePos>,
) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
InventoryEvent::CraftRecipe {
craft_event: CraftEvent::ModularWeaponPrimaryComponent {
toolkind,
material,
modifier,
slots,
},
craft_sprite: sprite_pos,
},
)));
}
/// Repairs the item in the given inventory slot. `sprite_pos` should be
/// the location of a relevant crafting station within range of the player.
pub fn repair_item(
&mut self,
item: Slot,
slots: Vec<(u32, InvSlotId)>,
sprite_pos: VolumePos,
) -> bool {
let is_repairable = {
let inventories = self.inventories();
let inventory = inventories.get(self.entity());
inventory.map_or(false, |inv| {
if let Some(item) = match item {
Slot::Equip(equip_slot) => inv.equipped(equip_slot),
Slot::Inventory(invslot) => inv.get(invslot),
} {
item.has_durability()
} else {
false
}
})
};
if is_repairable {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
InventoryEvent::CraftRecipe {
craft_event: CraftEvent::Repair { item, slots },
craft_sprite: Some(sprite_pos),
},
)));
}
is_repairable
}
fn update_available_recipes(&mut self) {
self.available_recipes = self
.recipe_book
.iter()
.map(|(name, _)| name.clone())
.filter_map(|name| {
let (can_craft, required_sprite) = self.can_craft_recipe(&name, 1);
if can_craft {
Some((name, required_sprite))
} else {
None
}
})
.collect();
}
/// Unstable, likely to be removed in a future release
pub fn sites(&self) -> &HashMap<SiteId, SiteInfoRich> { &self.sites }
pub fn possible_starting_sites(&self) -> &[SiteId] { &self.possible_starting_sites }
/// Unstable, likely to be removed in a future release
pub fn pois(&self) -> &Vec<PoiInfo> { &self.pois }
pub fn sites_mut(&mut self) -> &mut HashMap<SiteId, SiteInfoRich> { &mut self.sites }
pub fn enable_lantern(&mut self) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::EnableLantern));
}
pub fn disable_lantern(&mut self) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::DisableLantern));
}
pub fn toggle_sprite_light(&mut self, pos: VolumePos, enable: bool) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::BlockInteraction(
pos,
BlockInteraction::ToggleLight(enable),
)));
}
pub fn remove_buff(&mut self, buff_id: BuffKind) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::RemoveBuff(
buff_id,
)));
}
pub fn leave_stance(&mut self) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::LeaveStance));
}
pub fn unlock_skill(&mut self, skill: Skill) {
self.send_msg(ClientGeneral::UnlockSkill(skill));
}
pub fn max_group_size(&self) -> u32 { self.max_group_size }
pub fn invite(&self) -> Option<(Uid, Instant, Duration, InviteKind)> { self.invite }
pub fn group_info(&self) -> Option<(String, Uid)> {
self.group_leader.map(|l| ("Group".into(), l)) // TODO
}
pub fn group_members(&self) -> &HashMap<Uid, group::Role> { &self.group_members }
pub fn pending_invites(&self) -> &HashSet<Uid> { &self.pending_invites }
pub fn pending_trade(&self) -> &Option<(TradeId, PendingTrade, Option<SitePrices>)> {
&self.pending_trade
}
pub fn is_trading(&self) -> bool { self.pending_trade.is_some() }
pub fn send_invite(&mut self, invitee: Uid, kind: InviteKind) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InitiateInvite(
invitee, kind,
)))
}
pub fn accept_invite(&mut self) {
// Clear invite
self.invite.take();
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InviteResponse(
InviteResponse::Accept,
)));
}
pub fn decline_invite(&mut self) {
// Clear invite
self.invite.take();
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InviteResponse(
InviteResponse::Decline,
)));
}
pub fn leave_group(&mut self) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
GroupManip::Leave,
)));
}
pub fn kick_from_group(&mut self, uid: Uid) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
GroupManip::Kick(uid),
)));
}
pub fn assign_group_leader(&mut self, uid: Uid) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
GroupManip::AssignLeader(uid),
)));
}
pub fn is_riding(&self) -> bool {
self.state
.ecs()
.read_storage::<Is<Rider>>()
.get(self.entity())
.is_some()
|| self
.state
.ecs()
.read_storage::<Is<VolumeRider>>()
.get(self.entity())
.is_some()
}
pub fn is_lantern_enabled(&self) -> bool {
self.state
.ecs()
.read_storage::<comp::LightEmitter>()
.get(self.entity())
.is_some()
}
pub fn mount(&mut self, entity: EcsEntity) {
if let Some(uid) = self.state.read_component_copied(entity) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Mount(uid)));
}
}
/// Mount a block at a `VolumePos`.
pub fn mount_volume(&mut self, volume_pos: VolumePos) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::MountVolume(
volume_pos,
)));
}
pub fn unmount(&mut self) { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Unmount)); }
pub fn set_pet_stay(&mut self, entity: EcsEntity, stay: bool) {
if let Some(uid) = self.state.read_component_copied(entity) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::SetPetStay(
uid, stay,
)));
}
}
pub fn respawn(&mut self) {
if self
.state
.ecs()
.read_storage::<comp::Health>()
.get(self.entity())
.map_or(false, |h| h.is_dead)
{
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Respawn));
}
}
pub fn map_marker_event(&mut self, event: MapMarkerChange) {
self.send_msg(ClientGeneral::UpdateMapMarker(event));
}
/// Set the current position to spectate, returns true if the client's
/// player has a Pos component to write to.
pub fn spectate_position(&mut self, pos: Vec3<f32>) -> bool {
let write = if let Some(position) = self
.state
.ecs()
.write_storage::<comp::Pos>()
.get_mut(self.entity())
{
position.0 = pos;
true
} else {
false
};
if write {
self.send_msg(ClientGeneral::SpectatePosition(pos));
}
write
}
/// Checks whether a player can swap their weapon+ability `Loadout` settings
/// and sends the `ControlAction` event that signals to do the swap.
pub fn swap_loadout(&mut self) { self.control_action(ControlAction::SwapEquippedWeapons) }
/// Determine whether the player is wielding, if they're even capable of
/// being in a wield state.
pub fn is_wielding(&self) -> Option<bool> {
self.state
.ecs()
.read_storage::<CharacterState>()
.get(self.entity())
.map(|cs| cs.is_wield())
}
pub fn toggle_wield(&mut self) {
match self.is_wielding() {
Some(true) => self.control_action(ControlAction::Unwield),
Some(false) => self.control_action(ControlAction::Wield),
None => warn!("Can't toggle wield, client entity doesn't have a `CharacterState`"),
}
}
pub fn toggle_sit(&mut self) {
let is_sitting = self
.state
.ecs()
.read_storage::<CharacterState>()
.get(self.entity())
.map(|cs| matches!(cs, CharacterState::Sit));
match is_sitting {
Some(true) => self.control_action(ControlAction::Stand),
Some(false) => self.control_action(ControlAction::Sit),
None => warn!("Can't toggle sit, client entity doesn't have a `CharacterState`"),
}
}
pub fn toggle_dance(&mut self) {
let is_dancing = self
.state
.ecs()
.read_storage::<CharacterState>()
.get(self.entity())
.map(|cs| matches!(cs, CharacterState::Dance));
match is_dancing {
Some(true) => self.control_action(ControlAction::Stand),
Some(false) => self.control_action(ControlAction::Dance),
None => warn!("Can't toggle dance, client entity doesn't have a `CharacterState`"),
}
}
pub fn utter(&mut self, kind: UtteranceKind) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Utterance(kind)));
}
pub fn toggle_sneak(&mut self) {
let is_sneaking = self
.state
.ecs()
.read_storage::<CharacterState>()
.get(self.entity())
.map(CharacterState::is_stealthy);
match is_sneaking {
Some(true) => self.control_action(ControlAction::Stand),
Some(false) => self.control_action(ControlAction::Sneak),
None => warn!("Can't toggle sneak, client entity doesn't have a `CharacterState`"),
}
}
pub fn toggle_glide(&mut self) {
let using_glider = self
.state
.ecs()
.read_storage::<CharacterState>()
.get(self.entity())
.map(|cs| matches!(cs, CharacterState::GlideWield(_) | CharacterState::Glide(_)));
match using_glider {
Some(true) => self.control_action(ControlAction::Unwield),
Some(false) => self.control_action(ControlAction::GlideWield),
None => warn!("Can't toggle glide, client entity doesn't have a `CharacterState`"),
}
}
pub fn handle_input(
&mut self,
input: InputKind,
pressed: bool,
select_pos: Option<Vec3<f32>>,
target_entity: Option<EcsEntity>,
) {
if pressed {
self.control_action(ControlAction::StartInput {
input,
target_entity: target_entity.and_then(|e| self.state.read_component_copied(e)),
select_pos,
});
} else {
self.control_action(ControlAction::CancelInput(input));
}
}
pub fn activate_portal(&mut self, portal: Uid) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::ActivatePortal(
portal,
)));
}
fn control_action(&mut self, control_action: ControlAction) {
if let Some(controller) = self
.state
.ecs()
.write_storage::<Controller>()
.get_mut(self.entity())
{
controller.push_action(control_action);
}
self.send_msg(ClientGeneral::ControlAction(control_action));
}
pub fn view_distance(&self) -> Option<u32> { self.view_distance }
pub fn server_view_distance_limit(&self) -> Option<u32> { self.server_view_distance_limit }
pub fn loaded_distance(&self) -> f32 { self.loaded_distance }
pub fn position(&self) -> Option<Vec3<f32>> {
self.state
.read_storage::<comp::Pos>()
.get(self.entity())
.map(|v| v.0)
}
/// Returns Weather::default if no player position exists.
pub fn weather_at_player(&self) -> Weather {
self.position()
.map(|wpos| self.state.weather_at(wpos.xy()))
.unwrap_or_default()
}
pub fn current_chunk(&self) -> Option<Arc<TerrainChunk>> {
let chunk_pos = Vec2::from(self.position()?)
.map2(TerrainChunkSize::RECT_SIZE, |e: f32, sz| {
(e as u32).div_euclid(sz) as i32
});
self.state.terrain().get_key_arc(chunk_pos).cloned()
}
pub fn current<C: Component>(&self) -> Option<C>
where
C: Clone,
{
self.state.read_storage::<C>().get(self.entity()).cloned()
}
pub fn current_biome(&self) -> BiomeKind {
match self.current_chunk() {
Some(chunk) => chunk.meta().biome(),
_ => BiomeKind::Void,
}
}
pub fn current_site(&self) -> SiteKindMeta {
let mut player_alt = 0.0;
if let Some(position) = self.current::<comp::Pos>() {
player_alt = position.0.z;
}
//let mut contains_cave = false;
let mut terrain_alt = 0.0;
let mut site = None;
if let Some(chunk) = self.current_chunk() {
terrain_alt = chunk.meta().alt();
//contains_cave = chunk.meta().contains_cave();
site = chunk.meta().site();
}
if player_alt < terrain_alt - 40.0 {
if let Some(SiteKindMeta::Dungeon(dungeon)) = site {
SiteKindMeta::Dungeon(dungeon)
} else {
SiteKindMeta::Cave
}
} else if matches!(site, Some(SiteKindMeta::Dungeon(DungeonKindMeta::Old))) {
// If the player is in a dungeon chunk but aboveground, pass Void instead
SiteKindMeta::Void
} else {
site.unwrap_or_default()
}
}
pub fn request_site_economy(&mut self, id: SiteId) {
self.send_msg(ClientGeneral::RequestSiteInfo(id))
}
pub fn inventories(&self) -> ReadStorage<comp::Inventory> { self.state.read_storage() }
/// Send a chat message to the server.
pub fn send_chat(&mut self, message: String) { self.send_msg(ClientGeneral::ChatMsg(message)); }
/// Send a command to the server.
pub fn send_command(&mut self, name: String, args: Vec<String>) {
self.send_msg(ClientGeneral::Command(name, args));
}
/// Remove all cached terrain
pub fn clear_terrain(&mut self) {
self.state.clear_terrain();
self.pending_chunks.clear();
}
pub fn place_block(&mut self, pos: Vec3<i32>, block: Block) {
self.send_msg(ClientGeneral::PlaceBlock(pos, block));
}
pub fn remove_block(&mut self, pos: Vec3<i32>) {
self.send_msg(ClientGeneral::BreakBlock(pos));
}
pub fn collect_block(&mut self, pos: Vec3<i32>) {
self.control_action(ControlAction::InventoryAction(InventoryAction::Collect(
pos,
)));
}
pub fn change_ability(&mut self, slot: usize, new_ability: comp::ability::AuxiliaryAbility) {
let auxiliary_key = self
.inventories()
.get(self.entity())
.map_or((None, None), |inv| {
let tool_kind = |slot| {
inv.equipped(slot).and_then(|item| match &*item.kind() {
ItemKind::Tool(tool) => Some(tool.kind),
_ => None,
})
};
(
tool_kind(EquipSlot::ActiveMainhand),
tool_kind(EquipSlot::ActiveOffhand),
)
});
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::ChangeAbility {
slot,
auxiliary_key,
new_ability,
}))
}
/// Execute a single client tick, handle input and update the game state by
/// the given duration.
pub fn tick(&mut self, inputs: ControllerInputs, dt: Duration) -> Result<Vec<Event>, Error> {
span!(_guard, "tick", "Client::tick");
// This tick function is the centre of the Veloren universe. Most client-side
// things are managed from here, and as such it's important that it
// stays organised. Please consult the core developers before making
// significant changes to this code. Here is the approximate order of
// things. Please update it as this code changes.
//
// 1) Collect input from the frontend, apply input effects to the state of the
// game
// 2) Handle messages from the server
// 3) Go through any events (timer-driven or otherwise) that need handling and
// apply them to the state of the game
// 4) Perform a single LocalState tick (i.e: update the world and entities in
// the world)
// 5) Go through the terrain update queue and apply all changes to the terrain
// 6) Sync information to the server
// 7) Finish the tick, passing actions of the main thread back to the frontend
// 1) Handle input from frontend.
// Pass character actions from frontend input to the player's entity.
if self.presence.is_some() {
prof_span!("handle and send inputs");
if let Err(e) = self
.state
.ecs()
.write_storage::<Controller>()
.entry(self.entity())
.map(|entry| {
entry
.or_insert_with(|| Controller {
inputs: inputs.clone(),
queued_inputs: BTreeMap::new(),
events: Vec::new(),
actions: Vec::new(),
})
.inputs = inputs.clone();
})
{
let entry = self.entity();
error!(
?e,
?entry,
"Couldn't access controller component on client entity"
);
}
self.send_msg_err(ClientGeneral::ControllerInputs(Box::new(inputs)))?;
}
// 2) Build up a list of events for this frame, to be passed to the frontend.
let mut frontend_events = Vec::new();
// Prepare for new events
{
prof_span!("Last<CharacterState> comps update");
let ecs = self.state.ecs();
let mut last_character_states = ecs.write_storage::<comp::Last<CharacterState>>();
for (entity, _, character_state) in (
&ecs.entities(),
&ecs.read_storage::<comp::Body>(),
&ecs.read_storage::<CharacterState>(),
)
.join()
{
if let Some(l) = last_character_states
.entry(entity)
.ok()
.map(|l| l.or_insert_with(|| comp::Last(character_state.clone())))
// TODO: since this just updates when the variant changes we should
// just store the variant to avoid the clone overhead
.filter(|l| !character_state.same_variant(&l.0))
{
*l = comp::Last(character_state.clone());
}
}
}
// Handle new messages from the server.
frontend_events.append(&mut self.handle_new_messages()?);
// 3) Update client local data
// Check if the invite has timed out and remove if so
if self
.invite
.map_or(false, |(_, timeout, dur, _)| timeout.elapsed() > dur)
{
self.invite = None;
}
// Lerp the clientside weather.
self.weather.update(&mut self.state.weather_grid_mut());
if let Some(target_tod) = self.target_time_of_day {
let mut tod = self.state.ecs_mut().write_resource::<TimeOfDay>();
tod.0 = target_tod.0;
self.target_time_of_day = None;
}
// 4) Tick the client's LocalState
self.state.tick(
Duration::from_secs_f64(dt.as_secs_f64() * self.dt_adjustment),
true,
None,
&self.connected_server_constants,
|_, _| {},
);
// TODO: avoid emitting these in the first place
let _ = self
.state
.ecs()
.fetch::<EventBus<common::event::ServerEvent>>()
.recv_all();
// TODO: avoid emitting these in the first place OR actually use outcomes
// generated locally on the client (if they can be deduplicated from
// ones that the server generates or if the client can reliably generate
// them (e.g. syncing skipping character states past certain
// stages might skip points where outcomes are generated, however we might not
// care about this?) and the server doesn't need to send them)
let _ = self.state.ecs().fetch::<EventBus<Outcome>>().recv_all();
// 5) Terrain
self.tick_terrain()?;
// Send a ping to the server once every second
if self.state.get_program_time() - self.last_server_ping > 1. {
self.send_msg_err(PingMsg::Ping)?;
self.last_server_ping = self.state.get_program_time();
}
// 6) Update the server about the player's physics attributes.
if self.presence.is_some() {
if let (Some(pos), Some(vel), Some(ori)) = (
self.state.read_storage().get(self.entity()).cloned(),
self.state.read_storage().get(self.entity()).cloned(),
self.state.read_storage().get(self.entity()).cloned(),
) {
self.in_game_stream.send(ClientGeneral::PlayerPhysics {
pos,
vel,
ori,
force_counter: self.force_update_counter,
})?;
}
}
/*
// Output debug metrics
if log_enabled!(Level::Info) && self.tick % 600 == 0 {
let metrics = self
.state
.terrain()
.iter()
.fold(ChonkMetrics::default(), |a, (_, c)| a + c.get_metrics());
info!("{:?}", metrics);
}
*/
// 7) Finish the tick, pass control back to the frontend.
self.tick += 1;
Ok(frontend_events)
}
/// Clean up the client after a tick.
pub fn cleanup(&mut self) {
// Cleanup the local state
self.state.cleanup();
}
/// Handles terrain addition and removal.
///
/// Removes old terrain chunks outside the view distance.
/// Sends requests for missing chunks within the view distance.
fn tick_terrain(&mut self) -> Result<(), Error> {
let pos = self
.state
.read_storage::<comp::Pos>()
.get(self.entity())
.cloned();
if let (Some(pos), Some(view_distance)) = (pos, self.view_distance) {
prof_span!("terrain");
let chunk_pos = self.state.terrain().pos_key(pos.0.map(|e| e as i32));
// Remove chunks that are too far from the player.
let mut chunks_to_remove = Vec::new();
self.state.terrain().iter().for_each(|(key, _)| {
// Subtract 2 from the offset before computing squared magnitude
// 1 for the chunks needed bordering other chunks for meshing
// 1 as a buffer so that if the player moves back in that direction the chunks
// don't need to be reloaded
// Take the minimum of the adjusted difference vs the view_distance + 1 to
// prevent magnitude_squared from overflowing
if (chunk_pos - key)
.map(|e: i32| (e.unsigned_abs()).saturating_sub(2).min(view_distance + 1))
.magnitude_squared()
> view_distance.pow(2)
{
chunks_to_remove.push(key);
}
});
for key in chunks_to_remove {
self.state.remove_chunk(key);
}
let mut current_tick_send_chunk_requests = 0;
// Request chunks from the server.
self.loaded_distance = ((view_distance * TerrainChunkSize::RECT_SIZE.x) as f32).powi(2);
// +1 so we can find a chunk that's outside the vd for better fog
for dist in 0..view_distance as i32 + 1 {
// Only iterate through chunks that need to be loaded for circular vd
// The (dist - 2) explained:
// -0.5 because a chunk is visible if its corner is within the view distance
// -0.5 for being able to move to the corner of the current chunk
// -1 because chunks are not meshed if they don't have all their neighbors
// (notice also that view_distance is decreased by 1)
// (this subtraction on vd is omitted elsewhere in order to provide
// a buffer layer of loaded chunks)
let top = if 2 * (dist - 2).max(0).pow(2) > (view_distance - 1).pow(2) as i32 {
((view_distance - 1).pow(2) as f32 - (dist - 2).pow(2) as f32)
.sqrt()
.round() as i32
+ 1
} else {
dist
};
let mut skip_mode = false;
for i in -top..top + 1 {
let keys = [
chunk_pos + Vec2::new(dist, i),
chunk_pos + Vec2::new(i, dist),
chunk_pos + Vec2::new(-dist, i),
chunk_pos + Vec2::new(i, -dist),
];
for key in keys.iter() {
let dist_to_player = (TerrainGrid::key_chunk(*key).map(|x| x as f32)
+ TerrainChunkSize::RECT_SIZE.map(|x| x as f32) / 2.0)
.distance_squared(pos.0.into());
let terrain = self.state.terrain();
if let Some(chunk) = terrain.get_key_arc(*key) {
if !skip_mode && !terrain.contains_key_real(*key) {
let chunk = Arc::clone(chunk);
drop(terrain);
self.state.insert_chunk(*key, chunk);
}
} else {
drop(terrain);
if !skip_mode && !self.pending_chunks.contains_key(key) {
const TOTAL_PENDING_CHUNKS_LIMIT: usize = 12;
const CURRENT_TICK_PENDING_CHUNKS_LIMIT: usize = 2;
if self.pending_chunks.len() < TOTAL_PENDING_CHUNKS_LIMIT
&& current_tick_send_chunk_requests
< CURRENT_TICK_PENDING_CHUNKS_LIMIT
{
self.send_msg_err(ClientGeneral::TerrainChunkRequest {
key: *key,
})?;
current_tick_send_chunk_requests += 1;
self.pending_chunks.insert(*key, Instant::now());
} else {
skip_mode = true;
}
}
if dist_to_player < self.loaded_distance {
self.loaded_distance = dist_to_player;
}
}
}
}
}
self.loaded_distance = self.loaded_distance.sqrt()
- ((TerrainChunkSize::RECT_SIZE.x as f32 / 2.0).powi(2)
+ (TerrainChunkSize::RECT_SIZE.y as f32 / 2.0).powi(2))
.sqrt();
// If chunks are taking too long, assume they're no longer pending.
let now = Instant::now();
self.pending_chunks
.retain(|_, created| now.duration_since(*created) < Duration::from_secs(3));
}
if let Some(lod_pos) = pos.map(|p| p.0.xy()).or(self.lod_pos_fallback) {
// Manage LoD zones
let lod_zone = lod_pos.map(|e| lod::from_wpos(e as i32));
// Request LoD zones that are in range
if self
.lod_last_requested
.map_or(true, |i| i.elapsed() > Duration::from_secs(5))
{
if let Some(rpos) = Spiral2d::new()
.take((1 + self.lod_distance.ceil() as i32 * 2).pow(2) as usize)
.filter(|rpos| !self.lod_zones.contains_key(&(lod_zone + *rpos)))
.min_by_key(|rpos| rpos.magnitude_squared())
.filter(|rpos| {
rpos.map(|e| e as f32).magnitude() < (self.lod_distance - 0.5).max(0.0)
})
{
self.send_msg_err(ClientGeneral::LodZoneRequest {
key: lod_zone + rpos,
})?;
self.lod_last_requested = Some(Instant::now());
}
}
// Cull LoD zones out of range
self.lod_zones.retain(|p, _| {
(*p - lod_zone).map(|e| e as f32).magnitude_squared() < self.lod_distance.powi(2)
});
}
Ok(())
}
fn handle_server_msg(
&mut self,
frontend_events: &mut Vec<Event>,
msg: ServerGeneral,
) -> Result<(), Error> {
prof_span!("handle_server_msg");
match msg {
ServerGeneral::Disconnect(reason) => match reason {
DisconnectReason::Shutdown => return Err(Error::ServerShutdown),
DisconnectReason::Kicked(reason) => {
debug!("sending ClientMsg::Terminate because we got kicked");
frontend_events.push(Event::Kicked(reason));
self.send_msg_err(ClientGeneral::Terminate)?;
},
},
ServerGeneral::PlayerListUpdate(PlayerListUpdate::Init(list)) => {
self.player_list = list
},
ServerGeneral::PlayerListUpdate(PlayerListUpdate::Add(uid, player_info)) => {
if let Some(old_player_info) = self.player_list.insert(uid, player_info.clone()) {
warn!(
"Received msg to insert {} with uid {} into the player list but there was \
already an entry for {} with the same uid that was overwritten!",
player_info.player_alias, uid, old_player_info.player_alias
);
}
},
ServerGeneral::PlayerListUpdate(PlayerListUpdate::Moderator(uid, moderator)) => {
if let Some(player_info) = self.player_list.get_mut(&uid) {
player_info.is_moderator = moderator;
} else {
warn!(
"Received msg to update admin status of uid {}, but they were not in the \
list.",
uid
);
}
},
ServerGeneral::PlayerListUpdate(PlayerListUpdate::SelectedCharacter(
uid,
char_info,
)) => {
if let Some(player_info) = self.player_list.get_mut(&uid) {
player_info.character = Some(char_info);
} else {
warn!(
"Received msg to update character info for uid {}, but they were not in \
the list.",
uid
);
}
},
ServerGeneral::PlayerListUpdate(PlayerListUpdate::LevelChange(uid, next_level)) => {
if let Some(player_info) = self.player_list.get_mut(&uid) {
player_info.character = match &player_info.character {
Some(character) => Some(msg::CharacterInfo {
name: character.name.to_string(),
}),
None => {
warn!(
"Received msg to update character level info to {} for uid {}, \
but this player's character is None.",
next_level, uid
);
None
},
};
}
},
ServerGeneral::PlayerListUpdate(PlayerListUpdate::Remove(uid)) => {
// Instead of removing players, mark them as offline because we need to
// remember the names of disconnected players in chat.
//
// TODO: consider alternatives since this leads to an ever growing list as
// players log out and in. Keep in mind we might only want to
// keep only so many messages in chat the history. We could
// potentially use an ID that's more persistent than the Uid.
// One of the reasons we don't just store the string of the player name
// into the message is to make alias changes reflected in older messages.
if let Some(player_info) = self.player_list.get_mut(&uid) {
if player_info.is_online {
player_info.is_online = false;
} else {
warn!(
"Received msg to remove uid {} from the player list by they were \
already marked offline",
uid
);
}
} else {
warn!(
"Received msg to remove uid {} from the player list by they weren't in \
the list!",
uid
);
}
},
ServerGeneral::PlayerListUpdate(PlayerListUpdate::Alias(uid, new_name)) => {
if let Some(player_info) = self.player_list.get_mut(&uid) {
player_info.player_alias = new_name;
} else {
warn!(
"Received msg to alias player with uid {} to {} but this uid is not in \
the player list",
uid, new_name
);
}
},
ServerGeneral::ChatMsg(m) => frontend_events.push(Event::Chat(m)),
ServerGeneral::ChatMode(m) => {
self.chat_mode = m;
},
ServerGeneral::SetPlayerEntity(uid) => {
if let Some(entity) = self.state.ecs().entity_from_uid(uid) {
let old_player_entity = mem::replace(
&mut *self.state.ecs_mut().write_resource(),
PlayerEntity(Some(entity)),
);
if let Some(old_entity) = old_player_entity.0 {
// Transfer controller to the new entity.
let mut controllers = self.state.ecs().write_storage::<Controller>();
if let Some(controller) = controllers.remove(old_entity) {
if let Err(e) = controllers.insert(entity, controller) {
error!(
?e,
"Failed to insert controller when setting new player entity!"
);
}
}
}
if let Some(presence) = self.presence {
self.presence = Some(match presence {
PresenceKind::Spectator => PresenceKind::Spectator,
PresenceKind::LoadingCharacter(_) => PresenceKind::Possessor,
PresenceKind::Character(_) => PresenceKind::Possessor,
PresenceKind::Possessor => PresenceKind::Possessor,
});
}
// Clear pending trade
self.pending_trade = None;
} else {
return Err(Error::Other("Failed to find entity from uid.".into()));
}
},
ServerGeneral::TimeOfDay(time_of_day, calendar, new_time, time_scale) => {
self.target_time_of_day = Some(time_of_day);
*self.state.ecs_mut().write_resource() = calendar;
*self.state.ecs_mut().write_resource() = time_scale;
let mut time = self.state.ecs_mut().write_resource::<Time>();
// Avoid side-eye from Einstein
// If new time from server is at least 5 seconds ahead, replace client time.
// Otherwise try to slightly twean client time (by 1%) to keep it in line with
// server time.
self.dt_adjustment = if new_time.0 > time.0 + 5.0 {
*time = new_time;
1.0
} else if new_time.0 > time.0 {
1.01
} else {
0.99
};
},
ServerGeneral::EntitySync(entity_sync_package) => {
let uid = self.uid();
self.state
.ecs_mut()
.apply_entity_sync_package(entity_sync_package, uid);
},
ServerGeneral::CompSync(comp_sync_package, force_counter) => {
self.force_update_counter = force_counter;
self.state
.ecs_mut()
.apply_comp_sync_package(comp_sync_package);
},
ServerGeneral::CreateEntity(entity_package) => {
self.state.ecs_mut().apply_entity_package(entity_package);
},
ServerGeneral::DeleteEntity(entity_uid) => {
if self.uid() != Some(entity_uid) {
self.state
.ecs_mut()
.delete_entity_and_clear_uid_mapping(entity_uid);
}
},
ServerGeneral::Notification(n) => {
frontend_events.push(Event::Notification(n));
},
_ => unreachable!("Not a general msg"),
}
Ok(())
}
fn handle_server_in_game_msg(
&mut self,
frontend_events: &mut Vec<Event>,
msg: ServerGeneral,
) -> Result<(), Error> {
prof_span!("handle_server_in_game_msg");
match msg {
ServerGeneral::GroupUpdate(change_notification) => {
use comp::group::ChangeNotification::*;
// Note: we use a hashmap since this would not work with entities outside
// the view distance
match change_notification {
Added(uid, role) => {
// Check if this is a newly formed group by looking for absence of
// other non pet group members
if !matches!(role, group::Role::Pet)
&& !self
.group_members
.values()
.any(|r| !matches!(r, group::Role::Pet))
{
frontend_events
// TODO: localise
.push(Event::Chat(comp::ChatType::Meta.into_plain_msg(
"Type /g or /group to chat with your group members",
)));
}
if let Some(player_info) = self.player_list.get(&uid) {
frontend_events.push(Event::Chat(
// TODO: localise
comp::ChatType::GroupMeta("Group".into()).into_plain_msg(format!(
"[{}] joined group",
self.personalize_alias(uid, player_info.player_alias.clone())
)),
));
}
if self.group_members.insert(uid, role) == Some(role) {
warn!(
"Received msg to add uid {} to the group members but they were \
already there",
uid
);
}
},
Removed(uid) => {
if let Some(player_info) = self.player_list.get(&uid) {
frontend_events.push(Event::Chat(
// TODO: localise
comp::ChatType::GroupMeta("Group".into()).into_plain_msg(format!(
"[{}] left group",
self.personalize_alias(uid, player_info.player_alias.clone())
)),
));
frontend_events.push(Event::MapMarker(
comp::MapMarkerUpdate::GroupMember(uid, MapMarkerChange::Remove),
));
}
if self.group_members.remove(&uid).is_none() {
warn!(
"Received msg to remove uid {} from group members but by they \
weren't in there!",
uid
);
}
},
NewLeader(leader) => {
self.group_leader = Some(leader);
},
NewGroup { leader, members } => {
self.group_leader = Some(leader);
self.group_members = members.into_iter().collect();
// Currently add/remove messages treat client as an implicit member
// of the group whereas this message explicitly includes them so to
// be consistent for now we will remove the client from the
// received hashset
if let Some(uid) = self.uid() {
self.group_members.remove(&uid);
}
frontend_events.push(Event::MapMarker(comp::MapMarkerUpdate::ClearGroup));
},
NoGroup => {
self.group_leader = None;
self.group_members = HashMap::new();
frontend_events.push(Event::MapMarker(comp::MapMarkerUpdate::ClearGroup));
},
}
},
ServerGeneral::Invite {
inviter,
timeout,
kind,
} => {
self.invite = Some((inviter, Instant::now(), timeout, kind));
},
ServerGeneral::InvitePending(uid) => {
if !self.pending_invites.insert(uid) {
warn!("Received message about pending invite that was already pending");
}
},
ServerGeneral::InviteComplete {
target,
answer,
kind,
} => {
if !self.pending_invites.remove(&target) {
warn!(
"Received completed invite message for invite that was not in the list of \
pending invites"
)
}
frontend_events.push(Event::InviteComplete {
target,
answer,
kind,
});
},
ServerGeneral::GroupInventoryUpdate(item, taker, uid) => {
frontend_events.push(Event::GroupInventoryUpdate(item, taker, uid));
},
// Cleanup for when the client goes back to the `presence = None`
ServerGeneral::ExitInGameSuccess => {
self.presence = None;
self.clean_state();
},
ServerGeneral::InventoryUpdate(inventory, events) => {
let mut update_inventory = false;
for event in events.iter() {
match event {
InventoryUpdateEvent::BlockCollectFailed { .. } => {},
InventoryUpdateEvent::EntityCollectFailed { .. } => {},
_ => update_inventory = true,
}
}
if update_inventory {
// Push the updated inventory component to the client
// FIXME: Figure out whether this error can happen under normal gameplay,
// if not find a better way to handle it, if so maybe consider kicking the
// client back to login?
let entity = self.entity();
if let Err(e) = self
.state
.ecs_mut()
.write_storage()
.insert(entity, inventory)
{
warn!(
?e,
"Received an inventory update event for client entity, but this \
entity was not found... this may be a bug."
);
}
}
self.update_available_recipes();
frontend_events.push(Event::InventoryUpdated(events));
},
ServerGeneral::SetViewDistance(vd) => {
self.view_distance = Some(vd);
frontend_events.push(Event::SetViewDistance(vd));
// If the server is correcting client vd selection we assume this is the max
// allowed view distance.
self.server_view_distance_limit = Some(vd);
},
ServerGeneral::Outcomes(outcomes) => {
frontend_events.extend(outcomes.into_iter().map(Event::Outcome))
},
ServerGeneral::Knockback(impulse) => {
self.state
.ecs()
.read_resource::<EventBus<LocalEvent>>()
.emit_now(LocalEvent::ApplyImpulse {
entity: self.entity(),
impulse,
});
},
ServerGeneral::UpdatePendingTrade(id, trade, pricing) => {
trace!("UpdatePendingTrade {:?} {:?}", id, trade);
self.pending_trade = Some((id, trade, pricing));
},
ServerGeneral::FinishedTrade(result) => {
if let Some((_, trade, _)) = self.pending_trade.take() {
self.update_available_recipes();
frontend_events.push(Event::TradeComplete { result, trade })
}
},
ServerGeneral::SiteEconomy(economy) => {
if let Some(rich) = self.sites_mut().get_mut(&economy.id) {
rich.economy = Some(economy);
}
},
ServerGeneral::MapMarker(event) => {
frontend_events.push(Event::MapMarker(event));
},
ServerGeneral::WeatherUpdate(weather) => {
self.weather.weather_update(weather);
},
ServerGeneral::SpectatePosition(pos) => {
frontend_events.push(Event::SpectatePosition(pos));
},
_ => unreachable!("Not a in_game message"),
}
Ok(())
}
fn handle_server_terrain_msg(&mut self, msg: ServerGeneral) -> Result<(), Error> {
prof_span!("handle_server_terrain_mgs");
match msg {
ServerGeneral::TerrainChunkUpdate { key, chunk } => {
if let Some(chunk) = chunk.ok().and_then(|c| c.to_chunk()) {
self.state.insert_chunk(key, Arc::new(chunk));
}
self.pending_chunks.remove(&key);
},
ServerGeneral::LodZoneUpdate { key, zone } => {
self.lod_zones.insert(key, zone);
self.lod_last_requested = None;
},
ServerGeneral::TerrainBlockUpdates(blocks) => {
if let Some(mut blocks) = blocks.decompress() {
blocks.drain().for_each(|(pos, block)| {
self.state.set_block(pos, block);
});
}
},
_ => unreachable!("Not a terrain message"),
}
Ok(())
}
fn handle_server_character_screen_msg(
&mut self,
events: &mut Vec<Event>,
msg: ServerGeneral,
) -> Result<(), Error> {
prof_span!("handle_server_character_screen_msg");
match msg {
ServerGeneral::CharacterListUpdate(character_list) => {
self.character_list.characters = character_list;
self.character_list.loading = false;
},
ServerGeneral::CharacterActionError(error) => {
warn!("CharacterActionError: {:?}.", error);
events.push(Event::CharacterError(error));
},
ServerGeneral::CharacterDataLoadResult(Ok(metadata)) => {
trace!("Handling join result by server");
events.push(Event::CharacterJoined(metadata));
},
ServerGeneral::CharacterDataLoadResult(Err(error)) => {
trace!("Handling join error by server");
self.presence = None;
self.clean_state();
events.push(Event::CharacterError(error));
},
ServerGeneral::CharacterCreated(character_id) => {
events.push(Event::CharacterCreated(character_id));
},
ServerGeneral::CharacterEdited(character_id) => {
events.push(Event::CharacterEdited(character_id));
},
ServerGeneral::CharacterSuccess => debug!("client is now in ingame state on server"),
ServerGeneral::SpectatorSuccess(spawn_point) => {
events.push(Event::StartSpectate(spawn_point));
debug!("client is now in ingame state on server");
},
_ => unreachable!("Not a character_screen msg"),
}
Ok(())
}
fn handle_ping_msg(&mut self, msg: PingMsg) -> Result<(), Error> {
prof_span!("handle_ping_msg");
match msg {
PingMsg::Ping => {
self.send_msg_err(PingMsg::Pong)?;
},
PingMsg::Pong => {
self.last_server_pong = self.state.get_program_time();
self.last_ping_delta = self.state.get_program_time() - self.last_server_ping;
// Maintain the correct number of deltas for calculating the rolling average
// ping. The client sends a ping to the server every second so we should be
// receiving a pong reply roughly every second.
while self.ping_deltas.len() > PING_ROLLING_AVERAGE_SECS - 1 {
self.ping_deltas.pop_front();
}
self.ping_deltas.push_back(self.last_ping_delta);
},
}
Ok(())
}
fn handle_messages(&mut self, frontend_events: &mut Vec<Event>) -> Result<u64, Error> {
let mut cnt = 0;
#[cfg(feature = "tracy")]
let (mut terrain_cnt, mut ingame_cnt) = (0, 0);
loop {
let cnt_start = cnt;
while let Some(msg) = self.general_stream.try_recv()? {
cnt += 1;
self.handle_server_msg(frontend_events, msg)?;
}
while let Some(msg) = self.ping_stream.try_recv()? {
cnt += 1;
self.handle_ping_msg(msg)?;
}
while let Some(msg) = self.character_screen_stream.try_recv()? {
cnt += 1;
self.handle_server_character_screen_msg(frontend_events, msg)?;
}
while let Some(msg) = self.in_game_stream.try_recv()? {
cnt += 1;
#[cfg(feature = "tracy")]
{
ingame_cnt += 1;
}
self.handle_server_in_game_msg(frontend_events, msg)?;
}
while let Some(msg) = self.terrain_stream.try_recv()? {
cnt += 1;
#[cfg(feature = "tracy")]
{
if let ServerGeneral::TerrainChunkUpdate { chunk, .. } = &msg {
terrain_cnt += chunk.as_ref().map(|x| x.approx_len()).unwrap_or(0);
}
}
self.handle_server_terrain_msg(msg)?;
}
if cnt_start == cnt {
#[cfg(feature = "tracy")]
{
plot!("terrain_recvs", terrain_cnt as f64);
plot!("ingame_recvs", ingame_cnt as f64);
}
return Ok(cnt);
}
}
}
/// Handle new server messages.
fn handle_new_messages(&mut self) -> Result<Vec<Event>, Error> {
prof_span!("handle_new_messages");
let mut frontend_events = Vec::new();
// Check that we have an valid connection.
// Use the last ping time as a 1s rate limiter, we only notify the user once per
// second
if self.state.get_program_time() - self.last_server_ping > 1. {
let duration_since_last_pong = self.state.get_program_time() - self.last_server_pong;
// Dispatch a notification to the HUD warning they will be kicked in {n} seconds
const KICK_WARNING_AFTER_REL_TO_TIMEOUT_FRACTION: f64 = 0.75;
if duration_since_last_pong
>= (self.client_timeout.as_secs() as f64
* KICK_WARNING_AFTER_REL_TO_TIMEOUT_FRACTION)
&& self.state.get_program_time() - duration_since_last_pong > 0.
{
frontend_events.push(Event::DisconnectionNotification(
(self.state.get_program_time() - duration_since_last_pong).round() as u64,
));
}
}
let msg_count = self.handle_messages(&mut frontend_events)?;
if msg_count == 0
&& self.state.get_program_time() - self.last_server_pong
> self.client_timeout.as_secs() as f64
{
dbg!(self.state.get_program_time());
dbg!(self.last_server_pong);
return Err(Error::ServerTimeout);
}
// ignore network events
while let Some(res) = self
.participant
.as_mut()
.and_then(|p| p.try_fetch_event().transpose())
{
let event = res?;
trace!(?event, "received network event");
}
Ok(frontend_events)
}
pub fn entity(&self) -> EcsEntity {
self.state
.ecs()
.read_resource::<PlayerEntity>()
.0
.expect("Client::entity should always have PlayerEntity be Some")
}
pub fn uid(&self) -> Option<Uid> { self.state.read_component_copied(self.entity()) }
pub fn presence(&self) -> Option<PresenceKind> { self.presence }
pub fn registered(&self) -> bool { self.registered }
pub fn get_tick(&self) -> u64 { self.tick }
pub fn get_ping_ms(&self) -> f64 { self.last_ping_delta * 1000.0 }
pub fn get_ping_ms_rolling_avg(&self) -> f64 {
let mut total_weight = 0.;
let pings = self.ping_deltas.len() as f64;
(self
.ping_deltas
.iter()
.enumerate()
.fold(0., |acc, (i, ping)| {
let weight = i as f64 + 1. / pings;
total_weight += weight;
acc + (weight * ping)
})
/ total_weight)
* 1000.0
}
/// Get a reference to the client's runtime thread pool. This pool should be
/// used for any computationally expensive operations that run outside
/// of the main thread (i.e., threads that block on I/O operations are
/// exempt).
pub fn runtime(&self) -> &Arc<Runtime> { &self.runtime }
/// Get a reference to the client's game state.
pub fn state(&self) -> &State { &self.state }
/// Get a mutable reference to the client's game state.
pub fn state_mut(&mut self) -> &mut State { &mut self.state }
/// Returns an iterator over the aliases of all the online players on the
/// server
pub fn players(&self) -> impl Iterator<Item = &str> {
self.player_list()
.values()
.filter_map(|player_info| player_info.is_online.then_some(&*player_info.player_alias))
}
/// Return true if this client is a moderator on the server
pub fn is_moderator(&self) -> bool {
let client_uid = self
.state
.read_component_copied::<Uid>(self.entity())
.expect("Client doesn't have a Uid!!!");
self.player_list
.get(&client_uid)
.map_or(false, |info| info.is_moderator)
}
/// Clean client ECS state
fn clean_state(&mut self) {
// Clear pending trade
self.pending_trade = None;
let client_uid = self.uid().expect("Client doesn't have a Uid!!!");
// Clear ecs of all entities
self.state.ecs_mut().delete_all();
self.state.ecs_mut().maintain();
self.state.ecs_mut().insert(IdMaps::default());
// Recreate client entity with Uid
let entity_builder = self.state.ecs_mut().create_entity();
entity_builder
.world
.write_resource::<IdMaps>()
.add_entity(client_uid, entity_builder.entity);
let entity = entity_builder.with(client_uid).build();
self.state.ecs().write_resource::<PlayerEntity>().0 = Some(entity);
}
/// Change player alias to "You" if client belongs to matching player
pub fn personalize_alias(&self, uid: Uid, alias: String) -> String {
let client_uid = self.uid().expect("Client doesn't have a Uid!!!");
if client_uid == uid {
"You".to_string() // TODO: Localize
} else {
alias
}
}
/// Get important information from client that is necessary for message
/// localisation
pub fn lookup_msg_context(&self, msg: &comp::ChatMsg) -> ChatTypeContext {
let mut result = ChatTypeContext {
you: self.uid().expect("Client doesn't have a Uid!!!"),
player_alias: HashMap::new(),
entity_name: HashMap::new(),
};
let name_of_uid = |uid| {
let ecs = self.state.ecs();
(
&ecs.read_storage::<comp::Stats>(),
&ecs.read_storage::<Uid>(),
)
.join()
.find(|(_, u)| u == &uid)
.map(|(c, _)| c.name.clone())
};
let mut alias_of_uid = |uid| match self.player_list.get(uid) {
Some(player_info) => {
result.player_alias.insert(*uid, player_info.clone());
},
None => {
result
.entity_name
.insert(*uid, name_of_uid(uid).unwrap_or_else(|| "<?>".to_string()));
},
};
match &msg.chat_type {
comp::ChatType::Online(uid) | comp::ChatType::Offline(uid) => {
alias_of_uid(uid);
},
comp::ChatType::CommandError => (),
comp::ChatType::CommandInfo => (),
comp::ChatType::FactionMeta(_) => (),
comp::ChatType::GroupMeta(_) => (),
comp::ChatType::Kill(kill_source, victim) => {
alias_of_uid(victim);
match kill_source {
KillSource::Player(attacker_uid, _) => {
alias_of_uid(attacker_uid);
},
KillSource::NonPlayer(_, _) => (),
KillSource::Environment(_) => (),
KillSource::FallDamage => (),
KillSource::Suicide => (),
KillSource::NonExistent(_) => (),
KillSource::Other => (),
};
},
comp::ChatType::Tell(from, to) | comp::ChatType::NpcTell(from, to) => {
alias_of_uid(from);
alias_of_uid(to);
},
comp::ChatType::Say(uid)
| comp::ChatType::Region(uid)
| comp::ChatType::World(uid)
| comp::ChatType::NpcSay(uid) => {
alias_of_uid(uid);
},
comp::ChatType::Group(uid, _) | comp::ChatType::Faction(uid, _) => {
alias_of_uid(uid);
},
comp::ChatType::Npc(uid) => alias_of_uid(uid),
comp::ChatType::Meta => (),
};
result
}
/// Execute a single client tick:
/// - handles messages from the server
/// - sends physics update
/// - requests chunks
///
/// The game state is purposefully not simulated to reduce the overhead of
/// running the client. This method is for use in testing a server with
/// many clients connected.
#[cfg(feature = "tick_network")]
#[allow(clippy::needless_collect)] // False positive
pub fn tick_network(&mut self, dt: Duration) -> Result<(), Error> {
span!(_guard, "tick_network", "Client::tick_network");
// Advance state time manually since we aren't calling `State::tick`
self.state
.ecs()
.write_resource::<common::resources::ProgramTime>()
.0 += dt.as_secs_f64();
let time_scale = *self
.state
.ecs()
.read_resource::<common::resources::TimeScale>();
self.state
.ecs()
.write_resource::<common::resources::Time>()
.0 += dt.as_secs_f64() * time_scale.0;
// Handle new messages from the server.
self.handle_new_messages()?;
// 5) Terrain
self.tick_terrain()?;
let empty = Arc::new(TerrainChunk::new(
0,
Block::empty(),
Block::empty(),
common::terrain::TerrainChunkMeta::void(),
));
let mut terrain = self.state.terrain_mut();
// Replace chunks with empty chunks to save memory
let to_clear = terrain
.iter()
.filter_map(|(key, chunk)| (chunk.sub_chunks_len() != 0).then(|| key))
.collect::<Vec<_>>();
to_clear.into_iter().for_each(|key| {
terrain.insert(key, Arc::clone(&empty));
});
drop(terrain);
// Send a ping to the server once every second
if self.state.get_program_time() - self.last_server_ping > 1. {
self.send_msg_err(PingMsg::Ping)?;
self.last_server_ping = self.state.get_program_time();
}
// 6) Update the server about the player's physics attributes.
if self.presence.is_some() {
if let (Some(pos), Some(vel), Some(ori)) = (
self.state.read_storage().get(self.entity()).cloned(),
self.state.read_storage().get(self.entity()).cloned(),
self.state.read_storage().get(self.entity()).cloned(),
) {
self.in_game_stream.send(ClientGeneral::PlayerPhysics {
pos,
vel,
ori,
force_counter: self.force_update_counter,
})?;
}
}
// 7) Finish the tick, pass control back to the frontend.
self.tick += 1;
Ok(())
}
}
impl Drop for Client {
fn drop(&mut self) {
trace!("Dropping client");
if self.registered {
if let Err(e) = self.send_msg_err(ClientGeneral::Terminate) {
warn!(
?e,
"Error during drop of client, couldn't send disconnect package, is the \
connection already closed?",
);
}
} else {
trace!("no disconnect msg necessary as client wasn't registered")
}
tokio::task::block_in_place(|| {
if let Err(e) = self
.runtime
.block_on(self.participant.take().unwrap().disconnect())
{
warn!(?e, "error when disconnecting, couldn't send all data");
}
});
//explicitly drop the network here while the runtime is still existing
drop(self.network.take());
}
}
#[cfg(test)]
mod tests {
use super::*;
use client_i18n::LocalizationHandle;
#[test]
/// THIS TEST VERIFIES THE CONSTANT API.
/// CHANGING IT WILL BREAK 3rd PARTY APPLICATIONS (please extend) which
/// needs to be informed (or fixed)
/// - torvus: https://gitlab.com/veloren/torvus
/// CONTACT @Core Developer BEFORE MERGING CHANGES TO THIS TEST
fn constant_api_test() {
use common::clock::Clock;
use voxygen_i18n_helpers::localize_chat_message;
const SPT: f64 = 1.0 / 60.0;
let runtime = Arc::new(Runtime::new().unwrap());
let runtime2 = Arc::clone(&runtime);
let username = "Foo";
let password = "Bar";
let auth_server = "auth.veloren.net";
let veloren_client: Result<Client, Error> = runtime.block_on(Client::new(
ConnectionArgs::Tcp {
hostname: "127.0.0.1:9000".to_owned(),
prefer_ipv6: false,
},
runtime2,
&mut None,
username,
password,
None,
|suggestion: &str| suggestion == auth_server,
&|_| {},
|_| {},
));
let localisation = LocalizationHandle::load_expect("en");
let _ = veloren_client.map(|mut client| {
//clock
let mut clock = Clock::new(Duration::from_secs_f64(SPT));
//tick
let events_result: Result<Vec<Event>, Error> =
client.tick(ControllerInputs::default(), clock.dt());
//chat functionality
client.send_chat("foobar".to_string());
let _ = events_result.map(|mut events| {
// event handling
if let Some(event) = events.pop() {
match event {
Event::Chat(msg) => {
let msg: comp::ChatMsg = msg;
let _s: String = localize_chat_message(
msg,
|msg| client.lookup_msg_context(msg),
&localisation.read(),
true,
)
.1;
},
Event::Disconnect => {},
Event::DisconnectionNotification(_) => {
debug!("Will be disconnected soon! :/")
},
Event::Notification(notification) => {
let notification: Notification = notification;
debug!("Notification: {:?}", notification);
},
_ => {},
}
};
});
client.cleanup();
clock.tick();
});
}
}