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