Skip to main content

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