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