veloren_client/
lib.rs

1#![deny(unsafe_code)]
2#![deny(clippy::clone_on_ref_ptr)]
3
4pub mod addr;
5pub mod error;
6
7// Reexports
8pub use crate::error::Error;
9pub use authc::AuthClientError;
10pub use common_net::msg::ServerInfo;
11pub use specs::{
12    Builder, DispatcherBuilder, Entity as EcsEntity, Join, LendJoin, ReadStorage, World, WorldExt,
13};
14
15use crate::addr::ConnectionArgs;
16use byteorder::{ByteOrder, LittleEndian};
17use common::{
18    character::{CharacterId, CharacterItem},
19    comp::{
20        self, AdminRole, CharacterState, ChatMode, ControlAction, ControlEvent, Controller,
21        ControllerInputs, GroupManip, Hardcore, InputKind, InventoryAction, InventoryEvent,
22        InventoryUpdateEvent, MapMarkerChange, PresenceKind, UtteranceKind,
23        chat::KillSource,
24        controller::CraftEvent,
25        group,
26        inventory::item::{ItemKind, modular, tool},
27        invite::{InviteKind, InviteResponse},
28        skills::Skill,
29        slot::{EquipSlot, InvSlotId, Slot},
30    },
31    event::{EventBus, LocalEvent, PluginHash, UpdateCharacterMetadata},
32    grid::Grid,
33    link::Is,
34    lod,
35    mounting::{Rider, VolumePos, VolumeRider},
36    outcome::Outcome,
37    recipe::{ComponentRecipeBook, RecipeBookManifest, RepairRecipeBook},
38    resources::{BattleMode, GameMode, PlayerEntity, Time, TimeOfDay},
39    rtsim,
40    shared_server_config::ServerConstants,
41    spiral::Spiral2d,
42    terrain::{
43        BiomeKind, CoordinateConversions, SiteKindMeta, SpriteKind, TerrainChunk, TerrainChunkSize,
44        TerrainGrid, block::Block, map::MapConfig, neighbors,
45    },
46    trade::{PendingTrade, SitePrices, TradeAction, TradeId, TradeResult},
47    uid::{IdMaps, Uid},
48    vol::RectVolSize,
49    weather::{CompressedWeather, SharedWeatherGrid, Weather, WeatherGrid},
50};
51#[cfg(feature = "tracy")] use common_base::plot;
52use common_base::{prof_span, span};
53use common_net::{
54    msg::{
55        ChatTypeContext, ClientGeneral, ClientMsg, ClientRegister, DisconnectReason, InviteAnswer,
56        Notification, PingMsg, PlayerInfo, PlayerListUpdate, RegisterError, ServerGeneral,
57        ServerInit, ServerRegisterAnswer,
58        server::ServerDescription,
59        world_msg::{EconomyInfo, Marker, PoiInfo, SiteId},
60    },
61    sync::WorldSyncExt,
62};
63
64pub use common_net::msg::ClientType;
65use common_state::State;
66#[cfg(feature = "plugins")]
67use common_state::plugin::PluginMgr;
68use common_systems::add_local_systems;
69use comp::BuffKind;
70use hashbrown::{HashMap, HashSet};
71use hickory_resolver::{
72    AsyncResolver,
73    config::{ResolverConfig, ResolverOpts},
74};
75use image::DynamicImage;
76use network::{ConnectAddr, Network, Participant, Pid, Stream};
77use num::traits::FloatConst;
78use rayon::prelude::*;
79use rustls::client::danger::ServerCertVerified;
80use specs::Component;
81use std::{
82    collections::{BTreeMap, VecDeque},
83    fmt::Debug,
84    mem,
85    path::PathBuf,
86    sync::Arc,
87    time::{Duration, Instant},
88};
89use tokio::runtime::Runtime;
90use tracing::{debug, error, trace, warn};
91use vek::*;
92
93pub const MAX_SELECTABLE_VIEW_DISTANCE: u32 = 65;
94
95const PING_ROLLING_AVERAGE_SECS: usize = 10;
96
97/// Client frontend events.
98///
99/// These events are returned to the frontend that ticks the client.
100#[derive(Debug)]
101pub enum Event {
102    Chat(comp::ChatMsg),
103    GroupInventoryUpdate(comp::FrontendItem, Uid),
104    InviteComplete {
105        target: Uid,
106        answer: InviteAnswer,
107        kind: InviteKind,
108    },
109    TradeComplete {
110        result: TradeResult,
111        trade: PendingTrade,
112    },
113    Disconnect,
114    DisconnectionNotification(u64),
115    InventoryUpdated(Vec<InventoryUpdateEvent>),
116    Notification(Notification),
117    SetViewDistance(u32),
118    Outcome(Outcome),
119    CharacterCreated(CharacterId),
120    CharacterEdited(CharacterId),
121    CharacterJoined(UpdateCharacterMetadata),
122    CharacterError(String),
123    MapMarker(comp::MapMarkerUpdate),
124    StartSpectate(Vec3<f32>),
125    SpectatePosition(Vec3<f32>),
126    PluginDataReceived(Vec<u8>),
127    Dialogue(Uid, rtsim::Dialogue<true>),
128}
129
130#[derive(Debug)]
131pub enum ClientInitStage {
132    /// A connection to the server is being created
133    ConnectionEstablish,
134    /// Waiting for server version
135    WatingForServerVersion,
136    /// We're currently authenticating with the server
137    Authentication,
138    /// Loading map data, site information, recipe information and other
139    /// initialization data
140    LoadingInitData,
141    /// Prepare data received by the server to be used by the client (insert
142    /// data into the ECS, render map)
143    StartingClient,
144}
145
146pub struct WorldData {
147    /// Just the "base" layer for LOD; currently includes colors and nothing
148    /// else. In the future we'll add more layers, like shadows, rivers, and
149    /// probably foliage, cities, roads, and other structures.
150    pub lod_base: Grid<u32>,
151    /// The "height" layer for LOD; currently includes only land altitudes, but
152    /// in the future should also water depth, and probably other
153    /// information as well.
154    pub lod_alt: Grid<u32>,
155    /// The "shadow" layer for LOD.  Includes east and west horizon angles and
156    /// an approximate max occluder height, which we use to try to
157    /// approximate soft and volumetric shadows.
158    pub lod_horizon: Grid<u32>,
159    /// A fully rendered map image for use with the map and minimap; note that
160    /// this can be constructed dynamically by combining the layers of world
161    /// map data (e.g. with shadow map data or river data), but at present
162    /// we opt not to do this.
163    ///
164    /// The first two elements of the tuple are the regular and topographic maps
165    /// respectively. The third element of the tuple is the world size (as a 2D
166    /// grid, in chunks), and the fourth element holds the minimum height for
167    /// any land chunk (i.e. the sea level) in its x coordinate, and the maximum
168    /// land height above this height (i.e. the max height) in its y coordinate.
169    map: (Vec<Arc<DynamicImage>>, Vec2<u16>, Vec2<f32>),
170}
171
172impl WorldData {
173    pub fn chunk_size(&self) -> Vec2<u16> { self.map.1 }
174
175    pub fn map_layers(&self) -> &Vec<Arc<DynamicImage>> { &self.map.0 }
176
177    pub fn map_image(&self) -> &Arc<DynamicImage> { &self.map.0[0] }
178
179    pub fn topo_map_image(&self) -> &Arc<DynamicImage> { &self.map.0[1] }
180
181    pub fn min_chunk_alt(&self) -> f32 { self.map.2.x }
182
183    pub fn max_chunk_alt(&self) -> f32 { self.map.2.y }
184}
185
186pub struct SiteMarker {
187    pub marker: Marker,
188    pub economy: Option<EconomyInfo>,
189}
190
191struct WeatherLerp {
192    old: (SharedWeatherGrid, Instant),
193    new: (SharedWeatherGrid, Instant),
194    old_local_wind: (Vec2<f32>, Instant),
195    new_local_wind: (Vec2<f32>, Instant),
196    local_wind: Vec2<f32>,
197}
198
199impl WeatherLerp {
200    fn local_wind_update(&mut self, wind: Vec2<f32>) {
201        self.old_local_wind = mem::replace(&mut self.new_local_wind, (wind, Instant::now()));
202    }
203
204    fn update_local_wind(&mut self) {
205        // Assumes updates are regular
206        let t = (self.new_local_wind.1.elapsed().as_secs_f32()
207            / self
208                .new_local_wind
209                .1
210                .duration_since(self.old_local_wind.1)
211                .as_secs_f32())
212        .clamp(0.0, 1.0);
213
214        self.local_wind = Vec2::lerp_unclamped(self.old_local_wind.0, self.new_local_wind.0, t);
215    }
216
217    fn weather_update(&mut self, weather: SharedWeatherGrid) {
218        self.old = mem::replace(&mut self.new, (weather, Instant::now()));
219    }
220
221    // TODO: Make improvements to this interpolation, it's main issue is assuming
222    // that updates come at regular intervals.
223    fn update(&mut self, to_update: &mut WeatherGrid) {
224        prof_span!("WeatherLerp::update");
225        self.update_local_wind();
226        let old = &self.old.0;
227        let new = &self.new.0;
228        if new.size() == Vec2::zero() {
229            return;
230        }
231        if to_update.size() != new.size() {
232            *to_update = WeatherGrid::from(new);
233        }
234        if old.size() == new.size() {
235            // Assumes updates are regular
236            let t = (self.new.1.elapsed().as_secs_f32()
237                / self.new.1.duration_since(self.old.1).as_secs_f32())
238            .clamp(0.0, 1.0);
239
240            to_update
241                .iter_mut()
242                .zip(old.iter().zip(new.iter()))
243                .for_each(|((_, current), ((_, old), (_, new)))| {
244                    *current = CompressedWeather::lerp_unclamped(old, new, t);
245                    // `local_wind` is set for all weather cells on the client,
246                    // which will still be inaccurate outside the "local" area
247                    current.wind = self.local_wind;
248                });
249        }
250    }
251}
252
253impl Default for WeatherLerp {
254    fn default() -> Self {
255        let old = Instant::now();
256        let new = Instant::now();
257        Self {
258            old: (SharedWeatherGrid::new(Vec2::zero()), old),
259            new: (SharedWeatherGrid::new(Vec2::zero()), new),
260            old_local_wind: (Vec2::zero(), old),
261            new_local_wind: (Vec2::zero(), new),
262            local_wind: Vec2::zero(),
263        }
264    }
265}
266
267pub struct Client {
268    client_type: ClientType,
269    registered: bool,
270    presence: Option<PresenceKind>,
271    runtime: Arc<Runtime>,
272    server_info: ServerInfo,
273    /// Localized server motd and rules
274    server_description: ServerDescription,
275    world_data: WorldData,
276    weather: WeatherLerp,
277    player_list: HashMap<Uid, PlayerInfo>,
278    character_list: CharacterList,
279    character_being_deleted: Option<CharacterId>,
280    sites: HashMap<SiteId, SiteMarker>,
281    possible_starting_sites: Vec<SiteId>,
282    pois: Vec<PoiInfo>,
283    pub chat_mode: ChatMode,
284    component_recipe_book: ComponentRecipeBook,
285    repair_recipe_book: RepairRecipeBook,
286    available_recipes: HashMap<String, Option<SpriteKind>>,
287    lod_zones: HashMap<Vec2<i32>, lod::Zone>,
288    lod_last_requested: Option<Instant>,
289    lod_pos_fallback: Option<Vec2<f32>>,
290    force_update_counter: u64,
291
292    role: Option<AdminRole>,
293    max_group_size: u32,
294    // Client has received an invite (inviter uid, time out instant)
295    invite: Option<(Uid, Instant, Duration, InviteKind)>,
296    group_leader: Option<Uid>,
297    // Note: potentially representable as a client only component
298    group_members: HashMap<Uid, group::Role>,
299    // Pending invites that this client has sent out
300    pending_invites: HashSet<Uid>,
301    // The pending trade the client is involved in, and it's id
302    pending_trade: Option<(TradeId, PendingTrade, Option<SitePrices>)>,
303
304    network: Option<Network>,
305    participant: Option<Participant>,
306    general_stream: Stream,
307    ping_stream: Stream,
308    register_stream: Stream,
309    character_screen_stream: Stream,
310    in_game_stream: Stream,
311    terrain_stream: Stream,
312
313    client_timeout: Duration,
314    last_server_ping: f64,
315    last_server_pong: f64,
316    last_ping_delta: f64,
317    ping_deltas: VecDeque<f64>,
318
319    tick: u64,
320    state: State,
321
322    flashing_lights_enabled: bool,
323
324    /// Terrrain view distance
325    server_view_distance_limit: Option<u32>,
326    view_distance: Option<u32>,
327    lod_distance: f32,
328    // TODO: move into voxygen
329    loaded_distance: f32,
330
331    pending_chunks: HashMap<Vec2<i32>, Instant>,
332    target_time_of_day: Option<TimeOfDay>,
333    dt_adjustment: f64,
334
335    connected_server_constants: ServerConstants,
336    /// Requested but not yet received plugins
337    missing_plugins: HashSet<PluginHash>,
338    /// Locally cached plugins needed by the server
339    local_plugins: Vec<PathBuf>,
340}
341
342/// Holds data related to the current players characters, as well as some
343/// additional state to handle UI.
344#[derive(Debug, Default)]
345pub struct CharacterList {
346    pub characters: Vec<CharacterItem>,
347    pub loading: bool,
348}
349
350async fn connect_quic(
351    network: &Network,
352    hostname: String,
353    override_port: Option<u16>,
354    prefer_ipv6: bool,
355    validate_tls: bool,
356) -> Result<network::Participant, crate::error::Error> {
357    let config = if validate_tls {
358        quinn::ClientConfig::with_platform_verifier()
359    } else {
360        warn!(
361            "skipping validation of server identity. There is no guarantee that the server you're \
362             connected to is the one you expect to be connecting to."
363        );
364        #[derive(Debug)]
365        struct Verifier;
366        impl rustls::client::danger::ServerCertVerifier for Verifier {
367            fn verify_server_cert(
368                &self,
369                _end_entity: &rustls::pki_types::CertificateDer<'_>,
370                _intermediates: &[rustls::pki_types::CertificateDer<'_>],
371                _server_name: &rustls::pki_types::ServerName<'_>,
372                _ocsp_response: &[u8],
373                _now: rustls::pki_types::UnixTime,
374            ) -> Result<ServerCertVerified, rustls::Error> {
375                Ok(ServerCertVerified::assertion())
376            }
377
378            fn verify_tls12_signature(
379                &self,
380                _message: &[u8],
381                _cert: &rustls::pki_types::CertificateDer<'_>,
382                _dss: &rustls::DigitallySignedStruct,
383            ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error>
384            {
385                Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
386            }
387
388            fn verify_tls13_signature(
389                &self,
390                _message: &[u8],
391                _cert: &rustls::pki_types::CertificateDer<'_>,
392                _dss: &rustls::DigitallySignedStruct,
393            ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error>
394            {
395                Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
396            }
397
398            fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
399                vec![
400                    rustls::SignatureScheme::RSA_PKCS1_SHA1,
401                    rustls::SignatureScheme::ECDSA_SHA1_Legacy,
402                    rustls::SignatureScheme::RSA_PKCS1_SHA256,
403                    rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
404                    rustls::SignatureScheme::RSA_PKCS1_SHA384,
405                    rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
406                    rustls::SignatureScheme::RSA_PKCS1_SHA512,
407                    rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
408                    rustls::SignatureScheme::RSA_PSS_SHA256,
409                    rustls::SignatureScheme::RSA_PSS_SHA384,
410                    rustls::SignatureScheme::RSA_PSS_SHA512,
411                    rustls::SignatureScheme::ED25519,
412                    rustls::SignatureScheme::ED448,
413                ]
414            }
415        }
416
417        let mut cfg = rustls::ClientConfig::builder()
418            .dangerous()
419            .with_custom_certificate_verifier(Arc::new(Verifier))
420            .with_no_client_auth();
421        cfg.enable_early_data = true;
422
423        quinn::ClientConfig::new(Arc::new(
424            quinn::crypto::rustls::QuicClientConfig::try_from(cfg).unwrap(),
425        ))
426    };
427
428    addr::try_connect(network, &hostname, override_port, prefer_ipv6, |a| {
429        ConnectAddr::Quic(a, config.clone(), hostname.clone())
430    })
431    .await
432}
433
434impl Client {
435    pub async fn new(
436        addr: ConnectionArgs,
437        runtime: Arc<Runtime>,
438        // TODO: refactor to avoid needing to use this out parameter
439        mismatched_server_info: &mut Option<ServerInfo>,
440        username: &str,
441        password: &str,
442        locale: Option<String>,
443        auth_trusted: impl FnMut(&str) -> bool,
444        init_stage_update: &(dyn Fn(ClientInitStage) + Send + Sync),
445        add_foreign_systems: impl Fn(&mut DispatcherBuilder) + Send + 'static,
446        #[cfg_attr(not(feature = "plugins"), expect(unused_variables))] config_dir: PathBuf,
447        client_type: ClientType,
448    ) -> Result<Self, Error> {
449        let _ = rustls::crypto::ring::default_provider().install_default(); // needs to be initialized before usage
450        let network = Network::new(Pid::new(), &runtime);
451
452        init_stage_update(ClientInitStage::ConnectionEstablish);
453
454        let mut participant = match addr {
455            ConnectionArgs::Srv {
456                hostname,
457                prefer_ipv6,
458                validate_tls,
459                use_quic,
460            } => {
461                // Try to create a resolver backed by /etc/resolv.conf or the Windows Registry
462                // first. If that fails, create a resolver being hard-coded to
463                // Google's 8.8.8.8 public resolver.
464                let resolver = AsyncResolver::tokio_from_system_conf().unwrap_or_else(|error| {
465                    error!("Failed to create DNS resolver using system configuration: {error:?}");
466                    warn!("Falling back to a default configured resolver.");
467                    AsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default())
468                });
469
470                let quic_service_host = format!("_veloren._udp.{hostname}");
471                let quic_lookup_future = resolver.srv_lookup(quic_service_host);
472                let tcp_service_host = format!("_veloren._tcp.{hostname}");
473                let tcp_lookup_future = resolver.srv_lookup(tcp_service_host);
474                let (quic_rr, tcp_rr) = tokio::join!(quic_lookup_future, tcp_lookup_future);
475
476                #[derive(Eq, PartialEq)]
477                enum ConnMode {
478                    Quic,
479                    Tcp,
480                }
481
482                // Push the results of both futures into `srv_rr`. This uses map_or_else purely
483                // for side effects.
484                let mut srv_rr = Vec::new();
485                let () = quic_rr.map_or_else(
486                    |error| {
487                        warn!("QUIC SRV lookup failed: {error:?}");
488                    },
489                    |srv_lookup| {
490                        srv_rr.extend(srv_lookup.iter().cloned().map(|srv| (ConnMode::Quic, srv)))
491                    },
492                );
493                let () = tcp_rr.map_or_else(
494                    |error| {
495                        warn!("TCP SRV lookup failed: {error:?}");
496                    },
497                    |srv_lookup| {
498                        srv_rr.extend(srv_lookup.iter().cloned().map(|srv| (ConnMode::Tcp, srv)))
499                    },
500                );
501
502                // SRV records have a priority; lowest priority hosts MUST be contacted first.
503                let srv_rr_slice = srv_rr.as_mut_slice();
504                srv_rr_slice.sort_by_key(|(_, srv)| srv.priority());
505
506                let mut iter = srv_rr_slice.iter();
507
508                // This loops exits as soon as the above iter over `srv_rr_slice` is exhausted
509                loop {
510                    if let Some((conn_mode, srv_rr)) = iter.next() {
511                        let hostname = format!("{}", srv_rr.target());
512                        let port = Some(srv_rr.port());
513                        let conn_result = match conn_mode {
514                            ConnMode::Quic => {
515                                connect_quic(&network, hostname, port, prefer_ipv6, validate_tls)
516                                    .await
517                            },
518                            ConnMode::Tcp => {
519                                addr::try_connect(
520                                    &network,
521                                    &hostname,
522                                    port,
523                                    prefer_ipv6,
524                                    ConnectAddr::Tcp,
525                                )
526                                .await
527                            },
528                        };
529                        match conn_result {
530                            Ok(c) => break c,
531                            Err(error) => {
532                                warn!("Failed to connect to host {}: {error:?}", srv_rr.target())
533                            },
534                        }
535                    } else {
536                        warn!(
537                            "No SRV hosts succeeded connection, falling back to direct connection"
538                        );
539                        // This case is also hit if no SRV host was returned from the query, so we
540                        // check for QUIC/TCP preference.
541                        let c = if use_quic {
542                            connect_quic(&network, hostname, None, prefer_ipv6, validate_tls)
543                                .await?
544                        } else {
545                            match addr::try_connect(
546                                &network,
547                                &hostname,
548                                None,
549                                prefer_ipv6,
550                                ConnectAddr::Tcp,
551                            )
552                            .await
553                            {
554                                Ok(c) => c,
555                                Err(error) => return Err(error),
556                            }
557                        };
558                        break c;
559                    }
560                }
561            },
562            ConnectionArgs::Tcp {
563                hostname,
564                prefer_ipv6,
565            } => {
566                addr::try_connect(&network, &hostname, None, prefer_ipv6, ConnectAddr::Tcp).await?
567            },
568            ConnectionArgs::Quic {
569                hostname,
570                prefer_ipv6,
571                validate_tls,
572            } => {
573                warn!(
574                    "QUIC is enabled. This is experimental and you won't be able to connect to \
575                     TCP servers unless deactivated"
576                );
577
578                connect_quic(&network, hostname, None, prefer_ipv6, validate_tls).await?
579            },
580            ConnectionArgs::Mpsc(id) => network.connect(ConnectAddr::Mpsc(id)).await?,
581        };
582
583        let stream = participant.opened().await?;
584        let ping_stream = participant.opened().await?;
585        let mut register_stream = participant.opened().await?;
586        let character_screen_stream = participant.opened().await?;
587        let in_game_stream = participant.opened().await?;
588        let terrain_stream = participant.opened().await?;
589
590        init_stage_update(ClientInitStage::WatingForServerVersion);
591        register_stream.send(client_type)?;
592        let server_info: ServerInfo = register_stream.recv().await?;
593        if server_info.git_hash != *common::util::GIT_HASH {
594            warn!(
595                "Server is running {}[{}], you are running {}[{}], versions might be incompatible!",
596                server_info.git_hash,
597                server_info.git_date,
598                common::util::GIT_HASH.to_string(),
599                *common::util::GIT_DATE,
600            );
601        }
602        // Pass the server info back to the caller to ensure they can access it even
603        // if this function errors.
604        mem::swap(mismatched_server_info, &mut Some(server_info.clone()));
605        debug!("Auth Server: {:?}", server_info.auth_provider);
606
607        ping_stream.send(PingMsg::Ping)?;
608
609        init_stage_update(ClientInitStage::Authentication);
610        // Register client
611        Self::register(
612            username,
613            password,
614            locale,
615            auth_trusted,
616            &server_info,
617            &mut register_stream,
618        )
619        .await?;
620
621        init_stage_update(ClientInitStage::LoadingInitData);
622        // Wait for initial sync
623        let mut ping_interval = tokio::time::interval(Duration::from_secs(1));
624        let ServerInit::GameSync {
625            entity_package,
626            time_of_day,
627            max_group_size,
628            client_timeout,
629            world_map,
630            recipe_book,
631            component_recipe_book,
632            material_stats,
633            ability_map,
634            server_constants,
635            repair_recipe_book,
636            description,
637            active_plugins: _active_plugins,
638            role,
639        } = loop {
640            tokio::select! {
641                // Spawn in a blocking thread (leaving the network thread free).  This is mostly
642                // useful for bots.
643                res = register_stream.recv() => break res?,
644                _ = ping_interval.tick() => ping_stream.send(PingMsg::Ping)?,
645            }
646        };
647
648        init_stage_update(ClientInitStage::StartingClient);
649        // Spawn in a blocking thread (leaving the network thread free).  This is mostly
650        // useful for bots.
651        let mut task = tokio::task::spawn_blocking(move || {
652            let map_size_lg =
653                common::terrain::MapSizeLg::new(world_map.dimensions_lg).map_err(|_| {
654                    Error::Other(format!(
655                        "Server sent bad world map dimensions: {:?}",
656                        world_map.dimensions_lg,
657                    ))
658                })?;
659            let sea_level = world_map.default_chunk.get_min_z() as f32;
660
661            // Initialize `State`
662            let pools = State::pools(GameMode::Client);
663            let mut state = State::client(
664                pools,
665                map_size_lg,
666                world_map.default_chunk,
667                // TODO: Add frontend systems
668                |dispatch_builder| {
669                    add_local_systems(dispatch_builder);
670                    add_foreign_systems(dispatch_builder);
671                },
672                #[cfg(feature = "plugins")]
673                common_state::plugin::PluginMgr::from_asset_or_default(),
674            );
675
676            #[cfg_attr(not(feature = "plugins"), expect(unused_mut))]
677            let mut missing_plugins: Vec<PluginHash> = Vec::new();
678            #[cfg_attr(not(feature = "plugins"), expect(unused_mut))]
679            let mut local_plugins: Vec<PathBuf> = Vec::new();
680            #[cfg(feature = "plugins")]
681            {
682                let already_present = state.ecs().read_resource::<PluginMgr>().plugin_list();
683                for hash in _active_plugins.iter() {
684                    if !already_present.contains(hash) {
685                        // look in config_dir first (cache)
686                        if let Ok(local_path) = common_state::plugin::find_cached(&config_dir, hash)
687                        {
688                            local_plugins.push(local_path);
689                        } else {
690                            //tracing::info!("cache not found {local_path:?}");
691                            tracing::info!("Server requires plugin {hash:x?}");
692                            missing_plugins.push(*hash);
693                        }
694                    }
695                }
696            }
697            // Client-only components
698            state.ecs_mut().register::<comp::Last<CharacterState>>();
699            let entity = state.ecs_mut().apply_entity_package(entity_package);
700            *state.ecs_mut().write_resource() = time_of_day;
701            *state.ecs_mut().write_resource() = PlayerEntity(Some(entity));
702            state.ecs_mut().insert(material_stats);
703            state.ecs_mut().insert(ability_map);
704            state.ecs_mut().insert(recipe_book);
705
706            let map_size = map_size_lg.chunks();
707            let max_height = world_map.max_height;
708            let rgba = world_map.rgba;
709            let alt = world_map.alt;
710            if rgba.size() != map_size.map(|e| e as i32) {
711                return Err(Error::Other("Server sent a bad world map image".into()));
712            }
713            if alt.size() != map_size.map(|e| e as i32) {
714                return Err(Error::Other("Server sent a bad altitude map.".into()));
715            }
716            let [west, east] = world_map.horizons;
717            let scale_angle = |a: u8| (a as f32 / 255.0 * <f32 as FloatConst>::FRAC_PI_2()).tan();
718            let scale_height = |h: u8| h as f32 / 255.0 * max_height;
719            let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height;
720
721            debug!("Preparing image...");
722            let unzip_horizons = |(angles, heights): &(Vec<_>, Vec<_>)| {
723                (
724                    angles.iter().copied().map(scale_angle).collect::<Vec<_>>(),
725                    heights
726                        .iter()
727                        .copied()
728                        .map(scale_height)
729                        .collect::<Vec<_>>(),
730                )
731            };
732            let horizons = [unzip_horizons(&west), unzip_horizons(&east)];
733
734            // Redraw map (with shadows this time).
735            let mut world_map_rgba = vec![0u32; rgba.size().product() as usize];
736            let mut world_map_topo = vec![0u32; rgba.size().product() as usize];
737            let mut map_config = common::terrain::map::MapConfig::orthographic(
738                map_size_lg,
739                core::ops::RangeInclusive::new(0.0, max_height),
740            );
741            map_config.horizons = Some(&horizons);
742            let rescale_height = |h: f32| h / max_height;
743            let bounds_check = |pos: Vec2<i32>| {
744                pos.reduce_partial_min() >= 0
745                    && pos.x < map_size.x as i32
746                    && pos.y < map_size.y as i32
747            };
748            fn sample_pos(
749                map_config: &MapConfig,
750                pos: Vec2<i32>,
751                alt: &Grid<u32>,
752                rgba: &Grid<u32>,
753                map_size: &Vec2<u16>,
754                map_size_lg: &common::terrain::MapSizeLg,
755                max_height: f32,
756            ) -> common::terrain::map::MapSample {
757                let rescale_height = |h: f32| h / max_height;
758                let scale_height_big = |h: u32| (h >> 3) as f32 / 8191.0 * max_height;
759                let bounds_check = |pos: Vec2<i32>| {
760                    pos.reduce_partial_min() >= 0
761                        && pos.x < map_size.x as i32
762                        && pos.y < map_size.y as i32
763                };
764                let MapConfig {
765                    gain,
766                    is_contours,
767                    is_height_map,
768                    is_stylized_topo,
769                    ..
770                } = *map_config;
771                let mut is_contour_line = false;
772                let mut is_border = false;
773                let (rgb, alt, downhill_wpos) = if bounds_check(pos) {
774                    let posi = pos.y as usize * map_size.x as usize + pos.x as usize;
775                    let [r, g, b, _a] = rgba[pos].to_le_bytes();
776                    let is_water = r == 0 && b > 102 && g < 77;
777                    let alti = alt[pos];
778                    // Compute contours (chunks are assigned in the river code below)
779                    let altj = rescale_height(scale_height_big(alti));
780                    let contour_interval = 150.0;
781                    let chunk_contour = (altj * gain / contour_interval) as u32;
782
783                    // Compute downhill.
784                    let downhill = {
785                        let mut best = -1;
786                        let mut besth = alti;
787                        for nposi in neighbors(*map_size_lg, posi) {
788                            let nbh = alt.raw()[nposi];
789                            let nalt = rescale_height(scale_height_big(nbh));
790                            let nchunk_contour = (nalt * gain / contour_interval) as u32;
791                            if !is_contour_line && chunk_contour > nchunk_contour {
792                                is_contour_line = true;
793                            }
794                            let [nr, ng, nb, _na] = rgba.raw()[nposi].to_le_bytes();
795                            let n_is_water = nr == 0 && nb > 102 && ng < 77;
796
797                            if !is_border && is_water && !n_is_water {
798                                is_border = true;
799                            }
800
801                            if nbh < besth {
802                                besth = nbh;
803                                best = nposi as isize;
804                            }
805                        }
806                        best
807                    };
808                    let downhill_wpos = if downhill < 0 {
809                        None
810                    } else {
811                        Some(
812                            Vec2::new(
813                                (downhill as usize % map_size.x as usize) as i32,
814                                (downhill as usize / map_size.x as usize) as i32,
815                            ) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
816                        )
817                    };
818                    (Rgb::new(r, g, b), alti, downhill_wpos)
819                } else {
820                    (Rgb::zero(), 0, None)
821                };
822                let alt = f64::from(rescale_height(scale_height_big(alt)));
823                let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
824                let downhill_wpos =
825                    downhill_wpos.unwrap_or(wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32));
826                let is_path = rgb.r == 0x37 && rgb.g == 0x29 && rgb.b == 0x23;
827                let rgb = rgb.map(|e: u8| e as f64 / 255.0);
828                let is_water = rgb.r == 0.0 && rgb.b > 0.4 && rgb.g < 0.3;
829
830                let rgb = if is_height_map {
831                    if is_path {
832                        // Path color is Rgb::new(0x37, 0x29, 0x23)
833                        Rgb::new(0.9, 0.9, 0.63)
834                    } else if is_water {
835                        Rgb::new(0.23, 0.47, 0.53)
836                    } else if is_contours && is_contour_line {
837                        // Color contour lines
838                        Rgb::new(0.15, 0.15, 0.15)
839                    } else {
840                        // Color hill shading
841                        let lightness = (alt + 0.2).min(1.0);
842                        Rgb::new(lightness, 0.9 * lightness, 0.5 * lightness)
843                    }
844                } else if is_stylized_topo {
845                    if is_path {
846                        Rgb::new(0.9, 0.9, 0.63)
847                    } else if is_water {
848                        if is_border {
849                            Rgb::new(0.10, 0.34, 0.50)
850                        } else {
851                            Rgb::new(0.23, 0.47, 0.63)
852                        }
853                    } else if is_contour_line {
854                        Rgb::new(0.25, 0.25, 0.25)
855                    } else {
856                        // Stylized colors
857                        Rgb::new(
858                            (rgb.r + 0.25).min(1.0),
859                            (rgb.g + 0.23).min(1.0),
860                            (rgb.b + 0.10).min(1.0),
861                        )
862                    }
863                } else {
864                    Rgb::new(rgb.r, rgb.g, rgb.b)
865                }
866                .map(|e| (e * 255.0) as u8);
867                common::terrain::map::MapSample {
868                    rgb,
869                    alt,
870                    downhill_wpos,
871                    connections: None,
872                }
873            }
874            // Generate standard shaded map
875            map_config.is_shaded = true;
876            map_config.generate(
877                |pos| {
878                    sample_pos(
879                        &map_config,
880                        pos,
881                        &alt,
882                        &rgba,
883                        &map_size,
884                        &map_size_lg,
885                        max_height,
886                    )
887                },
888                |wpos| {
889                    let pos = wpos.wpos_to_cpos();
890                    rescale_height(if bounds_check(pos) {
891                        scale_height_big(alt[pos])
892                    } else {
893                        0.0
894                    })
895                },
896                |pos, (r, g, b, a)| {
897                    world_map_rgba[pos.y * map_size.x as usize + pos.x] =
898                        u32::from_le_bytes([r, g, b, a]);
899                },
900            );
901            // Generate map with topographical lines and stylized colors
902            map_config.is_contours = true;
903            map_config.is_stylized_topo = true;
904            map_config.generate(
905                |pos| {
906                    sample_pos(
907                        &map_config,
908                        pos,
909                        &alt,
910                        &rgba,
911                        &map_size,
912                        &map_size_lg,
913                        max_height,
914                    )
915                },
916                |wpos| {
917                    let pos = wpos.wpos_to_cpos();
918                    rescale_height(if bounds_check(pos) {
919                        scale_height_big(alt[pos])
920                    } else {
921                        0.0
922                    })
923                },
924                |pos, (r, g, b, a)| {
925                    world_map_topo[pos.y * map_size.x as usize + pos.x] =
926                        u32::from_le_bytes([r, g, b, a]);
927                },
928            );
929            let make_raw = |rgb| -> Result<_, Error> {
930                let mut raw = vec![0u8; 4 * world_map_rgba.len()];
931                LittleEndian::write_u32_into(rgb, &mut raw);
932                Ok(Arc::new(
933                    DynamicImage::ImageRgba8({
934                        // Should not fail if the dimensions are correct.
935                        let map =
936                            image::ImageBuffer::from_raw(u32::from(map_size.x), u32::from(map_size.y), raw);
937                        map.ok_or_else(|| Error::Other("Server sent a bad world map image".into()))?
938                    })
939                    // Flip the image, since Voxygen uses an orientation where rotation from
940                    // positive x axis to positive y axis is counterclockwise around the z axis.
941                    .flipv(),
942                ))
943            };
944            let lod_base = rgba;
945            let lod_alt = alt;
946            let world_map_rgb_img = make_raw(&world_map_rgba)?;
947            let world_map_topo_img = make_raw(&world_map_topo)?;
948            let world_map_layers = vec![world_map_rgb_img, world_map_topo_img];
949            let horizons = (west.0, west.1, east.0, east.1)
950                .into_par_iter()
951                .map(|(wa, wh, ea, eh)| u32::from_le_bytes([wa, wh, ea, eh]))
952                .collect::<Vec<_>>();
953            let lod_horizon = horizons;
954            let map_bounds = Vec2::new(sea_level, max_height);
955            debug!("Done preparing image...");
956
957            Ok((
958                state,
959                lod_base,
960                lod_alt,
961                Grid::from_raw(map_size.map(|e| e as i32), lod_horizon),
962                (world_map_layers, map_size, map_bounds),
963                world_map.sites,
964                world_map.possible_starting_sites,
965                world_map.pois,
966                component_recipe_book,
967                repair_recipe_book,
968                max_group_size,
969                client_timeout,
970                missing_plugins,
971                local_plugins,
972                role,
973            ))
974        });
975
976        let (
977            state,
978            lod_base,
979            lod_alt,
980            lod_horizon,
981            world_map,
982            sites,
983            possible_starting_sites,
984            pois,
985            component_recipe_book,
986            repair_recipe_book,
987            max_group_size,
988            client_timeout,
989            missing_plugins,
990            local_plugins,
991            role,
992        ) = loop {
993            tokio::select! {
994                res = &mut task => break res.expect("Client thread should not panic")?,
995                _ = ping_interval.tick() => ping_stream.send(PingMsg::Ping)?,
996            }
997        };
998        let missing_plugins_set = missing_plugins.iter().cloned().collect();
999        if !missing_plugins.is_empty() {
1000            stream.send(ClientGeneral::RequestPlugins(missing_plugins))?;
1001        }
1002        ping_stream.send(PingMsg::Ping)?;
1003
1004        debug!("Initial sync done");
1005
1006        Ok(Self {
1007            client_type,
1008            registered: true,
1009            presence: None,
1010            runtime,
1011            server_info,
1012            server_description: description,
1013            world_data: WorldData {
1014                lod_base,
1015                lod_alt,
1016                lod_horizon,
1017                map: world_map,
1018            },
1019            weather: WeatherLerp::default(),
1020            player_list: HashMap::new(),
1021            character_list: CharacterList::default(),
1022            character_being_deleted: None,
1023            sites: sites
1024                .iter()
1025                .filter_map(|m| {
1026                    Some((m.id?, SiteMarker {
1027                        marker: m.clone(),
1028                        economy: None,
1029                    }))
1030                })
1031                .collect(),
1032            possible_starting_sites,
1033            pois,
1034            component_recipe_book,
1035            repair_recipe_book,
1036            available_recipes: HashMap::default(),
1037            chat_mode: ChatMode::default(),
1038
1039            lod_zones: HashMap::new(),
1040            lod_last_requested: None,
1041            lod_pos_fallback: None,
1042
1043            force_update_counter: 0,
1044
1045            role,
1046            max_group_size,
1047            invite: None,
1048            group_leader: None,
1049            group_members: HashMap::new(),
1050            pending_invites: HashSet::new(),
1051            pending_trade: None,
1052
1053            network: Some(network),
1054            participant: Some(participant),
1055            general_stream: stream,
1056            ping_stream,
1057            register_stream,
1058            character_screen_stream,
1059            in_game_stream,
1060            terrain_stream,
1061
1062            client_timeout,
1063
1064            last_server_ping: 0.0,
1065            last_server_pong: 0.0,
1066            last_ping_delta: 0.0,
1067            ping_deltas: VecDeque::new(),
1068
1069            tick: 0,
1070            state,
1071
1072            flashing_lights_enabled: true,
1073
1074            server_view_distance_limit: None,
1075            view_distance: None,
1076            lod_distance: 4.0,
1077            loaded_distance: 0.0,
1078
1079            pending_chunks: HashMap::new(),
1080            target_time_of_day: None,
1081            dt_adjustment: 1.0,
1082
1083            connected_server_constants: server_constants,
1084            missing_plugins: missing_plugins_set,
1085            local_plugins,
1086        })
1087    }
1088
1089    /// Request a state transition to `ClientState::Registered`.
1090    async fn register(
1091        username: &str,
1092        password: &str,
1093        locale: Option<String>,
1094        mut auth_trusted: impl FnMut(&str) -> bool,
1095        server_info: &ServerInfo,
1096        register_stream: &mut Stream,
1097    ) -> Result<(), Error> {
1098        // Authentication
1099        let token_or_username = match &server_info.auth_provider {
1100            Some(addr) => {
1101                // Query whether this is a trusted auth server
1102                if auth_trusted(addr) {
1103                    let (scheme, authority) = match addr.split_once("://") {
1104                        Some((s, a)) => (s, a),
1105                        None => return Err(Error::AuthServerUrlInvalid(addr.to_string())),
1106                    };
1107
1108                    let scheme = match scheme.parse::<authc::Scheme>() {
1109                        Ok(s) => s,
1110                        Err(_) => return Err(Error::AuthServerUrlInvalid(addr.to_string())),
1111                    };
1112
1113                    let authority = match authority.parse::<authc::Authority>() {
1114                        Ok(a) => a,
1115                        Err(_) => return Err(Error::AuthServerUrlInvalid(addr.to_string())),
1116                    };
1117
1118                    Ok(authc::AuthClient::new(scheme, authority)?
1119                        .sign_in(username, password)
1120                        .await?
1121                        .serialize())
1122                } else {
1123                    Err(Error::AuthServerNotTrusted)
1124                }
1125            },
1126            None => Ok(username.to_owned()),
1127        }?;
1128
1129        debug!("Registering client...");
1130
1131        register_stream.send(ClientRegister {
1132            token_or_username,
1133            locale,
1134        })?;
1135
1136        match register_stream.recv::<ServerRegisterAnswer>().await? {
1137            Err(RegisterError::AuthError(err)) => Err(Error::AuthErr(err)),
1138            Err(RegisterError::InvalidCharacter) => Err(Error::InvalidCharacter),
1139            Err(RegisterError::NotOnWhitelist) => Err(Error::NotOnWhitelist),
1140            Err(RegisterError::Kicked(err)) => Err(Error::Kicked(err)),
1141            Err(RegisterError::Banned(info)) => Err(Error::Banned(info)),
1142            Err(RegisterError::TooManyPlayers) => Err(Error::TooManyPlayers),
1143            Ok(()) => {
1144                debug!("Client registered successfully.");
1145                Ok(())
1146            },
1147        }
1148    }
1149
1150    fn send_msg_err<S>(&mut self, msg: S) -> Result<(), network::StreamError>
1151    where
1152        S: Into<ClientMsg>,
1153    {
1154        prof_span!("send_msg_err");
1155        let msg: ClientMsg = msg.into();
1156        #[cfg(debug_assertions)]
1157        {
1158            const C_TYPE: ClientType = ClientType::Game;
1159            let verified = msg.verify(C_TYPE, self.registered, self.presence);
1160
1161            // Due to the fact that character loading is performed asynchronously after
1162            // initial connect it is possible to receive messages after a character load
1163            // error while in the wrong state.
1164            if !verified {
1165                warn!(
1166                    "Received ClientType::Game message when not in game (Registered: {} Presence: \
1167                     {:?}), dropping message: {:?} ",
1168                    self.registered, self.presence, msg
1169                );
1170                return Ok(());
1171            }
1172        }
1173        match msg {
1174            ClientMsg::Type(msg) => self.register_stream.send(msg),
1175            ClientMsg::Register(msg) => self.register_stream.send(msg),
1176            ClientMsg::General(msg) => {
1177                #[cfg(feature = "tracy")]
1178                let (mut ingame, mut terrain) = (0.0, 0.0);
1179                let stream = match msg {
1180                    ClientGeneral::RequestCharacterList
1181                    | ClientGeneral::CreateCharacter { .. }
1182                    | ClientGeneral::EditCharacter { .. }
1183                    | ClientGeneral::DeleteCharacter(_)
1184                    | ClientGeneral::Character(_, _)
1185                    | ClientGeneral::Spectate(_) => &mut self.character_screen_stream,
1186                    // Only in game
1187                    ClientGeneral::ControllerInputs(_)
1188                    | ClientGeneral::ControlEvent(_)
1189                    | ClientGeneral::ControlAction(_)
1190                    | ClientGeneral::SetViewDistance(_)
1191                    | ClientGeneral::BreakBlock(_)
1192                    | ClientGeneral::PlaceBlock(_, _)
1193                    | ClientGeneral::ExitInGame
1194                    | ClientGeneral::PlayerPhysics { .. }
1195                    | ClientGeneral::UnlockSkill(_)
1196                    | ClientGeneral::RequestSiteInfo(_)
1197                    | ClientGeneral::RequestPlayerPhysics { .. }
1198                    | ClientGeneral::RequestLossyTerrainCompression { .. }
1199                    | ClientGeneral::UpdateMapMarker(_)
1200                    | ClientGeneral::SpectatePosition(_)
1201                    | ClientGeneral::SetBattleMode(_) => {
1202                        #[cfg(feature = "tracy")]
1203                        {
1204                            ingame = 1.0;
1205                        }
1206                        &mut self.in_game_stream
1207                    },
1208                    // Terrain
1209                    ClientGeneral::TerrainChunkRequest { .. }
1210                    | ClientGeneral::LodZoneRequest { .. } => {
1211                        #[cfg(feature = "tracy")]
1212                        {
1213                            terrain = 1.0;
1214                        }
1215                        &mut self.terrain_stream
1216                    },
1217                    // Always possible
1218                    ClientGeneral::ChatMsg(_)
1219                    | ClientGeneral::Command(_, _)
1220                    | ClientGeneral::Terminate
1221                    | ClientGeneral::RequestPlugins(_) => &mut self.general_stream,
1222                };
1223                #[cfg(feature = "tracy")]
1224                {
1225                    plot!("ingame_sends", ingame);
1226                    plot!("terrain_sends", terrain);
1227                }
1228                stream.send(msg)
1229            },
1230            ClientMsg::Ping(msg) => self.ping_stream.send(msg),
1231        }
1232    }
1233
1234    pub fn request_player_physics(&mut self, server_authoritative: bool) {
1235        self.send_msg(ClientGeneral::RequestPlayerPhysics {
1236            server_authoritative,
1237        })
1238    }
1239
1240    pub fn request_lossy_terrain_compression(&mut self, lossy_terrain_compression: bool) {
1241        self.send_msg(ClientGeneral::RequestLossyTerrainCompression {
1242            lossy_terrain_compression,
1243        })
1244    }
1245
1246    fn send_msg<S>(&mut self, msg: S)
1247    where
1248        S: Into<ClientMsg>,
1249    {
1250        let res = self.send_msg_err(msg);
1251        if let Err(e) = res {
1252            warn!(
1253                ?e,
1254                "connection to server no longer possible, couldn't send msg"
1255            );
1256        }
1257    }
1258
1259    /// Request a state transition to `ClientState::Character`.
1260    pub fn request_character(
1261        &mut self,
1262        character_id: CharacterId,
1263        view_distances: common::ViewDistances,
1264    ) {
1265        let view_distances = self.set_view_distances_local(view_distances);
1266        self.send_msg(ClientGeneral::Character(character_id, view_distances));
1267
1268        // Assume we are in_game unless server tells us otherwise
1269        self.presence = Some(PresenceKind::Character(character_id));
1270    }
1271
1272    /// Request a state transition to `ClientState::Spectate`.
1273    pub fn request_spectate(&mut self, view_distances: common::ViewDistances) {
1274        let view_distances = self.set_view_distances_local(view_distances);
1275        self.send_msg(ClientGeneral::Spectate(view_distances));
1276
1277        self.presence = Some(PresenceKind::Spectator);
1278    }
1279
1280    /// Load the current players character list
1281    pub fn load_character_list(&mut self) {
1282        self.character_list.loading = true;
1283        self.send_msg(ClientGeneral::RequestCharacterList);
1284    }
1285
1286    /// New character creation
1287    pub fn create_character(
1288        &mut self,
1289        alias: String,
1290        mainhand: Option<String>,
1291        offhand: Option<String>,
1292        body: comp::Body,
1293        hardcore: bool,
1294        start_site: Option<SiteId>,
1295    ) {
1296        self.character_list.loading = true;
1297        self.send_msg(ClientGeneral::CreateCharacter {
1298            alias,
1299            mainhand,
1300            offhand,
1301            body,
1302            hardcore,
1303            start_site,
1304        });
1305    }
1306
1307    pub fn edit_character(&mut self, alias: String, id: CharacterId, body: comp::Body) {
1308        self.character_list.loading = true;
1309        self.send_msg(ClientGeneral::EditCharacter { alias, id, body });
1310    }
1311
1312    /// Character deletion
1313    pub fn delete_character(&mut self, character_id: CharacterId) {
1314        // Pre-emptively remove the character to be deleted from the character list as
1315        // character deletes are processed asynchronously by the server so we can't rely
1316        // on a timely response to update the character list
1317        if let Some(pos) = self
1318            .character_list
1319            .characters
1320            .iter()
1321            .position(|x| x.character.id == Some(character_id))
1322        {
1323            self.character_list.characters.remove(pos);
1324        }
1325        self.send_msg(ClientGeneral::DeleteCharacter(character_id));
1326    }
1327
1328    /// Send disconnect message to the server
1329    pub fn logout(&mut self) {
1330        debug!("Sending logout from server");
1331        self.send_msg(ClientGeneral::Terminate);
1332        self.registered = false;
1333        self.presence = None;
1334    }
1335
1336    /// Request a state transition to `ClientState::Registered` from an ingame
1337    /// state.
1338    pub fn request_remove_character(&mut self) {
1339        self.chat_mode = ChatMode::World;
1340        self.send_msg(ClientGeneral::ExitInGame);
1341    }
1342
1343    pub fn set_view_distances(&mut self, view_distances: common::ViewDistances) {
1344        let view_distances = self.set_view_distances_local(view_distances);
1345        self.send_msg(ClientGeneral::SetViewDistance(view_distances));
1346    }
1347
1348    /// Clamps provided view distances, locally sets the terrain view distance
1349    /// in the client's properties and returns the clamped values for the
1350    /// caller to send to the server.
1351    fn set_view_distances_local(
1352        &mut self,
1353        view_distances: common::ViewDistances,
1354    ) -> common::ViewDistances {
1355        let view_distances = common::ViewDistances {
1356            terrain: view_distances
1357                .terrain
1358                .clamp(1, MAX_SELECTABLE_VIEW_DISTANCE),
1359            entity: view_distances.entity.max(1),
1360        };
1361        self.view_distance = Some(view_distances.terrain);
1362        view_distances
1363    }
1364
1365    pub fn set_lod_distance(&mut self, lod_distance: u32) {
1366        let lod_distance = lod_distance.clamp(0, 1000) as f32 / lod::ZONE_SIZE as f32;
1367        self.lod_distance = lod_distance;
1368    }
1369
1370    pub fn set_flashing_lights_enabled(&mut self, flashing_lights_enabled: bool) {
1371        self.flashing_lights_enabled = flashing_lights_enabled;
1372    }
1373
1374    pub fn use_slot(&mut self, slot: Slot) {
1375        self.control_action(ControlAction::InventoryAction(InventoryAction::Use(slot)))
1376    }
1377
1378    pub fn swap_slots(&mut self, a: Slot, b: Slot) {
1379        match (a, b) {
1380            (Slot::Overflow(o), Slot::Inventory(inv))
1381            | (Slot::Inventory(inv), Slot::Overflow(o)) => {
1382                self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1383                    InventoryEvent::OverflowMove(o, inv),
1384                )));
1385            },
1386            (Slot::Overflow(_), _) | (_, Slot::Overflow(_)) => {},
1387            (Slot::Equip(equip), slot) | (slot, Slot::Equip(equip)) => self.control_action(
1388                ControlAction::InventoryAction(InventoryAction::Swap(equip, slot)),
1389            ),
1390            (Slot::Inventory(inv1), Slot::Inventory(inv2)) => {
1391                self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1392                    InventoryEvent::Swap(inv1, inv2),
1393                )))
1394            },
1395        }
1396    }
1397
1398    pub fn drop_slot(&mut self, slot: Slot) {
1399        match slot {
1400            Slot::Equip(equip) => {
1401                self.control_action(ControlAction::InventoryAction(InventoryAction::Drop(equip)))
1402            },
1403            Slot::Inventory(inv) => self.send_msg(ClientGeneral::ControlEvent(
1404                ControlEvent::InventoryEvent(InventoryEvent::Drop(inv)),
1405            )),
1406            Slot::Overflow(o) => self.send_msg(ClientGeneral::ControlEvent(
1407                ControlEvent::InventoryEvent(InventoryEvent::OverflowDrop(o)),
1408            )),
1409        }
1410    }
1411
1412    pub fn sort_inventory(&mut self) {
1413        self.control_action(ControlAction::InventoryAction(InventoryAction::Sort));
1414    }
1415
1416    pub fn perform_trade_action(&mut self, action: TradeAction) {
1417        if let Some((id, _, _)) = self.pending_trade {
1418            if let TradeAction::Decline = action {
1419                self.pending_trade.take();
1420            }
1421            self.send_msg(ClientGeneral::ControlEvent(
1422                ControlEvent::PerformTradeAction(id, action),
1423            ));
1424        }
1425    }
1426
1427    pub fn is_dead(&self) -> bool { self.current::<comp::Health>().is_some_and(|h| h.is_dead) }
1428
1429    pub fn is_gliding(&self) -> bool {
1430        self.current::<CharacterState>()
1431            .is_some_and(|cs| matches!(cs, CharacterState::Glide(_)))
1432    }
1433
1434    pub fn split_swap_slots(&mut self, a: Slot, b: Slot) {
1435        match (a, b) {
1436            (Slot::Overflow(_), _) | (_, Slot::Overflow(_)) => {},
1437            (Slot::Equip(equip), slot) | (slot, Slot::Equip(equip)) => self.control_action(
1438                ControlAction::InventoryAction(InventoryAction::Swap(equip, slot)),
1439            ),
1440            (Slot::Inventory(inv1), Slot::Inventory(inv2)) => {
1441                self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1442                    InventoryEvent::SplitSwap(inv1, inv2),
1443                )))
1444            },
1445        }
1446    }
1447
1448    pub fn split_drop_slot(&mut self, slot: Slot) {
1449        match slot {
1450            Slot::Equip(equip) => {
1451                self.control_action(ControlAction::InventoryAction(InventoryAction::Drop(equip)))
1452            },
1453            Slot::Inventory(inv) => self.send_msg(ClientGeneral::ControlEvent(
1454                ControlEvent::InventoryEvent(InventoryEvent::SplitDrop(inv)),
1455            )),
1456            Slot::Overflow(o) => self.send_msg(ClientGeneral::ControlEvent(
1457                ControlEvent::InventoryEvent(InventoryEvent::OverflowSplitDrop(o)),
1458            )),
1459        }
1460    }
1461
1462    pub fn pick_up(&mut self, entity: EcsEntity) {
1463        // Get the health component from the entity
1464
1465        if let Some(uid) = self.state.read_component_copied(entity) {
1466            // If we're dead, exit before sending the message
1467            if self.is_dead() {
1468                return;
1469            }
1470
1471            self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1472                InventoryEvent::Pickup(uid),
1473            )));
1474        }
1475    }
1476
1477    pub fn do_pet(&mut self, target_entity: EcsEntity) {
1478        if self.is_dead() {
1479            return;
1480        }
1481
1482        if let Some(target_uid) = self.state.read_component_copied(target_entity) {
1483            self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InteractWith {
1484                target: target_uid,
1485                kind: common::interaction::InteractionKind::Pet,
1486            }))
1487        }
1488    }
1489
1490    pub fn npc_interact(&mut self, npc_entity: EcsEntity) {
1491        // If we're dead, exit before sending message
1492        if self.is_dead() {
1493            return;
1494        }
1495
1496        if let Some(uid) = self.state.read_component_copied(npc_entity) {
1497            self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Interact(uid)));
1498        }
1499    }
1500
1501    pub fn player_list(&self) -> &HashMap<Uid, PlayerInfo> { &self.player_list }
1502
1503    pub fn character_list(&self) -> &CharacterList { &self.character_list }
1504
1505    pub fn server_info(&self) -> &ServerInfo { &self.server_info }
1506
1507    pub fn server_description(&self) -> &ServerDescription { &self.server_description }
1508
1509    pub fn world_data(&self) -> &WorldData { &self.world_data }
1510
1511    pub fn component_recipe_book(&self) -> &ComponentRecipeBook { &self.component_recipe_book }
1512
1513    pub fn repair_recipe_book(&self) -> &RepairRecipeBook { &self.repair_recipe_book }
1514
1515    pub fn client_type(&self) -> &ClientType { &self.client_type }
1516
1517    pub fn available_recipes(&self) -> &HashMap<String, Option<SpriteKind>> {
1518        &self.available_recipes
1519    }
1520
1521    pub fn lod_zones(&self) -> &HashMap<Vec2<i32>, lod::Zone> { &self.lod_zones }
1522
1523    /// Set the fallback position used for loading LoD zones when the client
1524    /// entity does not have a position.
1525    pub fn set_lod_pos_fallback(&mut self, pos: Vec2<f32>) { self.lod_pos_fallback = Some(pos); }
1526
1527    pub fn craft_recipe(
1528        &mut self,
1529        recipe: &str,
1530        slots: Vec<(u32, InvSlotId)>,
1531        craft_sprite: Option<(VolumePos, SpriteKind)>,
1532        amount: u32,
1533    ) -> bool {
1534        let (can_craft, has_sprite) = if let Some(inventory) = self
1535            .state
1536            .ecs()
1537            .read_storage::<comp::Inventory>()
1538            .get(self.entity())
1539        {
1540            let rbm = self.state.ecs().read_resource::<RecipeBookManifest>();
1541            let (can_craft, required_sprite) = inventory.can_craft_recipe(recipe, 1, &rbm);
1542            let has_sprite =
1543                required_sprite.is_none_or(|s| Some(s) == craft_sprite.map(|(_, s)| s));
1544            (can_craft, has_sprite)
1545        } else {
1546            (false, false)
1547        };
1548        if can_craft && has_sprite {
1549            self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1550                InventoryEvent::CraftRecipe {
1551                    craft_event: CraftEvent::Simple {
1552                        recipe: recipe.to_string(),
1553                        slots,
1554                        amount,
1555                    },
1556                    craft_sprite: craft_sprite.map(|(pos, _)| pos),
1557                },
1558            )));
1559            true
1560        } else {
1561            false
1562        }
1563    }
1564
1565    /// Checks if the item in the given slot can be salvaged.
1566    pub fn can_salvage_item(&self, slot: InvSlotId) -> bool {
1567        self.inventories()
1568            .get(self.entity())
1569            .and_then(|inv| inv.get(slot))
1570            .is_some_and(|item| item.is_salvageable())
1571    }
1572
1573    /// Salvage the item in the given inventory slot. `salvage_pos` should be
1574    /// the location of a relevant crafting station within range of the player.
1575    pub fn salvage_item(&mut self, slot: InvSlotId, salvage_pos: VolumePos) -> bool {
1576        let is_salvageable = self.can_salvage_item(slot);
1577        if is_salvageable {
1578            self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1579                InventoryEvent::CraftRecipe {
1580                    craft_event: CraftEvent::Salvage(slot),
1581                    craft_sprite: Some(salvage_pos),
1582                },
1583            )));
1584        }
1585        is_salvageable
1586    }
1587
1588    /// Crafts modular weapon from components in the provided slots.
1589    /// `sprite_pos` should be the location of the necessary crafting station in
1590    /// range of the player.
1591    /// Returns whether or not the networking event was sent (which is based on
1592    /// whether the player has two modular components in the provided slots)
1593    pub fn craft_modular_weapon(
1594        &mut self,
1595        primary_component: InvSlotId,
1596        secondary_component: InvSlotId,
1597        sprite_pos: Option<VolumePos>,
1598    ) -> bool {
1599        let inventories = self.inventories();
1600        let inventory = inventories.get(self.entity());
1601
1602        enum ModKind {
1603            Primary,
1604            Secondary,
1605        }
1606
1607        // Closure to get inner modular component info from item in a given slot
1608        let mod_kind = |slot| match inventory
1609            .and_then(|inv| inv.get(slot).map(|item| item.kind()))
1610            .as_deref()
1611        {
1612            Some(ItemKind::ModularComponent(modular::ModularComponent::ToolPrimaryComponent {
1613                ..
1614            })) => Some(ModKind::Primary),
1615            Some(ItemKind::ModularComponent(
1616                modular::ModularComponent::ToolSecondaryComponent { .. },
1617            )) => Some(ModKind::Secondary),
1618            _ => None,
1619        };
1620
1621        if let (Some(ModKind::Primary), Some(ModKind::Secondary)) =
1622            (mod_kind(primary_component), mod_kind(secondary_component))
1623        {
1624            drop(inventories);
1625            self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1626                InventoryEvent::CraftRecipe {
1627                    craft_event: CraftEvent::ModularWeapon {
1628                        primary_component,
1629                        secondary_component,
1630                    },
1631                    craft_sprite: sprite_pos,
1632                },
1633            )));
1634            true
1635        } else {
1636            false
1637        }
1638    }
1639
1640    pub fn craft_modular_weapon_component(
1641        &mut self,
1642        toolkind: tool::ToolKind,
1643        material: InvSlotId,
1644        modifier: Option<InvSlotId>,
1645        slots: Vec<(u32, InvSlotId)>,
1646        sprite_pos: Option<VolumePos>,
1647    ) {
1648        self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1649            InventoryEvent::CraftRecipe {
1650                craft_event: CraftEvent::ModularWeaponPrimaryComponent {
1651                    toolkind,
1652                    material,
1653                    modifier,
1654                    slots,
1655                },
1656                craft_sprite: sprite_pos,
1657            },
1658        )));
1659    }
1660
1661    /// Repairs the item in the given inventory slot. `sprite_pos` should be
1662    /// the location of a relevant crafting station within range of the player.
1663    pub fn repair_item(
1664        &mut self,
1665        item: Slot,
1666        slots: Vec<(u32, InvSlotId)>,
1667        sprite_pos: VolumePos,
1668    ) -> bool {
1669        let is_repairable = {
1670            let inventories = self.inventories();
1671            let inventory = inventories.get(self.entity());
1672            inventory.is_some_and(|inv| {
1673                if let Some(item) = match item {
1674                    Slot::Equip(equip_slot) => inv.equipped(equip_slot),
1675                    Slot::Inventory(invslot) => inv.get(invslot),
1676                    Slot::Overflow(_) => None,
1677                } {
1678                    item.has_durability()
1679                } else {
1680                    false
1681                }
1682            })
1683        };
1684        if is_repairable {
1685            self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
1686                InventoryEvent::CraftRecipe {
1687                    craft_event: CraftEvent::Repair { item, slots },
1688                    craft_sprite: Some(sprite_pos),
1689                },
1690            )));
1691        }
1692        is_repairable
1693    }
1694
1695    fn update_available_recipes(&mut self) {
1696        let rbm = self.state.ecs().read_resource::<RecipeBookManifest>();
1697        let inventories = self.state.ecs().read_storage::<comp::Inventory>();
1698        if let Some(inventory) = inventories.get(self.entity()) {
1699            self.available_recipes = inventory
1700                .recipes_iter()
1701                .cloned()
1702                .filter_map(|name| {
1703                    let (can_craft, required_sprite) = inventory.can_craft_recipe(&name, 1, &rbm);
1704                    if can_craft {
1705                        Some((name, required_sprite))
1706                    } else {
1707                        None
1708                    }
1709                })
1710                .collect();
1711        }
1712    }
1713
1714    /// Unstable, likely to be removed in a future release
1715    pub fn sites(&self) -> &HashMap<SiteId, SiteMarker> { &self.sites }
1716
1717    pub fn markers(&self) -> impl ExactSizeIterator<Item = &Marker> {
1718        self.sites.values().map(|s| &s.marker)
1719    }
1720
1721    pub fn possible_starting_sites(&self) -> &[SiteId] { &self.possible_starting_sites }
1722
1723    /// Unstable, likely to be removed in a future release
1724    pub fn pois(&self) -> &Vec<PoiInfo> { &self.pois }
1725
1726    pub fn enable_lantern(&mut self) {
1727        self.send_msg(ClientGeneral::ControlEvent(ControlEvent::EnableLantern));
1728    }
1729
1730    pub fn disable_lantern(&mut self) {
1731        self.send_msg(ClientGeneral::ControlEvent(ControlEvent::DisableLantern));
1732    }
1733
1734    pub fn toggle_sprite_light(&mut self, pos: VolumePos, enable: bool) {
1735        self.control_action(ControlAction::InventoryAction(
1736            InventoryAction::ToggleSpriteLight(pos, enable),
1737        ));
1738    }
1739
1740    pub fn help_downed(&mut self, target_entity: EcsEntity) {
1741        if self.is_dead() {
1742            return;
1743        }
1744
1745        if let Some(target_uid) = self.state.read_component_copied(target_entity) {
1746            self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InteractWith {
1747                target: target_uid,
1748                kind: common::interaction::InteractionKind::HelpDowned,
1749            }))
1750        }
1751    }
1752
1753    pub fn remove_buff(&mut self, buff_id: BuffKind) {
1754        self.send_msg(ClientGeneral::ControlEvent(ControlEvent::RemoveBuff(
1755            buff_id,
1756        )));
1757    }
1758
1759    pub fn leave_stance(&mut self) {
1760        self.send_msg(ClientGeneral::ControlEvent(ControlEvent::LeaveStance));
1761    }
1762
1763    pub fn unlock_skill(&mut self, skill: Skill) {
1764        self.send_msg(ClientGeneral::UnlockSkill(skill));
1765    }
1766
1767    pub fn max_group_size(&self) -> u32 { self.max_group_size }
1768
1769    pub fn invite(&self) -> Option<(Uid, Instant, Duration, InviteKind)> { self.invite }
1770
1771    pub fn group_info(&self) -> Option<(String, Uid)> {
1772        self.group_leader.map(|l| ("Group".into(), l)) // TODO
1773    }
1774
1775    pub fn group_members(&self) -> &HashMap<Uid, group::Role> { &self.group_members }
1776
1777    pub fn pending_invites(&self) -> &HashSet<Uid> { &self.pending_invites }
1778
1779    pub fn pending_trade(&self) -> &Option<(TradeId, PendingTrade, Option<SitePrices>)> {
1780        &self.pending_trade
1781    }
1782
1783    pub fn is_trading(&self) -> bool { self.pending_trade.is_some() }
1784
1785    pub fn send_invite(&mut self, invitee: Uid, kind: InviteKind) {
1786        self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InitiateInvite(
1787            invitee, kind,
1788        )))
1789    }
1790
1791    pub fn accept_invite(&mut self) {
1792        // Clear invite
1793        self.invite.take();
1794        self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InviteResponse(
1795            InviteResponse::Accept,
1796        )));
1797    }
1798
1799    pub fn decline_invite(&mut self) {
1800        // Clear invite
1801        self.invite.take();
1802        self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InviteResponse(
1803            InviteResponse::Decline,
1804        )));
1805    }
1806
1807    pub fn leave_group(&mut self) {
1808        self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
1809            GroupManip::Leave,
1810        )));
1811    }
1812
1813    pub fn kick_from_group(&mut self, uid: Uid) {
1814        self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
1815            GroupManip::Kick(uid),
1816        )));
1817    }
1818
1819    pub fn assign_group_leader(&mut self, uid: Uid) {
1820        self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
1821            GroupManip::AssignLeader(uid),
1822        )));
1823    }
1824
1825    pub fn is_riding(&self) -> bool {
1826        self.state
1827            .ecs()
1828            .read_storage::<Is<Rider>>()
1829            .get(self.entity())
1830            .is_some()
1831            || self
1832                .state
1833                .ecs()
1834                .read_storage::<Is<VolumeRider>>()
1835                .get(self.entity())
1836                .is_some()
1837    }
1838
1839    pub fn is_lantern_enabled(&self) -> bool {
1840        self.state
1841            .ecs()
1842            .read_storage::<comp::LightEmitter>()
1843            .get(self.entity())
1844            .is_some()
1845    }
1846
1847    pub fn mount(&mut self, entity: EcsEntity) {
1848        if let Some(uid) = self.state.read_component_copied(entity) {
1849            self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Mount(uid)));
1850        }
1851    }
1852
1853    /// Mount a block at a `VolumePos`.
1854    pub fn mount_volume(&mut self, volume_pos: VolumePos) {
1855        self.send_msg(ClientGeneral::ControlEvent(ControlEvent::MountVolume(
1856            volume_pos,
1857        )));
1858    }
1859
1860    pub fn unmount(&mut self) { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Unmount)); }
1861
1862    pub fn set_pet_stay(&mut self, entity: EcsEntity, stay: bool) {
1863        if let Some(uid) = self.state.read_component_copied(entity) {
1864            self.send_msg(ClientGeneral::ControlEvent(ControlEvent::SetPetStay(
1865                uid, stay,
1866            )));
1867        }
1868    }
1869
1870    pub fn give_up(&mut self) {
1871        if comp::is_downed(self.current().as_ref(), self.current().as_ref()) {
1872            self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GiveUp));
1873        }
1874    }
1875
1876    pub fn respawn(&mut self) {
1877        if self.current::<comp::Health>().is_some_and(|h| h.is_dead) {
1878            // Hardcore characters cannot respawn, kick them to character selection
1879            if self.current::<Hardcore>().is_some() {
1880                self.request_remove_character();
1881            } else {
1882                self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Respawn));
1883            }
1884        }
1885    }
1886
1887    pub fn map_marker_event(&mut self, event: MapMarkerChange) {
1888        self.send_msg(ClientGeneral::UpdateMapMarker(event));
1889    }
1890
1891    /// Set the current position to spectate, returns true if the client's
1892    /// player has a Pos component to write to.
1893    pub fn spectate_position(&mut self, pos: Vec3<f32>) -> bool {
1894        let write = if let Some(position) = self
1895            .state
1896            .ecs()
1897            .write_storage::<comp::Pos>()
1898            .get_mut(self.entity())
1899        {
1900            position.0 = pos;
1901            true
1902        } else {
1903            false
1904        };
1905        if write {
1906            self.send_msg(ClientGeneral::SpectatePosition(pos));
1907        }
1908        write
1909    }
1910
1911    /// Checks whether a player can swap their weapon+ability `Loadout` settings
1912    /// and sends the `ControlAction` event that signals to do the swap.
1913    pub fn swap_loadout(&mut self) { self.control_action(ControlAction::SwapEquippedWeapons) }
1914
1915    /// Determine whether the player is wielding, if they're even capable of
1916    /// being in a wield state.
1917    pub fn is_wielding(&self) -> Option<bool> {
1918        self.state
1919            .ecs()
1920            .read_storage::<CharacterState>()
1921            .get(self.entity())
1922            .map(|cs| cs.is_wield())
1923    }
1924
1925    pub fn toggle_wield(&mut self) {
1926        match self.is_wielding() {
1927            Some(true) => self.control_action(ControlAction::Unwield),
1928            Some(false) => self.control_action(ControlAction::Wield),
1929            None => warn!("Can't toggle wield, client entity doesn't have a `CharacterState`"),
1930        }
1931    }
1932
1933    pub fn toggle_sit(&mut self) {
1934        let is_sitting = self
1935            .state
1936            .ecs()
1937            .read_storage::<CharacterState>()
1938            .get(self.entity())
1939            .map(|cs| matches!(cs, CharacterState::Sit));
1940
1941        match is_sitting {
1942            Some(true) => self.control_action(ControlAction::Stand),
1943            Some(false) => self.control_action(ControlAction::Sit),
1944            None => warn!("Can't toggle sit, client entity doesn't have a `CharacterState`"),
1945        }
1946    }
1947
1948    pub fn toggle_crawl(&mut self) {
1949        let is_crawling = self
1950            .state
1951            .ecs()
1952            .read_storage::<CharacterState>()
1953            .get(self.entity())
1954            .map(|cs| matches!(cs, CharacterState::Crawl));
1955
1956        match is_crawling {
1957            Some(true) => self.control_action(ControlAction::Stand),
1958            Some(false) => self.control_action(ControlAction::Crawl),
1959            None => warn!("Can't toggle crawl, client entity doesn't have a `CharacterState`"),
1960        }
1961    }
1962
1963    pub fn toggle_dance(&mut self) {
1964        let is_dancing = self
1965            .state
1966            .ecs()
1967            .read_storage::<CharacterState>()
1968            .get(self.entity())
1969            .map(|cs| matches!(cs, CharacterState::Dance));
1970
1971        match is_dancing {
1972            Some(true) => self.control_action(ControlAction::Stand),
1973            Some(false) => self.control_action(ControlAction::Dance),
1974            None => warn!("Can't toggle dance, client entity doesn't have a `CharacterState`"),
1975        }
1976    }
1977
1978    pub fn utter(&mut self, kind: UtteranceKind) {
1979        self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Utterance(kind)));
1980    }
1981
1982    pub fn toggle_sneak(&mut self) {
1983        let is_sneaking = self
1984            .state
1985            .ecs()
1986            .read_storage::<CharacterState>()
1987            .get(self.entity())
1988            .map(CharacterState::is_stealthy);
1989
1990        match is_sneaking {
1991            Some(true) => self.control_action(ControlAction::Stand),
1992            Some(false) => self.control_action(ControlAction::Sneak),
1993            None => warn!("Can't toggle sneak, client entity doesn't have a `CharacterState`"),
1994        }
1995    }
1996
1997    pub fn toggle_glide(&mut self) {
1998        let using_glider = self
1999            .state
2000            .ecs()
2001            .read_storage::<CharacterState>()
2002            .get(self.entity())
2003            .map(|cs| matches!(cs, CharacterState::GlideWield(_) | CharacterState::Glide(_)));
2004
2005        match using_glider {
2006            Some(true) => self.control_action(ControlAction::Unwield),
2007            Some(false) => self.control_action(ControlAction::GlideWield),
2008            None => warn!("Can't toggle glide, client entity doesn't have a `CharacterState`"),
2009        }
2010    }
2011
2012    pub fn cancel_climb(&mut self) {
2013        let is_climbing = self
2014            .state
2015            .ecs()
2016            .read_storage::<CharacterState>()
2017            .get(self.entity())
2018            .map(|cs| matches!(cs, CharacterState::Climb(_)));
2019
2020        match is_climbing {
2021            Some(true) => self.control_action(ControlAction::Stand),
2022            Some(false) => {},
2023            None => warn!("Can't stop climbing, client entity doesn't have a `CharacterState`"),
2024        }
2025    }
2026
2027    pub fn handle_input(
2028        &mut self,
2029        input: InputKind,
2030        pressed: bool,
2031        select_pos: Option<Vec3<f32>>,
2032        target_entity: Option<EcsEntity>,
2033    ) {
2034        if pressed {
2035            self.control_action(ControlAction::StartInput {
2036                input,
2037                target_entity: target_entity.and_then(|e| self.state.read_component_copied(e)),
2038                select_pos,
2039            });
2040        } else {
2041            self.control_action(ControlAction::CancelInput(input));
2042        }
2043    }
2044
2045    pub fn activate_portal(&mut self, portal: EcsEntity) {
2046        if let Some(portal_uid) = self.state.read_component_copied(portal) {
2047            self.send_msg(ClientGeneral::ControlEvent(ControlEvent::ActivatePortal(
2048                portal_uid,
2049            )));
2050        }
2051    }
2052
2053    fn control_action(&mut self, control_action: ControlAction) {
2054        if let Some(controller) = self
2055            .state
2056            .ecs()
2057            .write_storage::<Controller>()
2058            .get_mut(self.entity())
2059        {
2060            controller.push_action(control_action);
2061        }
2062        self.send_msg(ClientGeneral::ControlAction(control_action));
2063    }
2064
2065    fn control_event(&mut self, control_event: ControlEvent) {
2066        if let Some(controller) = self
2067            .state
2068            .ecs()
2069            .write_storage::<Controller>()
2070            .get_mut(self.entity())
2071        {
2072            controller.push_event(control_event.clone());
2073        }
2074        self.send_msg(ClientGeneral::ControlEvent(control_event));
2075    }
2076
2077    pub fn view_distance(&self) -> Option<u32> { self.view_distance }
2078
2079    pub fn server_view_distance_limit(&self) -> Option<u32> { self.server_view_distance_limit }
2080
2081    pub fn loaded_distance(&self) -> f32 { self.loaded_distance }
2082
2083    pub fn position(&self) -> Option<Vec3<f32>> {
2084        self.state
2085            .read_storage::<comp::Pos>()
2086            .get(self.entity())
2087            .map(|v| v.0)
2088    }
2089
2090    /// Returns Weather::default if no player position exists.
2091    pub fn weather_at_player(&self) -> Weather {
2092        self.position()
2093            .map(|p| {
2094                let mut weather = self.state.weather_at(p.xy());
2095                weather.wind = self.weather.local_wind;
2096                weather
2097            })
2098            .unwrap_or_default()
2099    }
2100
2101    pub fn current_chunk(&self) -> Option<Arc<TerrainChunk>> {
2102        let chunk_pos = Vec2::from(self.position()?)
2103            .map2(TerrainChunkSize::RECT_SIZE, |e: f32, sz| {
2104                (e as u32).div_euclid(sz) as i32
2105            });
2106
2107        self.state.terrain().get_key_arc(chunk_pos).cloned()
2108    }
2109
2110    pub fn current<C>(&self) -> Option<C>
2111    where
2112        C: Component + Clone,
2113    {
2114        self.state.read_storage::<C>().get(self.entity()).cloned()
2115    }
2116
2117    pub fn current_biome(&self) -> BiomeKind {
2118        match self.current_chunk() {
2119            Some(chunk) => chunk.meta().biome(),
2120            _ => BiomeKind::Void,
2121        }
2122    }
2123
2124    pub fn current_site(&self) -> SiteKindMeta {
2125        let mut player_alt = 0.0;
2126        if let Some(position) = self.current::<comp::Pos>() {
2127            player_alt = position.0.z;
2128        }
2129        let mut terrain_alt = 0.0;
2130        let mut site = None;
2131        if let Some(chunk) = self.current_chunk() {
2132            terrain_alt = chunk.meta().alt();
2133            site = chunk.meta().site();
2134        }
2135        if player_alt < terrain_alt - 40.0 {
2136            if let Some(SiteKindMeta::Dungeon(dungeon)) = site {
2137                SiteKindMeta::Dungeon(dungeon)
2138            } else {
2139                SiteKindMeta::Cave
2140            }
2141        } else {
2142            site.unwrap_or_default()
2143        }
2144    }
2145
2146    pub fn request_site_economy(&mut self, id: SiteId) {
2147        self.send_msg(ClientGeneral::RequestSiteInfo(id))
2148    }
2149
2150    pub fn inventories(&self) -> ReadStorage<comp::Inventory> { self.state.read_storage() }
2151
2152    /// Send a chat message to the server.
2153    pub fn send_chat(&mut self, message: String) {
2154        self.send_msg(ClientGeneral::ChatMsg(comp::Content::Plain(message)));
2155    }
2156
2157    /// Send a command to the server.
2158    pub fn send_command(&mut self, name: String, args: Vec<String>) {
2159        self.send_msg(ClientGeneral::Command(name, args));
2160    }
2161
2162    /// Remove all cached terrain
2163    pub fn clear_terrain(&mut self) {
2164        self.state.clear_terrain();
2165        self.pending_chunks.clear();
2166    }
2167
2168    pub fn place_block(&mut self, pos: Vec3<i32>, block: Block) {
2169        self.send_msg(ClientGeneral::PlaceBlock(pos, block));
2170    }
2171
2172    pub fn remove_block(&mut self, pos: Vec3<i32>) {
2173        self.send_msg(ClientGeneral::BreakBlock(pos));
2174    }
2175
2176    pub fn collect_block(&mut self, pos: Vec3<i32>) {
2177        self.control_action(ControlAction::InventoryAction(InventoryAction::Collect(
2178            pos,
2179        )));
2180    }
2181
2182    pub fn perform_dialogue(&mut self, target: EcsEntity, dialogue: rtsim::Dialogue) {
2183        if let Some(target_uid) = self.state.read_component_copied(target) {
2184            // TODO: Add a way to do send-only chat
2185            // if let Some(msg) = dialogue.message().cloned() {
2186            //     self.send_msg(ClientGeneral::ChatMsg(msg));
2187            // }
2188            self.control_event(ControlEvent::Dialogue(target_uid, dialogue));
2189        }
2190    }
2191
2192    pub fn change_ability(&mut self, slot: usize, new_ability: comp::ability::AuxiliaryAbility) {
2193        let auxiliary_key = self
2194            .inventories()
2195            .get(self.entity())
2196            .map_or((None, None), |inv| {
2197                let tool_kind = |slot| {
2198                    inv.equipped(slot).and_then(|item| match &*item.kind() {
2199                        ItemKind::Tool(tool) => Some(tool.kind),
2200                        _ => None,
2201                    })
2202                };
2203
2204                (
2205                    tool_kind(EquipSlot::ActiveMainhand),
2206                    tool_kind(EquipSlot::ActiveOffhand),
2207                )
2208            });
2209
2210        self.send_msg(ClientGeneral::ControlEvent(ControlEvent::ChangeAbility {
2211            slot,
2212            auxiliary_key,
2213            new_ability,
2214        }))
2215    }
2216
2217    pub fn set_battle_mode(&mut self, battle_mode: BattleMode) {
2218        self.send_msg(ClientGeneral::SetBattleMode(battle_mode));
2219    }
2220
2221    pub fn get_battle_mode(&self) -> BattleMode {
2222        let uid = if let Some(uid) = self.uid() {
2223            uid
2224        } else {
2225            error!("Client entity does not have a Uid component");
2226
2227            return BattleMode::PvP;
2228        };
2229        let player_info = if let Some(player_info) = self.player_list.get(&uid) {
2230            player_info
2231        } else {
2232            error!("Client does not have PlayerInfo for its Uid");
2233
2234            return BattleMode::PvP;
2235        };
2236
2237        player_info.battle_mode
2238    }
2239
2240    /// Execute a single client tick, handle input and update the game state by
2241    /// the given duration.
2242    pub fn tick(&mut self, inputs: ControllerInputs, dt: Duration) -> Result<Vec<Event>, Error> {
2243        span!(_guard, "tick", "Client::tick");
2244        // This tick function is the centre of the Veloren universe. Most client-side
2245        // things are managed from here, and as such it's important that it
2246        // stays organised. Please consult the core developers before making
2247        // significant changes to this code. Here is the approximate order of
2248        // things. Please update it as this code changes.
2249        //
2250        // 1) Collect input from the frontend, apply input effects to the state of the
2251        //    game
2252        // 2) Handle messages from the server
2253        // 3) Go through any events (timer-driven or otherwise) that need handling and
2254        //    apply them to the state of the game
2255        // 4) Perform a single LocalState tick (i.e: update the world and entities in
2256        //    the world)
2257        // 5) Go through the terrain update queue and apply all changes to the terrain
2258        // 6) Sync information to the server
2259        // 7) Finish the tick, passing actions of the main thread back to the frontend
2260
2261        // 1) Handle input from frontend.
2262        // Pass character actions from frontend input to the player's entity.
2263        if self.presence.is_some() {
2264            prof_span!("handle and send inputs");
2265            if let Err(e) = self
2266                .state
2267                .ecs()
2268                .write_storage::<Controller>()
2269                .entry(self.entity())
2270                .map(|entry| {
2271                    entry
2272                        .or_insert_with(|| Controller {
2273                            inputs: inputs.clone(),
2274                            queued_inputs: BTreeMap::new(),
2275                            events: Vec::new(),
2276                            actions: Vec::new(),
2277                        })
2278                        .inputs = inputs.clone();
2279                })
2280            {
2281                let entry = self.entity();
2282                error!(
2283                    ?e,
2284                    ?entry,
2285                    "Couldn't access controller component on client entity"
2286                );
2287            }
2288            self.send_msg_err(ClientGeneral::ControllerInputs(Box::new(inputs)))?;
2289        }
2290
2291        // 2) Build up a list of events for this frame, to be passed to the frontend.
2292        let mut frontend_events = Vec::new();
2293
2294        // Prepare for new events
2295        {
2296            prof_span!("Last<CharacterState> comps update");
2297            let ecs = self.state.ecs();
2298            let mut last_character_states = ecs.write_storage::<comp::Last<CharacterState>>();
2299            for (entity, _, character_state) in (
2300                &ecs.entities(),
2301                &ecs.read_storage::<comp::Body>(),
2302                &ecs.read_storage::<CharacterState>(),
2303            )
2304                .join()
2305            {
2306                if let Some(l) = last_character_states
2307                    .entry(entity)
2308                    .ok()
2309                    .map(|l| l.or_insert_with(|| comp::Last(character_state.clone())))
2310                    // TODO: since this just updates when the variant changes we should
2311                    // just store the variant to avoid the clone overhead
2312                    .filter(|l| !character_state.same_variant(&l.0))
2313                {
2314                    *l = comp::Last(character_state.clone());
2315                }
2316            }
2317        }
2318
2319        // Handle new messages from the server.
2320        frontend_events.append(&mut self.handle_new_messages()?);
2321
2322        // 3) Update client local data
2323        // Check if the invite has timed out and remove if so
2324        if self
2325            .invite
2326            .is_some_and(|(_, timeout, dur, _)| timeout.elapsed() > dur)
2327        {
2328            self.invite = None;
2329        }
2330
2331        // Lerp the clientside weather.
2332        self.weather.update(&mut self.state.weather_grid_mut());
2333
2334        if let Some(target_tod) = self.target_time_of_day {
2335            let mut tod = self.state.ecs_mut().write_resource::<TimeOfDay>();
2336            tod.0 = target_tod.0;
2337            self.target_time_of_day = None;
2338        }
2339
2340        // Save dead hardcore character ids to avoid displaying in the character list
2341        // while the server is still in the process of deleting the character
2342        if self.current::<Hardcore>().is_some() && self.is_dead() {
2343            if let Some(PresenceKind::Character(character_id)) = self.presence {
2344                self.character_being_deleted = Some(character_id);
2345            }
2346        }
2347
2348        // 4) Tick the client's LocalState
2349        self.state.tick(
2350            Duration::from_secs_f64(dt.as_secs_f64() * self.dt_adjustment),
2351            true,
2352            None,
2353            &self.connected_server_constants,
2354            |_, _| {},
2355        );
2356
2357        // TODO: avoid emitting these in the first place OR actually use outcomes
2358        // generated locally on the client (if they can be deduplicated from
2359        // ones that the server generates or if the client can reliably generate
2360        // them (e.g. syncing skipping character states past certain
2361        // stages might skip points where outcomes are generated, however we might not
2362        // care about this?) and the server doesn't need to send them)
2363        let _ = self.state.ecs().fetch::<EventBus<Outcome>>().recv_all();
2364
2365        // 5) Terrain
2366        self.tick_terrain()?;
2367
2368        // Send a ping to the server once every second
2369        if self.state.get_program_time() - self.last_server_ping > 1. {
2370            self.send_msg_err(PingMsg::Ping)?;
2371            self.last_server_ping = self.state.get_program_time();
2372        }
2373
2374        // 6) Update the server about the player's physics attributes.
2375        if self.presence.is_some() {
2376            if let (Some(pos), Some(vel), Some(ori)) = (
2377                self.state.read_storage().get(self.entity()).cloned(),
2378                self.state.read_storage().get(self.entity()).cloned(),
2379                self.state.read_storage().get(self.entity()).cloned(),
2380            ) {
2381                self.in_game_stream.send(ClientGeneral::PlayerPhysics {
2382                    pos,
2383                    vel,
2384                    ori,
2385                    force_counter: self.force_update_counter,
2386                })?;
2387            }
2388        }
2389
2390        /*
2391        // Output debug metrics
2392        if log_enabled!(Level::Info) && self.tick % 600 == 0 {
2393            let metrics = self
2394                .state
2395                .terrain()
2396                .iter()
2397                .fold(ChonkMetrics::default(), |a, (_, c)| a + c.get_metrics());
2398            info!("{:?}", metrics);
2399        }
2400        */
2401
2402        // 7) Finish the tick, pass control back to the frontend.
2403        self.tick += 1;
2404        Ok(frontend_events)
2405    }
2406
2407    /// Clean up the client after a tick.
2408    pub fn cleanup(&mut self) {
2409        // Cleanup the local state
2410        self.state.cleanup();
2411    }
2412
2413    /// Handles terrain addition and removal.
2414    ///
2415    /// Removes old terrain chunks outside the view distance.
2416    /// Sends requests for missing chunks within the view distance.
2417    fn tick_terrain(&mut self) -> Result<(), Error> {
2418        let pos = self
2419            .state
2420            .read_storage::<comp::Pos>()
2421            .get(self.entity())
2422            .cloned();
2423        if let (Some(pos), Some(view_distance)) = (pos, self.view_distance) {
2424            prof_span!("terrain");
2425            let chunk_pos = self.state.terrain().pos_key(pos.0.map(|e| e as i32));
2426
2427            // Remove chunks that are too far from the player.
2428            let mut chunks_to_remove = Vec::new();
2429            self.state.terrain().iter().for_each(|(key, _)| {
2430                // Subtract 2 from the offset before computing squared magnitude
2431                // 1 for the chunks needed bordering other chunks for meshing
2432                // 1 as a buffer so that if the player moves back in that direction the chunks
2433                //   don't need to be reloaded
2434                // Take the minimum of the adjusted difference vs the view_distance + 1 to
2435                //   prevent magnitude_squared from overflowing
2436
2437                if (chunk_pos - key)
2438                    .map(|e: i32| (e.unsigned_abs()).saturating_sub(2).min(view_distance + 1))
2439                    .magnitude_squared()
2440                    > view_distance.pow(2)
2441                {
2442                    chunks_to_remove.push(key);
2443                }
2444            });
2445            for key in chunks_to_remove {
2446                self.state.remove_chunk(key);
2447            }
2448
2449            let mut current_tick_send_chunk_requests = 0;
2450            // Request chunks from the server.
2451            self.loaded_distance = ((view_distance * TerrainChunkSize::RECT_SIZE.x) as f32).powi(2);
2452            // +1 so we can find a chunk that's outside the vd for better fog
2453            for dist in 0..view_distance as i32 + 1 {
2454                // Only iterate through chunks that need to be loaded for circular vd
2455                // The (dist - 2) explained:
2456                // -0.5 because a chunk is visible if its corner is within the view distance
2457                // -0.5 for being able to move to the corner of the current chunk
2458                // -1 because chunks are not meshed if they don't have all their neighbors
2459                //     (notice also that view_distance is decreased by 1)
2460                //     (this subtraction on vd is omitted elsewhere in order to provide
2461                //     a buffer layer of loaded chunks)
2462                let top = if 2 * (dist - 2).max(0).pow(2) > (view_distance - 1).pow(2) as i32 {
2463                    ((view_distance - 1).pow(2) as f32 - (dist - 2).pow(2) as f32)
2464                        .sqrt()
2465                        .round() as i32
2466                        + 1
2467                } else {
2468                    dist
2469                };
2470
2471                let mut skip_mode = false;
2472                for i in -top..top + 1 {
2473                    let keys = [
2474                        chunk_pos + Vec2::new(dist, i),
2475                        chunk_pos + Vec2::new(i, dist),
2476                        chunk_pos + Vec2::new(-dist, i),
2477                        chunk_pos + Vec2::new(i, -dist),
2478                    ];
2479
2480                    for key in keys.iter() {
2481                        let dist_to_player = (TerrainGrid::key_chunk(*key).map(|x| x as f32)
2482                            + TerrainChunkSize::RECT_SIZE.map(|x| x as f32) / 2.0)
2483                            .distance_squared(pos.0.into());
2484
2485                        let terrain = self.state.terrain();
2486                        if let Some(chunk) = terrain.get_key_arc(*key) {
2487                            if !skip_mode && !terrain.contains_key_real(*key) {
2488                                let chunk = Arc::clone(chunk);
2489                                drop(terrain);
2490                                self.state.insert_chunk(*key, chunk);
2491                            }
2492                        } else {
2493                            drop(terrain);
2494                            if !skip_mode && !self.pending_chunks.contains_key(key) {
2495                                const TOTAL_PENDING_CHUNKS_LIMIT: usize = 12;
2496                                const CURRENT_TICK_PENDING_CHUNKS_LIMIT: usize = 2;
2497                                if self.pending_chunks.len() < TOTAL_PENDING_CHUNKS_LIMIT
2498                                    && current_tick_send_chunk_requests
2499                                        < CURRENT_TICK_PENDING_CHUNKS_LIMIT
2500                                {
2501                                    self.send_msg_err(ClientGeneral::TerrainChunkRequest {
2502                                        key: *key,
2503                                    })?;
2504                                    current_tick_send_chunk_requests += 1;
2505                                    self.pending_chunks.insert(*key, Instant::now());
2506                                } else {
2507                                    skip_mode = true;
2508                                }
2509                            }
2510
2511                            if dist_to_player < self.loaded_distance {
2512                                self.loaded_distance = dist_to_player;
2513                            }
2514                        }
2515                    }
2516                }
2517            }
2518            self.loaded_distance = self.loaded_distance.sqrt()
2519                - ((TerrainChunkSize::RECT_SIZE.x as f32 / 2.0).powi(2)
2520                    + (TerrainChunkSize::RECT_SIZE.y as f32 / 2.0).powi(2))
2521                .sqrt();
2522
2523            // If chunks are taking too long, assume they're no longer pending.
2524            let now = Instant::now();
2525            self.pending_chunks
2526                .retain(|_, created| now.duration_since(*created) < Duration::from_secs(3));
2527        }
2528
2529        if let Some(lod_pos) = pos.map(|p| p.0.xy()).or(self.lod_pos_fallback) {
2530            // Manage LoD zones
2531            let lod_zone = lod_pos.map(|e| lod::from_wpos(e as i32));
2532
2533            // Request LoD zones that are in range
2534            if self
2535                .lod_last_requested
2536                .is_none_or(|i| i.elapsed() > Duration::from_secs(5))
2537            {
2538                if let Some(rpos) = Spiral2d::new()
2539                    .take((1 + self.lod_distance.ceil() as i32 * 2).pow(2) as usize)
2540                    .filter(|rpos| !self.lod_zones.contains_key(&(lod_zone + *rpos)))
2541                    .min_by_key(|rpos| rpos.magnitude_squared())
2542                    .filter(|rpos| {
2543                        rpos.map(|e| e as f32).magnitude() < (self.lod_distance - 0.5).max(0.0)
2544                    })
2545                {
2546                    self.send_msg_err(ClientGeneral::LodZoneRequest {
2547                        key: lod_zone + rpos,
2548                    })?;
2549                    self.lod_last_requested = Some(Instant::now());
2550                }
2551            }
2552
2553            // Cull LoD zones out of range
2554            self.lod_zones.retain(|p, _| {
2555                (*p - lod_zone).map(|e| e as f32).magnitude_squared() < self.lod_distance.powi(2)
2556            });
2557        }
2558
2559        Ok(())
2560    }
2561
2562    fn handle_server_msg(
2563        &mut self,
2564        frontend_events: &mut Vec<Event>,
2565        msg: ServerGeneral,
2566    ) -> Result<(), Error> {
2567        prof_span!("handle_server_msg");
2568        match msg {
2569            ServerGeneral::Disconnect(reason) => match reason {
2570                DisconnectReason::Shutdown => return Err(Error::ServerShutdown),
2571                DisconnectReason::Kicked(reason) => return Err(Error::Kicked(reason)),
2572                DisconnectReason::Banned(info) => return Err(Error::Banned(info)),
2573            },
2574            ServerGeneral::PlayerListUpdate(PlayerListUpdate::Init(list)) => {
2575                self.player_list = list
2576            },
2577            ServerGeneral::PlayerListUpdate(PlayerListUpdate::Add(uid, player_info)) => {
2578                if let Some(old_player_info) = self.player_list.insert(uid, player_info.clone()) {
2579                    warn!(
2580                        "Received msg to insert {} with uid {} into the player list but there was \
2581                         already an entry for {} with the same uid that was overwritten!",
2582                        player_info.player_alias, uid, old_player_info.player_alias
2583                    );
2584                }
2585            },
2586            ServerGeneral::PlayerListUpdate(PlayerListUpdate::Moderator(uid, moderator)) => {
2587                if let Some(player_info) = self.player_list.get_mut(&uid) {
2588                    player_info.is_moderator = moderator;
2589                } else {
2590                    warn!(
2591                        "Received msg to update admin status of uid {}, but they were not in the \
2592                         list.",
2593                        uid
2594                    );
2595                }
2596            },
2597            ServerGeneral::PlayerListUpdate(PlayerListUpdate::SelectedCharacter(
2598                uid,
2599                char_info,
2600            )) => {
2601                if let Some(player_info) = self.player_list.get_mut(&uid) {
2602                    player_info.character = Some(char_info);
2603                } else {
2604                    warn!(
2605                        "Received msg to update character info for uid {}, but they were not in \
2606                         the list.",
2607                        uid
2608                    );
2609                }
2610            },
2611            ServerGeneral::PlayerListUpdate(PlayerListUpdate::ExitCharacter(uid)) => {
2612                if let Some(player_info) = self.player_list.get_mut(&uid) {
2613                    if player_info.character.is_none() {
2614                        debug!(?player_info.player_alias, ?uid, "Received PlayerListUpdate::ExitCharacter for a player who wasnt ingame");
2615                    }
2616                    player_info.character = None;
2617                } else {
2618                    debug!(
2619                        ?uid,
2620                        "Received PlayerListUpdate::ExitCharacter for a nonexitent player"
2621                    );
2622                }
2623            },
2624            ServerGeneral::PlayerListUpdate(PlayerListUpdate::Remove(uid)) => {
2625                // Instead of removing players, mark them as offline because we need to
2626                // remember the names of disconnected players in chat.
2627                //
2628                // TODO: consider alternatives since this leads to an ever growing list as
2629                // players log out and in. Keep in mind we might only want to
2630                // keep only so many messages in chat the history. We could
2631                // potentially use an ID that's more persistent than the Uid.
2632                // One of the reasons we don't just store the string of the player name
2633                // into the message is to make alias changes reflected in older messages.
2634
2635                if let Some(player_info) = self.player_list.get_mut(&uid) {
2636                    if player_info.is_online {
2637                        player_info.is_online = false;
2638                    } else {
2639                        warn!(
2640                            "Received msg to remove uid {} from the player list by they were \
2641                             already marked offline",
2642                            uid
2643                        );
2644                    }
2645                } else {
2646                    warn!(
2647                        "Received msg to remove uid {} from the player list by they weren't in \
2648                         the list!",
2649                        uid
2650                    );
2651                }
2652            },
2653            ServerGeneral::PlayerListUpdate(PlayerListUpdate::Alias(uid, new_name)) => {
2654                if let Some(player_info) = self.player_list.get_mut(&uid) {
2655                    player_info.player_alias = new_name;
2656                } else {
2657                    warn!(
2658                        "Received msg to alias player with uid {} to {} but this uid is not in \
2659                         the player list",
2660                        uid, new_name
2661                    );
2662                }
2663            },
2664            ServerGeneral::PlayerListUpdate(PlayerListUpdate::UpdateBattleMode(
2665                uid,
2666                battle_mode,
2667            )) => {
2668                if let Some(player_info) = self.player_list.get_mut(&uid) {
2669                    player_info.battle_mode = battle_mode;
2670                } else {
2671                    warn!(
2672                        "Received msg to update battle mode of uid {} to {:?} but this uid is not \
2673                         in the player list",
2674                        uid, battle_mode
2675                    );
2676                }
2677            },
2678            ServerGeneral::ChatMsg(m) => frontend_events.push(Event::Chat(m)),
2679            ServerGeneral::ChatMode(m) => {
2680                self.chat_mode = m;
2681            },
2682            ServerGeneral::SetPlayerEntity(uid) => {
2683                if let Some(entity) = self.state.ecs().entity_from_uid(uid) {
2684                    let old_player_entity = mem::replace(
2685                        &mut *self.state.ecs_mut().write_resource(),
2686                        PlayerEntity(Some(entity)),
2687                    );
2688                    if let Some(old_entity) = old_player_entity.0 {
2689                        // Transfer controller to the new entity.
2690                        let mut controllers = self.state.ecs().write_storage::<Controller>();
2691                        if let Some(controller) = controllers.remove(old_entity) {
2692                            if let Err(e) = controllers.insert(entity, controller) {
2693                                error!(
2694                                    ?e,
2695                                    "Failed to insert controller when setting new player entity!"
2696                                );
2697                            }
2698                        }
2699                    }
2700                    if let Some(presence) = self.presence {
2701                        self.presence = Some(match presence {
2702                            PresenceKind::Spectator => PresenceKind::Spectator,
2703                            PresenceKind::LoadingCharacter(_) => PresenceKind::Possessor,
2704                            PresenceKind::Character(_) => PresenceKind::Possessor,
2705                            PresenceKind::Possessor => PresenceKind::Possessor,
2706                        });
2707                    }
2708                    // Clear pending trade
2709                    self.pending_trade = None;
2710                } else {
2711                    return Err(Error::Other("Failed to find entity from uid.".into()));
2712                }
2713            },
2714            ServerGeneral::TimeOfDay(time_of_day, calendar, new_time, time_scale) => {
2715                self.target_time_of_day = Some(time_of_day);
2716                *self.state.ecs_mut().write_resource() = calendar;
2717                *self.state.ecs_mut().write_resource() = time_scale;
2718                let mut time = self.state.ecs_mut().write_resource::<Time>();
2719                // Avoid side-eye from Einstein
2720                // If new time from server is at least 5 seconds ahead, replace client time.
2721                // Otherwise try to slightly twean client time (by 1%) to keep it in line with
2722                // server time.
2723                self.dt_adjustment = if new_time.0 > time.0 + 5.0 {
2724                    *time = new_time;
2725                    1.0
2726                } else if new_time.0 > time.0 {
2727                    1.01
2728                } else {
2729                    0.99
2730                };
2731            },
2732            ServerGeneral::EntitySync(entity_sync_package) => {
2733                let uid = self.uid();
2734                self.state
2735                    .ecs_mut()
2736                    .apply_entity_sync_package(entity_sync_package, uid);
2737            },
2738            ServerGeneral::CompSync(comp_sync_package, force_counter) => {
2739                self.force_update_counter = force_counter;
2740                self.state
2741                    .ecs_mut()
2742                    .apply_comp_sync_package(comp_sync_package);
2743            },
2744            ServerGeneral::CreateEntity(entity_package) => {
2745                self.state.ecs_mut().apply_entity_package(entity_package);
2746            },
2747            ServerGeneral::DeleteEntity(entity_uid) => {
2748                if self.uid() != Some(entity_uid) {
2749                    self.state
2750                        .ecs_mut()
2751                        .delete_entity_and_clear_uid_mapping(entity_uid);
2752                }
2753            },
2754            ServerGeneral::Notification(n) => {
2755                frontend_events.push(Event::Notification(n));
2756            },
2757            ServerGeneral::PluginData(d) => {
2758                let plugin_len = d.len();
2759                tracing::info!(?plugin_len, "plugin data");
2760                frontend_events.push(Event::PluginDataReceived(d));
2761            },
2762            ServerGeneral::SetPlayerRole(role) => {
2763                debug!(?role, "Updating client role");
2764                self.role = role;
2765            },
2766            _ => unreachable!("Not a general msg"),
2767        }
2768        Ok(())
2769    }
2770
2771    fn handle_server_in_game_msg(
2772        &mut self,
2773        frontend_events: &mut Vec<Event>,
2774        msg: ServerGeneral,
2775    ) -> Result<(), Error> {
2776        prof_span!("handle_server_in_game_msg");
2777        match msg {
2778            ServerGeneral::GroupUpdate(change_notification) => {
2779                use comp::group::ChangeNotification::*;
2780                // Note: we use a hashmap since this would not work with entities outside
2781                // the view distance
2782                match change_notification {
2783                    Added(uid, role) => {
2784                        // Check if this is a newly formed group by looking for absence of
2785                        // other non pet group members
2786                        if !matches!(role, group::Role::Pet)
2787                            && !self
2788                                .group_members
2789                                .values()
2790                                .any(|r| !matches!(r, group::Role::Pet))
2791                        {
2792                            frontend_events
2793                                // TODO: localise
2794                                .push(Event::Chat(comp::ChatType::Meta.into_plain_msg(
2795                                    "Type /g or /group to chat with your group members",
2796                                )));
2797                        }
2798                        if let Some(player_info) = self.player_list.get(&uid) {
2799                            frontend_events.push(Event::Chat(
2800                                // TODO: localise
2801                                comp::ChatType::GroupMeta("Group".into()).into_plain_msg(format!(
2802                                    "[{}] joined group",
2803                                    self.personalize_alias(uid, player_info.player_alias.clone())
2804                                )),
2805                            ));
2806                        }
2807                        if self.group_members.insert(uid, role) == Some(role) {
2808                            warn!(
2809                                "Received msg to add uid {} to the group members but they were \
2810                                 already there",
2811                                uid
2812                            );
2813                        }
2814                    },
2815                    Removed(uid) => {
2816                        if let Some(player_info) = self.player_list.get(&uid) {
2817                            frontend_events.push(Event::Chat(
2818                                // TODO: localise
2819                                comp::ChatType::GroupMeta("Group".into()).into_plain_msg(format!(
2820                                    "[{}] left group",
2821                                    self.personalize_alias(uid, player_info.player_alias.clone())
2822                                )),
2823                            ));
2824                            frontend_events.push(Event::MapMarker(
2825                                comp::MapMarkerUpdate::GroupMember(uid, MapMarkerChange::Remove),
2826                            ));
2827                        }
2828                        if self.group_members.remove(&uid).is_none() {
2829                            warn!(
2830                                "Received msg to remove uid {} from group members but by they \
2831                                 weren't in there!",
2832                                uid
2833                            );
2834                        }
2835                    },
2836                    NewLeader(leader) => {
2837                        self.group_leader = Some(leader);
2838                    },
2839                    NewGroup { leader, members } => {
2840                        self.group_leader = Some(leader);
2841                        self.group_members = members.into_iter().collect();
2842                        // Currently add/remove messages treat client as an implicit member
2843                        // of the group whereas this message explicitly includes them so to
2844                        // be consistent for now we will remove the client from the
2845                        // received hashset
2846                        if let Some(uid) = self.uid() {
2847                            self.group_members.remove(&uid);
2848                        }
2849                        frontend_events.push(Event::MapMarker(comp::MapMarkerUpdate::ClearGroup));
2850                    },
2851                    NoGroup => {
2852                        self.group_leader = None;
2853                        self.group_members = HashMap::new();
2854                        frontend_events.push(Event::MapMarker(comp::MapMarkerUpdate::ClearGroup));
2855                    },
2856                }
2857            },
2858            ServerGeneral::Invite {
2859                inviter,
2860                timeout,
2861                kind,
2862            } => {
2863                self.invite = Some((inviter, Instant::now(), timeout, kind));
2864            },
2865            ServerGeneral::InvitePending(uid) => {
2866                if !self.pending_invites.insert(uid) {
2867                    warn!("Received message about pending invite that was already pending");
2868                }
2869            },
2870            ServerGeneral::InviteComplete {
2871                target,
2872                answer,
2873                kind,
2874            } => {
2875                if !self.pending_invites.remove(&target) {
2876                    warn!(
2877                        "Received completed invite message for invite that was not in the list of \
2878                         pending invites"
2879                    )
2880                }
2881                frontend_events.push(Event::InviteComplete {
2882                    target,
2883                    answer,
2884                    kind,
2885                });
2886            },
2887            ServerGeneral::GroupInventoryUpdate(item, uid) => {
2888                frontend_events.push(Event::GroupInventoryUpdate(item, uid));
2889            },
2890            // Cleanup for when the client goes back to the `presence = None`
2891            ServerGeneral::ExitInGameSuccess => {
2892                self.presence = None;
2893                self.clean_state();
2894            },
2895            ServerGeneral::InventoryUpdate(inventory, events) => {
2896                let mut update_inventory = false;
2897                for event in events.iter() {
2898                    match event {
2899                        InventoryUpdateEvent::BlockCollectFailed { .. } => {},
2900                        InventoryUpdateEvent::EntityCollectFailed { .. } => {},
2901                        _ => update_inventory = true,
2902                    }
2903                }
2904                if update_inventory {
2905                    // Push the updated inventory component to the client
2906                    // FIXME: Figure out whether this error can happen under normal gameplay,
2907                    // if not find a better way to handle it, if so maybe consider kicking the
2908                    // client back to login?
2909                    let entity = self.entity();
2910                    if let Err(e) = self
2911                        .state
2912                        .ecs_mut()
2913                        .write_storage()
2914                        .insert(entity, inventory)
2915                    {
2916                        warn!(
2917                            ?e,
2918                            "Received an inventory update event for client entity, but this \
2919                             entity was not found... this may be a bug."
2920                        );
2921                    }
2922                }
2923
2924                self.update_available_recipes();
2925
2926                frontend_events.push(Event::InventoryUpdated(events));
2927            },
2928            ServerGeneral::Dialogue(sender, dialogue) => {
2929                frontend_events.push(Event::Dialogue(sender, dialogue));
2930            },
2931            ServerGeneral::SetViewDistance(vd) => {
2932                self.view_distance = Some(vd);
2933                frontend_events.push(Event::SetViewDistance(vd));
2934                // If the server is correcting client vd selection we assume this is the max
2935                // allowed view distance.
2936                self.server_view_distance_limit = Some(vd);
2937            },
2938            ServerGeneral::Outcomes(outcomes) => {
2939                frontend_events.extend(outcomes.into_iter().map(Event::Outcome))
2940            },
2941            ServerGeneral::Knockback(impulse) => {
2942                self.state
2943                    .ecs()
2944                    .read_resource::<EventBus<LocalEvent>>()
2945                    .emit_now(LocalEvent::ApplyImpulse {
2946                        entity: self.entity(),
2947                        impulse,
2948                    });
2949            },
2950            ServerGeneral::UpdatePendingTrade(id, trade, pricing) => {
2951                trace!("UpdatePendingTrade {:?} {:?}", id, trade);
2952                self.pending_trade = Some((id, trade, pricing));
2953            },
2954            ServerGeneral::FinishedTrade(result) => {
2955                if let Some((_, trade, _)) = self.pending_trade.take() {
2956                    self.update_available_recipes();
2957                    frontend_events.push(Event::TradeComplete { result, trade })
2958                }
2959            },
2960            ServerGeneral::SiteEconomy(economy) => {
2961                if let Some(rich) = self.sites.get_mut(&economy.id) {
2962                    rich.economy = Some(economy);
2963                }
2964            },
2965            ServerGeneral::MapMarker(event) => {
2966                frontend_events.push(Event::MapMarker(event));
2967            },
2968            ServerGeneral::WeatherUpdate(weather) => {
2969                self.weather.weather_update(weather);
2970            },
2971            ServerGeneral::LocalWindUpdate(wind) => {
2972                self.weather.local_wind_update(wind);
2973            },
2974            ServerGeneral::SpectatePosition(pos) => {
2975                frontend_events.push(Event::SpectatePosition(pos));
2976            },
2977            ServerGeneral::UpdateRecipes => {
2978                self.update_available_recipes();
2979            },
2980            _ => unreachable!("Not a in_game message"),
2981        }
2982        Ok(())
2983    }
2984
2985    fn handle_server_terrain_msg(&mut self, msg: ServerGeneral) -> Result<(), Error> {
2986        prof_span!("handle_server_terrain_mgs");
2987        match msg {
2988            ServerGeneral::TerrainChunkUpdate { key, chunk } => {
2989                if let Some(chunk) = chunk.ok().and_then(|c| c.to_chunk()) {
2990                    self.state.insert_chunk(key, Arc::new(chunk));
2991                }
2992                self.pending_chunks.remove(&key);
2993            },
2994            ServerGeneral::LodZoneUpdate { key, zone } => {
2995                self.lod_zones.insert(key, zone);
2996                self.lod_last_requested = None;
2997            },
2998            ServerGeneral::TerrainBlockUpdates(blocks) => {
2999                if let Some(mut blocks) = blocks.decompress() {
3000                    blocks.drain().for_each(|(pos, block)| {
3001                        self.state.set_block(pos, block);
3002                    });
3003                }
3004            },
3005            _ => unreachable!("Not a terrain message"),
3006        }
3007        Ok(())
3008    }
3009
3010    fn handle_server_character_screen_msg(
3011        &mut self,
3012        events: &mut Vec<Event>,
3013        msg: ServerGeneral,
3014    ) -> Result<(), Error> {
3015        prof_span!("handle_server_character_screen_msg");
3016        match msg {
3017            ServerGeneral::CharacterListUpdate(character_list) => {
3018                self.character_list.characters = character_list;
3019                if self.character_being_deleted.is_some() {
3020                    if let Some(pos) = self
3021                        .character_list
3022                        .characters
3023                        .iter()
3024                        .position(|x| x.character.id == self.character_being_deleted)
3025                    {
3026                        self.character_list.characters.remove(pos);
3027                    } else {
3028                        self.character_being_deleted = None;
3029                    }
3030                }
3031                self.character_list.loading = false;
3032            },
3033            ServerGeneral::CharacterActionError(error) => {
3034                warn!("CharacterActionError: {:?}.", error);
3035                events.push(Event::CharacterError(error));
3036            },
3037            ServerGeneral::CharacterDataLoadResult(Ok(metadata)) => {
3038                trace!("Handling join result by server");
3039                events.push(Event::CharacterJoined(metadata));
3040            },
3041            ServerGeneral::CharacterDataLoadResult(Err(error)) => {
3042                trace!("Handling join error by server");
3043                self.presence = None;
3044                self.clean_state();
3045                events.push(Event::CharacterError(error));
3046            },
3047            ServerGeneral::CharacterCreated(character_id) => {
3048                events.push(Event::CharacterCreated(character_id));
3049            },
3050            ServerGeneral::CharacterEdited(character_id) => {
3051                events.push(Event::CharacterEdited(character_id));
3052            },
3053            ServerGeneral::CharacterSuccess => debug!("client is now in ingame state on server"),
3054            ServerGeneral::SpectatorSuccess(spawn_point) => {
3055                events.push(Event::StartSpectate(spawn_point));
3056                debug!("client is now in ingame state on server");
3057            },
3058            _ => unreachable!("Not a character_screen msg"),
3059        }
3060        Ok(())
3061    }
3062
3063    fn handle_ping_msg(&mut self, msg: PingMsg) -> Result<(), Error> {
3064        prof_span!("handle_ping_msg");
3065        match msg {
3066            PingMsg::Ping => {
3067                self.send_msg_err(PingMsg::Pong)?;
3068            },
3069            PingMsg::Pong => {
3070                self.last_server_pong = self.state.get_program_time();
3071                self.last_ping_delta = self.state.get_program_time() - self.last_server_ping;
3072
3073                // Maintain the correct number of deltas for calculating the rolling average
3074                // ping. The client sends a ping to the server every second so we should be
3075                // receiving a pong reply roughly every second.
3076                while self.ping_deltas.len() > PING_ROLLING_AVERAGE_SECS - 1 {
3077                    self.ping_deltas.pop_front();
3078                }
3079                self.ping_deltas.push_back(self.last_ping_delta);
3080            },
3081        }
3082        Ok(())
3083    }
3084
3085    fn handle_messages(&mut self, frontend_events: &mut Vec<Event>) -> Result<u64, Error> {
3086        let mut cnt = 0;
3087        #[cfg(feature = "tracy")]
3088        let (mut terrain_cnt, mut ingame_cnt) = (0, 0);
3089        loop {
3090            let cnt_start = cnt;
3091
3092            while let Some(msg) = self.general_stream.try_recv()? {
3093                cnt += 1;
3094                self.handle_server_msg(frontend_events, msg)?;
3095            }
3096            while let Some(msg) = self.ping_stream.try_recv()? {
3097                cnt += 1;
3098                self.handle_ping_msg(msg)?;
3099            }
3100            while let Some(msg) = self.character_screen_stream.try_recv()? {
3101                cnt += 1;
3102                self.handle_server_character_screen_msg(frontend_events, msg)?;
3103            }
3104            while let Some(msg) = self.in_game_stream.try_recv()? {
3105                cnt += 1;
3106                #[cfg(feature = "tracy")]
3107                {
3108                    ingame_cnt += 1;
3109                }
3110                self.handle_server_in_game_msg(frontend_events, msg)?;
3111            }
3112            while let Some(msg) = self.terrain_stream.try_recv()? {
3113                cnt += 1;
3114                #[cfg(feature = "tracy")]
3115                {
3116                    if let ServerGeneral::TerrainChunkUpdate { chunk, .. } = &msg {
3117                        terrain_cnt += chunk.as_ref().map(|x| x.approx_len()).unwrap_or(0);
3118                    }
3119                }
3120                self.handle_server_terrain_msg(msg)?;
3121            }
3122
3123            if cnt_start == cnt {
3124                #[cfg(feature = "tracy")]
3125                {
3126                    plot!("terrain_recvs", terrain_cnt as f64);
3127                    plot!("ingame_recvs", ingame_cnt as f64);
3128                }
3129                return Ok(cnt);
3130            }
3131        }
3132    }
3133
3134    /// Handle new server messages.
3135    fn handle_new_messages(&mut self) -> Result<Vec<Event>, Error> {
3136        prof_span!("handle_new_messages");
3137        let mut frontend_events = Vec::new();
3138
3139        // Check that we have an valid connection.
3140        // Use the last ping time as a 1s rate limiter, we only notify the user once per
3141        // second
3142        if self.state.get_program_time() - self.last_server_ping > 1. {
3143            let duration_since_last_pong = self.state.get_program_time() - self.last_server_pong;
3144
3145            // Dispatch a notification to the HUD warning they will be kicked in {n} seconds
3146            const KICK_WARNING_AFTER_REL_TO_TIMEOUT_FRACTION: f64 = 0.75;
3147            if duration_since_last_pong
3148                >= (self.client_timeout.as_secs() as f64
3149                    * KICK_WARNING_AFTER_REL_TO_TIMEOUT_FRACTION)
3150                && self.state.get_program_time() - duration_since_last_pong > 0.
3151            {
3152                frontend_events.push(Event::DisconnectionNotification(
3153                    (self.state.get_program_time() - duration_since_last_pong).round() as u64,
3154                ));
3155            }
3156        }
3157
3158        let msg_count = self.handle_messages(&mut frontend_events)?;
3159
3160        if msg_count == 0
3161            && self.state.get_program_time() - self.last_server_pong
3162                > self.client_timeout.as_secs() as f64
3163        {
3164            return Err(Error::ServerTimeout);
3165        }
3166
3167        // ignore network events
3168        while let Some(res) = self
3169            .participant
3170            .as_mut()
3171            .and_then(|p| p.try_fetch_event().transpose())
3172        {
3173            let event = res?;
3174            trace!(?event, "received network event");
3175        }
3176
3177        Ok(frontend_events)
3178    }
3179
3180    pub fn entity(&self) -> EcsEntity {
3181        self.state
3182            .ecs()
3183            .read_resource::<PlayerEntity>()
3184            .0
3185            .expect("Client::entity should always have PlayerEntity be Some")
3186    }
3187
3188    pub fn uid(&self) -> Option<Uid> { self.state.read_component_copied(self.entity()) }
3189
3190    pub fn presence(&self) -> Option<PresenceKind> { self.presence }
3191
3192    pub fn registered(&self) -> bool { self.registered }
3193
3194    pub fn get_tick(&self) -> u64 { self.tick }
3195
3196    pub fn get_ping_ms(&self) -> f64 { self.last_ping_delta * 1000.0 }
3197
3198    pub fn get_ping_ms_rolling_avg(&self) -> f64 {
3199        let mut total_weight = 0.;
3200        let pings = self.ping_deltas.len() as f64;
3201        (self
3202            .ping_deltas
3203            .iter()
3204            .enumerate()
3205            .fold(0., |acc, (i, ping)| {
3206                let weight = i as f64 + 1. / pings;
3207                total_weight += weight;
3208                acc + (weight * ping)
3209            })
3210            / total_weight)
3211            * 1000.0
3212    }
3213
3214    /// Get a reference to the client's runtime thread pool. This pool should be
3215    /// used for any computationally expensive operations that run outside
3216    /// of the main thread (i.e., threads that block on I/O operations are
3217    /// exempt).
3218    pub fn runtime(&self) -> &Arc<Runtime> { &self.runtime }
3219
3220    /// Get a reference to the client's game state.
3221    pub fn state(&self) -> &State { &self.state }
3222
3223    /// Get a mutable reference to the client's game state.
3224    pub fn state_mut(&mut self) -> &mut State { &mut self.state }
3225
3226    /// Returns an iterator over the aliases of all the online players on the
3227    /// server
3228    pub fn players(&self) -> impl Iterator<Item = &str> {
3229        self.player_list()
3230            .values()
3231            .filter_map(|player_info| player_info.is_online.then_some(&*player_info.player_alias))
3232    }
3233
3234    /// Return true if this client is a moderator on the server
3235    pub fn is_moderator(&self) -> bool { self.role.is_some() }
3236
3237    pub fn role(&self) -> &Option<AdminRole> { &self.role }
3238
3239    /// Clean client ECS state
3240    fn clean_state(&mut self) {
3241        // Clear pending trade
3242        self.pending_trade = None;
3243
3244        let client_uid = self.uid().expect("Client doesn't have a Uid!!!");
3245
3246        // Clear ecs of all entities
3247        self.state.ecs_mut().delete_all();
3248        self.state.ecs_mut().maintain();
3249        self.state.ecs_mut().insert(IdMaps::default());
3250
3251        // Recreate client entity with Uid
3252        let entity_builder = self.state.ecs_mut().create_entity();
3253        entity_builder
3254            .world
3255            .write_resource::<IdMaps>()
3256            .add_entity(client_uid, entity_builder.entity);
3257
3258        let entity = entity_builder.with(client_uid).build();
3259        self.state.ecs().write_resource::<PlayerEntity>().0 = Some(entity);
3260    }
3261
3262    /// Change player alias to "You" if client belongs to matching player
3263    // TODO: move this to voxygen or i18n-helpers and properly localize there
3264    // or what's better, just remove completely, it won't properly work with
3265    // localization anyway.
3266    pub fn personalize_alias(&self, uid: Uid, alias: String) -> String {
3267        let client_uid = self.uid().expect("Client doesn't have a Uid!!!");
3268        if client_uid == uid {
3269            "You".to_string()
3270        } else {
3271            alias
3272        }
3273    }
3274
3275    /// Get important information from client that is necessary for message
3276    /// localisation
3277    pub fn lookup_msg_context(&self, msg: &comp::ChatMsg) -> ChatTypeContext {
3278        let mut result = ChatTypeContext {
3279            you: self.uid().expect("Client doesn't have a Uid!!!"),
3280            player_info: HashMap::new(),
3281            entity_name: HashMap::new(),
3282        };
3283
3284        let name_of_uid = |uid| {
3285            let ecs = self.state().ecs();
3286            let id_maps = ecs.read_resource::<common::uid::IdMaps>();
3287            id_maps.uid_entity(uid).and_then(|e| {
3288                ecs.read_storage::<comp::Stats>()
3289                    .get(e)
3290                    .map(|s| s.name.clone())
3291            })
3292        };
3293
3294        let mut add_data_of = |uid| {
3295            match self.player_list.get(uid) {
3296                Some(player_info) => {
3297                    result.player_info.insert(*uid, player_info.clone());
3298                },
3299                None => {
3300                    result
3301                        .entity_name
3302                        .insert(*uid, name_of_uid(*uid).unwrap_or_else(|| "<?>".to_string()));
3303                },
3304            };
3305        };
3306
3307        match &msg.chat_type {
3308            comp::ChatType::Online(uid) | comp::ChatType::Offline(uid) => add_data_of(uid),
3309            comp::ChatType::Kill(kill_source, victim) => {
3310                add_data_of(victim);
3311
3312                match kill_source {
3313                    KillSource::Player(attacker_uid, _) => {
3314                        add_data_of(attacker_uid);
3315                    },
3316                    KillSource::NonPlayer(_, _)
3317                    | KillSource::FallDamage
3318                    | KillSource::Suicide
3319                    | KillSource::NonExistent(_)
3320                    | KillSource::Other => (),
3321                };
3322            },
3323            comp::ChatType::Tell(from, to) | comp::ChatType::NpcTell(from, to) => {
3324                add_data_of(from);
3325                add_data_of(to);
3326            },
3327            comp::ChatType::Say(uid)
3328            | comp::ChatType::Region(uid)
3329            | comp::ChatType::World(uid)
3330            | comp::ChatType::NpcSay(uid)
3331            | comp::ChatType::Group(uid, _)
3332            | comp::ChatType::Faction(uid, _)
3333            | comp::ChatType::Npc(uid) => add_data_of(uid),
3334            comp::ChatType::CommandError
3335            | comp::ChatType::CommandInfo
3336            | comp::ChatType::FactionMeta(_)
3337            | comp::ChatType::GroupMeta(_)
3338            | comp::ChatType::Meta => (),
3339        };
3340        result
3341    }
3342
3343    /// Execute a single client tick:
3344    /// - handles messages from the server
3345    /// - sends physics update
3346    /// - requests chunks
3347    ///
3348    /// The game state is purposefully not simulated to reduce the overhead of
3349    /// running the client. This method is for use in testing a server with
3350    /// many clients connected.
3351    #[cfg(feature = "tick_network")]
3352    #[expect(clippy::needless_collect)] // False positive
3353    pub fn tick_network(&mut self, dt: Duration) -> Result<(), Error> {
3354        span!(_guard, "tick_network", "Client::tick_network");
3355        // Advance state time manually since we aren't calling `State::tick`
3356        self.state
3357            .ecs()
3358            .write_resource::<common::resources::ProgramTime>()
3359            .0 += dt.as_secs_f64();
3360
3361        let time_scale = *self
3362            .state
3363            .ecs()
3364            .read_resource::<common::resources::TimeScale>();
3365        self.state
3366            .ecs()
3367            .write_resource::<common::resources::Time>()
3368            .0 += dt.as_secs_f64() * time_scale.0;
3369
3370        // Handle new messages from the server.
3371        self.handle_new_messages()?;
3372
3373        // 5) Terrain
3374        self.tick_terrain()?;
3375        let empty = Arc::new(TerrainChunk::new(
3376            0,
3377            Block::empty(),
3378            Block::empty(),
3379            common::terrain::TerrainChunkMeta::void(),
3380        ));
3381        let mut terrain = self.state.terrain_mut();
3382        // Replace chunks with empty chunks to save memory
3383        let to_clear = terrain
3384            .iter()
3385            .filter_map(|(key, chunk)| (chunk.sub_chunks_len() != 0).then(|| key))
3386            .collect::<Vec<_>>();
3387        to_clear.into_iter().for_each(|key| {
3388            terrain.insert(key, Arc::clone(&empty));
3389        });
3390        drop(terrain);
3391
3392        // Send a ping to the server once every second
3393        if self.state.get_program_time() - self.last_server_ping > 1. {
3394            self.send_msg_err(PingMsg::Ping)?;
3395            self.last_server_ping = self.state.get_program_time();
3396        }
3397
3398        // 6) Update the server about the player's physics attributes.
3399        if self.presence.is_some() {
3400            if let (Some(pos), Some(vel), Some(ori)) = (
3401                self.state.read_storage().get(self.entity()).cloned(),
3402                self.state.read_storage().get(self.entity()).cloned(),
3403                self.state.read_storage().get(self.entity()).cloned(),
3404            ) {
3405                self.in_game_stream.send(ClientGeneral::PlayerPhysics {
3406                    pos,
3407                    vel,
3408                    ori,
3409                    force_counter: self.force_update_counter,
3410                })?;
3411            }
3412        }
3413
3414        // 7) Finish the tick, pass control back to the frontend.
3415        self.tick += 1;
3416
3417        Ok(())
3418    }
3419
3420    /// another plugin data received, is this the last one
3421    pub fn plugin_received(&mut self, hash: PluginHash) -> usize {
3422        if !self.missing_plugins.remove(&hash) {
3423            tracing::warn!(?hash, "received unrequested plugin");
3424        }
3425        self.missing_plugins.len()
3426    }
3427
3428    /// true if missing_plugins is not empty
3429    pub fn are_plugins_missing(&self) -> bool { !self.missing_plugins.is_empty() }
3430
3431    /// extract list of locally cached plugins to load
3432    pub fn take_local_plugins(&mut self) -> Vec<PathBuf> { std::mem::take(&mut self.local_plugins) }
3433}
3434
3435impl Drop for Client {
3436    fn drop(&mut self) {
3437        trace!("Dropping client");
3438        if self.registered {
3439            if let Err(e) = self.send_msg_err(ClientGeneral::Terminate) {
3440                warn!(
3441                    ?e,
3442                    "Error during drop of client, couldn't send disconnect package, is the \
3443                     connection already closed?",
3444                );
3445            }
3446        } else {
3447            trace!("no disconnect msg necessary as client wasn't registered")
3448        }
3449
3450        tokio::task::block_in_place(|| {
3451            if let Err(e) = self
3452                .runtime
3453                .block_on(self.participant.take().unwrap().disconnect())
3454            {
3455                warn!(?e, "error when disconnecting, couldn't send all data");
3456            }
3457        });
3458        //explicitly drop the network here while the runtime is still existing
3459        drop(self.network.take());
3460    }
3461}
3462
3463#[cfg(test)]
3464mod tests {
3465    use super::*;
3466    use client_i18n::LocalizationHandle;
3467
3468    #[test]
3469    /// THIS TEST VERIFIES THE CONSTANT API.
3470    /// CHANGING IT WILL BREAK 3rd PARTY APPLICATIONS (please extend) which
3471    /// needs to be informed (or fixed)
3472    ///  - torvus: https://gitlab.com/veloren/torvus
3473    ///
3474    /// CONTACT @Core Developer BEFORE MERGING CHANGES TO THIS TEST
3475    fn constant_api_test() {
3476        use common::clock::Clock;
3477        use voxygen_i18n_helpers::localize_chat_message;
3478
3479        const SPT: f64 = 1.0 / 60.0;
3480
3481        let runtime = Arc::new(Runtime::new().unwrap());
3482        let runtime2 = Arc::clone(&runtime);
3483        let username = "Foo";
3484        let password = "Bar";
3485        let auth_server = "auth.veloren.net";
3486        let veloren_client: Result<Client, Error> = runtime.block_on(Client::new(
3487            ConnectionArgs::Tcp {
3488                hostname: "127.0.0.1:9000".to_owned(),
3489                prefer_ipv6: false,
3490            },
3491            runtime2,
3492            &mut None,
3493            username,
3494            password,
3495            None,
3496            |suggestion: &str| suggestion == auth_server,
3497            &|_| {},
3498            |_| {},
3499            PathBuf::default(),
3500            ClientType::ChatOnly,
3501        ));
3502        let localisation = LocalizationHandle::load_expect("en");
3503
3504        let _ = veloren_client.map(|mut client| {
3505            //clock
3506            let mut clock = Clock::new(Duration::from_secs_f64(SPT));
3507
3508            //tick
3509            let events_result: Result<Vec<Event>, Error> =
3510                client.tick(ControllerInputs::default(), clock.dt());
3511
3512            //chat functionality
3513            client.send_chat("foobar".to_string());
3514
3515            let _ = events_result.map(|mut events| {
3516                // event handling
3517                if let Some(event) = events.pop() {
3518                    match event {
3519                        Event::Chat(msg) => {
3520                            let msg: comp::ChatMsg = msg;
3521                            let _s: String = localize_chat_message(
3522                                &msg,
3523                                &client.lookup_msg_context(&msg),
3524                                &localisation.read(),
3525                                true,
3526                            )
3527                            .1;
3528                        },
3529                        Event::Disconnect => {},
3530                        Event::DisconnectionNotification(_) => {
3531                            debug!("Will be disconnected soon! :/")
3532                        },
3533                        Event::Notification(notification) => {
3534                            let notification: Notification = notification;
3535                            debug!("Notification: {:?}", notification);
3536                        },
3537                        _ => {},
3538                    }
3539                };
3540            });
3541
3542            client.cleanup();
3543            clock.tick();
3544        });
3545    }
3546}