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