Skip to main content

veloren_client/
lib.rs

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