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